FN-8171: expand active task thinking by default
Show Activity Live reasoning immediately for active and review-stage tasks. - Default thinking blocks open for in-progress and in-review tasks. - Preserve collapsed, user-toggleable reasoning for all other task columns. - Document behavior and cover default state and user collapse interactions. Files changed: .changeset/fn-8171-thinking-default-open.md | 7 ++++ docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/TaskChatTab.tsx | 20 +++++++--- .../app/components/__tests__/TaskChatTab.test.tsx | 43 ++++++++++++++++++++-- 4 files changed, 63 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-8171 Fusion-Task-Lineage: 705d6ac8-0c43-47d3-84df-57e9ec55a3b5 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8171-thinking-default-open.md
Normal file
7
.changeset/fn-8171-thinking-default-open.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Show active task reasoning by default in Activity Live logs.
|
||||
category: feature
|
||||
dev: TaskChatThinking defaults open for in-progress and in-review Activity Live tasks.
|
||||
@@ -1396,7 +1396,7 @@ Recommended workflow: ordinary chains stay as `Blocks N` so noise stays low, hig
|
||||
<!-- FNXC:TaskDetailActivityMobile 2026-07-03-21:30: On narrow mobile task-detail layouts, Activity view switching uses a fixed root-portaled Activity views menu so iOS viewport resize/scroll echoes during the tap cannot hide Live, Feed, or Raw Logs choices. -->
|
||||
<!-- FNXC:TaskDetailActivity 2026-07-04-18:37: The Activity views menu remains root-portaled to avoid tab/body clipping, but it is layered above and repositioned with its owning task-detail modal or task popup so drag/resize never leaves the menu behind or detached. -->
|
||||
<!-- FNXC:TaskDetailActivity 2026-07-04-19:10: FN-7536: the opening tap that shows the Activity views menu can itself trigger a same-gesture window resize/scroll echo (Android/mobile Chrome URL-bar collapse or tap-into-view auto-scroll, distinct from the iOS visualViewport echo above). That echo, and scrolling the `.detail-tabs` horizontal tab strip itself, now only reposition the open menu instead of closing it; a later, real viewport change still closes it as before. -->
|
||||
The **Activity** tab is the first task-detail tab by default and presents **Live**, **Feed**, and **Raw Logs** as a segmented control on wider layouts and as a fixed, root-portaled **Activity views** dropdown on narrow mobile layouts. The dropdown stays above its owning task-detail modal or task popup and follows the Activity tab while a popup is dragged or resized. Live contains the live, chat-styled transcript of task agent output. Consecutive entries are grouped by role and labeled as Planner, Executor, Reviewer, or Merger; legacy log rows without an agent role use the neutral Agent fallback. Agent group headers and user message headers show a small muted relative timestamp (for example, “just now”, “1m ago”, or “2h ago”) based on the transcript timestamp, while agent group metadata still includes the entry count. Consecutive text/message chunks inside a role group render as one continuous markdown bubble, while consecutive tool/tool-result/tool-error rows collapse into one expandable, compact tool-call summary that stays collapsed by default and mirrors regular Chat's dense treatment; the summary stays single-line/ellipsis-friendly on desktop and mobile, counts tool invocations, lists deduped tool names with overflow, and shows an error count when failures are present, while the expanded body pairs each call with its result or error in dense entry cards. Thinking entries render in a collapsible block that starts expanded. The transcript opens at the latest output whenever the tab loads or becomes active, then follows new live output when you are already near the bottom while preserving your scroll position when you review older messages. When older task-agent history exists, scrolling to the top or selecting **Load previous messages** prepends earlier transcript entries without moving the message you were reading. When you scroll away from the bottom of a populated transcript, a sticky **Latest** button appears inside the transcript so you can jump back to the newest message and resume live follow. For non-`done` tasks, the Activity Live composer sends typed guidance through the same steering path used by comments, including active planning/triage, `in-progress`, and `in-review` sessions, plus live CLI-agent sessions reported by the session bridge; an `in-review` Activity Live message or Comments-tab task comment re-engages an executor unless an open PR blocks moving the task back, and other messages are still saved as queued guidance when no session is currently live. Feed and Raw Logs do not show the composer. On a `done` task, the same composer starts a refinement task using the typed text as feedback and shows a success toast with the new task ID, while the current task detail modal remains on the completed task. The task-detail Activity Live segment keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally; its textarea placeholder reads “Steer the currently executing agent” for steering mode and switches to refinement copy for completed tasks, with the same inline, icon-only send affordance to the right of the input at every breakpoint. In the composer, plain **Enter** sends, **Shift+Enter** inserts a newline, and **Cmd/Ctrl+Enter** remains a supported send shortcut.
|
||||
The **Activity** tab is the first task-detail tab by default and presents **Live**, **Feed**, and **Raw Logs** as a segmented control on wider layouts and as a fixed, root-portaled **Activity views** dropdown on narrow mobile layouts. The dropdown stays above its owning task-detail modal or task popup and follows the Activity tab while a popup is dragged or resized. Live contains the live, chat-styled transcript of task agent output. Consecutive entries are grouped by role and labeled as Planner, Executor, Reviewer, or Merger; legacy log rows without an agent role use the neutral Agent fallback. Agent group headers and user message headers show a small muted relative timestamp (for example, “just now”, “1m ago”, or “2h ago”) based on the transcript timestamp, while agent group metadata still includes the entry count. Consecutive text/message chunks inside a role group render as one continuous markdown bubble, while consecutive tool/tool-result/tool-error rows collapse into one expandable, compact tool-call summary that stays collapsed by default and mirrors regular Chat's dense treatment; the summary stays single-line/ellipsis-friendly on desktop and mobile, counts tool invocations, lists deduped tool names with overflow, and shows an error count when failures are present, while the expanded body pairs each call with its result or error in dense entry cards. Thinking entries render in a collapsible block that starts expanded for `in-progress` and `in-review` tasks so active reasoning is visible at a glance; blocks start collapsed for other task columns and remain user-toggleable in every state. The transcript opens at the latest output whenever the tab loads or becomes active, then follows new live output when you are already near the bottom while preserving your scroll position when you review older messages. When older task-agent history exists, scrolling to the top or selecting **Load previous messages** prepends earlier transcript entries without moving the message you were reading. When you scroll away from the bottom of a populated transcript, a sticky **Latest** button appears inside the transcript so you can jump back to the newest message and resume live follow. For non-`done` tasks, the Activity Live composer sends typed guidance through the same steering path used by comments, including active planning/triage, `in-progress`, and `in-review` sessions, plus live CLI-agent sessions reported by the session bridge; an `in-review` Activity Live message or Comments-tab task comment re-engages an executor unless an open PR blocks moving the task back, and other messages are still saved as queued guidance when no session is currently live. Feed and Raw Logs do not show the composer. On a `done` task, the same composer starts a refinement task using the typed text as feedback and shows a success toast with the new task ID, while the current task detail modal remains on the completed task. The task-detail Activity Live segment keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally; its textarea placeholder reads “Steer the currently executing agent” for steering mode and switches to refinement copy for completed tasks, with the same inline, icon-only send affordance to the right of the input at every breakpoint. In the composer, plain **Enter** sends, **Shift+Enter** inserts a newline, and **Cmd/Ctrl+Enter** remains a supported send shortcut.
|
||||
|
||||
The top-level **Chat** tab opens the planner-model conversation for the same task instead of posting steering comments. It appears after Activity by default, or before Activity when **Settings → Appearance → Open task details with Chat first** is enabled. Each send includes server-built, bounded context for the task id, status/column/progress/current step, dependencies, recent activity/comment excerpts, prompt/plan content, and available source/review state; unavailable sections are labeled so the planner states uncertainty rather than inventing execution evidence. Opening the tab with no existing history does not create a database chat row; when no planner-chat history is found, Chat shows a guided empty state with starter prompts for recent activity, current status/blockers, next best action, and plan/definition review. Selecting a starter creates/resumes the planner chat and sends that prompt as an ordinary chat message through the task-context-aware planner-chat composer/stream path, including for completed tasks. On live tasks, clear bounded implementation-change requests are routed to task steering; on `done` tasks, clear follow-up implementation or improvement requests are routed through a task-scoped planner refinement tool that calls the same refinement creation path as the completed-task Activity composer. The starter prompts disappear while history is loading or after conversation history exists, so Activity Live, Feed, Raw Logs, and the steering/refinement composer remain separate. Planner Chat uses the same standard chat bubble, markdown/plain assistant rendering, thinking details, tool-call/question cards, and mobile first-tap send/stop affordance as the main Chat view while keeping task-scoped planner sessions separate. Planner Chat defaults to focused mode, keeps its composer visible at the bottom while only the transcript scrolls, and on narrow/mobile task-detail layouts collapses nonessential rows above the chat until the user selects the Chat collapse control.
|
||||
|
||||
|
||||
@@ -543,13 +543,22 @@ function TaskChatToolGroup({ entries }: { entries: AgentLogEntry[] }) {
|
||||
/*
|
||||
FNXC:Chat-Thinking 2026-07-15-10:33:
|
||||
Chat thinking (reasoning) blocks render collapsed by default so the response is scannable without manually closing each block; the summary remains an expand-on-click affordance. (FN-7974)
|
||||
|
||||
FNXC:Chat-Thinking 2026-07-16-18:05:
|
||||
FN-8171 keeps the scannable collapsed default for idle task columns, but opens Live Activity thinking for in-progress and in-review tasks so operators can follow active or awaiting-review reasoning at a glance. The expanded block remains user-collapsible.
|
||||
*/
|
||||
function TaskChatThinking({ entries }: { entries: AgentLogEntry[] }) {
|
||||
function TaskChatThinking({ entries, defaultOpen = false }: { entries: AgentLogEntry[]; defaultOpen?: boolean }) {
|
||||
const { t } = useTranslation("app");
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const combinedThinkingText = entries.map((entry) => entry.text).join("");
|
||||
|
||||
return (
|
||||
<details className="task-chat-thinking" data-testid="task-chat-thinking">
|
||||
<details
|
||||
className="task-chat-thinking"
|
||||
data-testid="task-chat-thinking"
|
||||
open={open}
|
||||
onToggle={(event) => setOpen(event.currentTarget.open)}
|
||||
>
|
||||
<summary className="task-chat-thinking-summary">
|
||||
<span>{t("taskChat.thinking", "Thinking")}</span>
|
||||
<TaskChatTimestamp timestamp={getLatestEntryTimestamp(entries)} label="Thinking block timestamp" />
|
||||
@@ -568,12 +577,12 @@ function TaskChatThinking({ entries }: { entries: AgentLogEntry[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TaskChatSegmentView({ segment }: { segment: TaskChatSegment }) {
|
||||
function TaskChatSegmentView({ segment, defaultOpen }: { segment: TaskChatSegment; defaultOpen?: boolean }) {
|
||||
if (segment.kind === "tool") {
|
||||
return <TaskChatToolGroup entries={segment.entries} />;
|
||||
}
|
||||
if (segment.kind === "thinking") {
|
||||
return <TaskChatThinking entries={segment.entries} />;
|
||||
return <TaskChatThinking entries={segment.entries} defaultOpen={defaultOpen} />;
|
||||
}
|
||||
return <TaskChatText entries={segment.entries} />;
|
||||
}
|
||||
@@ -616,6 +625,7 @@ export function TaskChatTab({ task, projectId, active, addToast, onTaskUpdated,
|
||||
const sendingRef = useRef(false);
|
||||
const [optimisticMessages, setOptimisticMessages] = useState<UserChatMessage[]>([]);
|
||||
const [isTranscriptAtBottom, setIsTranscriptAtBottom] = useState(true);
|
||||
const thinkingDefaultOpen = task.column === "in-progress" || task.column === "in-review";
|
||||
const transcriptRef = useRef<HTMLDivElement>(null);
|
||||
const previousEntryCountRef = useRef(0);
|
||||
const previousScrollHeightRef = useRef(0);
|
||||
@@ -979,7 +989,7 @@ export function TaskChatTab({ task, projectId, active, addToast, onTaskUpdated,
|
||||
<div className="task-chat-group-bubbles">
|
||||
{segments.map((segment) => {
|
||||
const segmentKey = `${segment.kind}-${segment.startIndex}-${segment.entries.length}`;
|
||||
return <TaskChatSegmentView key={segmentKey} segment={segment} />;
|
||||
return <TaskChatSegmentView key={segmentKey} segment={segment} defaultOpen={thinkingDefaultOpen} />;
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1039,13 +1039,13 @@ describe("TaskChatTab", () => {
|
||||
expect(within(standaloneEntry).getByLabelText("Tool entry timestamp")).toHaveTextContent(expectedTime);
|
||||
});
|
||||
|
||||
it("renders thinking in a collapsed-by-default expandable block", async () => {
|
||||
it("renders thinking in a collapsed-by-default expandable block for inactive tasks", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockLogs([
|
||||
makeEntry({ agent: "triage", type: "thinking", text: "I am considering options" }),
|
||||
]);
|
||||
|
||||
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||
render(<TaskChatTab task={makeTask({ column: "done" })} active addToast={vi.fn()} />);
|
||||
|
||||
const thinking = screen.getByTestId("task-chat-thinking");
|
||||
expect(thinking).not.toHaveAttribute("open");
|
||||
@@ -1064,6 +1064,43 @@ describe("TaskChatTab", () => {
|
||||
expect(screen.getByText("I am considering options")).not.toBeVisible();
|
||||
});
|
||||
|
||||
it.each(["in-progress", "in-review"] as const)("defaults thinking blocks open for %s tasks", (column) => {
|
||||
mockLogs([
|
||||
makeEntry({ agent: "executor", type: "thinking", text: "Live reasoning" }),
|
||||
]);
|
||||
|
||||
render(<TaskChatTab task={makeTask({ column })} active addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByTestId("task-chat-thinking")).toHaveAttribute("open");
|
||||
});
|
||||
|
||||
it.each(["todo", "done", "triage", "archived"] as const)("keeps thinking blocks collapsed for %s tasks", (column) => {
|
||||
mockLogs([
|
||||
makeEntry({ agent: "executor", type: "thinking", text: "Historical reasoning" }),
|
||||
]);
|
||||
|
||||
render(<TaskChatTab task={makeTask({ column })} active addToast={vi.fn()} />);
|
||||
|
||||
expect(screen.getByTestId("task-chat-thinking")).not.toHaveAttribute("open");
|
||||
});
|
||||
|
||||
it("lets users collapse auto-expanded thinking blocks", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockLogs([
|
||||
makeEntry({ agent: "executor", type: "thinking", text: "Active reasoning" }),
|
||||
]);
|
||||
|
||||
render(<TaskChatTab task={makeTask({ column: "in-progress" })} active addToast={vi.fn()} />);
|
||||
|
||||
const thinking = screen.getByTestId("task-chat-thinking");
|
||||
expect(thinking).toHaveAttribute("open");
|
||||
|
||||
await user.click(within(thinking).getByText("Thinking"));
|
||||
|
||||
expect(thinking).not.toHaveAttribute("open");
|
||||
expect(screen.getByText("Active reasoning")).not.toBeVisible();
|
||||
});
|
||||
|
||||
it("renders consecutive thinking entries as one continuous section", () => {
|
||||
mockLogs([
|
||||
makeEntry({ agent: "triage", type: "thinking", text: "First" }),
|
||||
@@ -1091,7 +1128,7 @@ describe("TaskChatTab", () => {
|
||||
makeEntry({ agent: "executor", type: "tool_result", text: "second tool", detail: "second detail" }),
|
||||
]);
|
||||
|
||||
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} />);
|
||||
render(<TaskChatTab task={makeTask({ column: "todo" })} active addToast={vi.fn()} />);
|
||||
|
||||
const toolGroups = screen.getAllByTestId("task-chat-tool-group");
|
||||
expect(toolGroups).toHaveLength(2);
|
||||
|
||||
Reference in New Issue
Block a user