FN-7325: rename Activity Current to Live
Renames the task-detail Activity Current segment to Live and makes expansion available across Activity views. - relabel Activity Current as Live while preserving the legacy internal current segment routing - move the Activity expand/collapse control to the shared Activity toolbar for Live, Feed, and Raw Logs - update expanded Activity layout, docs, tests, and release notes for the shared behavior Files changed: .changeset/fn-7325-activity-live-expand.md | 7 ++ docs/dashboard-guide.md | 8 +- packages/dashboard/app/components/TaskChatTab.tsx | 6 +- .../dashboard/app/components/TaskDetailModal.css | 53 ++++++++++- .../dashboard/app/components/TaskDetailModal.tsx | 103 ++++++++++++--------- .../app/components/__tests__/TaskChatTab.test.tsx | 12 +-- .../TaskDetailModal.attachments-and-tabs.test.tsx | 90 ++++++++++++++++-- .../__tests__/TaskDetailModal.rendering.test.tsx | 2 +- .../components/__tests__/TaskDetailModal.test.tsx | 8 +- 9 files changed, 213 insertions(+), 76 deletions(-) Fusion-Task-Id: FN-7325 Fusion-Task-Lineage: 1aee503b-ace8-4238-a9f9-3e536a43c1f8 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7325-activity-live-expand.md
Normal file
7
.changeset/fn-7325-activity-live-expand.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Rename task Activity Current to Live and allow expanding all Activity segments.
|
||||
category: feature
|
||||
dev: Keeps legacy Activity `current`, `chat`, and `logs` routing compatibility while sharing the expand control across Live, Feed, and Raw Logs.
|
||||
@@ -1041,7 +1041,7 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou
|
||||
|
||||
- Editable tasks with descriptions show **Summarize as title** beside the read-mode title; it asks AI to generate a concise title from the description and saves it without opening the edit form.
|
||||
- The **Summary** tab appears first for `done` tasks and is their default landing tab. It shows the recorded completion summary, changed-file/merge stats when available, completed steps, workflow results, retry counts, and a token usage & cost section broken down by model from the already-loaded task detail; unpriced models show cost as unavailable rather than `$0`. Non-`done` tasks still open on **Activity** by default.
|
||||
- The **Activity → Current** segment includes an expand/collapse control that lets the transcript and composer fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed.
|
||||
- The **Activity → Live**, **Feed**, and **Raw Logs** segments share an expand/collapse control that lets the active Activity segment fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed.
|
||||
- The top-level **Chat** tab appears immediately after **Activity** and starts a task-scoped planner-model conversation. It uses the task's effective planning model, persists messages in a resumable chat session, and sends bounded server-built task context so the planner can answer current status, progress, recent activity, dependency, and task definition questions without polluting visible chat history. It shows starter prompts for common planning questions, can render structured planner questions, and converts only explicit operator steering intent through the scoped steering tool. Activity remains the primary operational transcript/feed/raw-log surface.
|
||||
- Task-detail Activity steering comments are persisted as user comments/steering guidance and surfaced to every relevant agent lane: live executor sessions receive steering injection, while planner, reviewer (spec/plan/code), and merger agents (standard and clean-room AI merge/review) receive the latest user comments in their next prompt/pass.
|
||||
- The priority chip in task metadata is an inline picker: you can change priority directly without entering full edit mode.
|
||||
@@ -1118,10 +1118,10 @@ Recommended workflow: ordinary chains stay as `Blocks N` so noise stays low, hig
|
||||
|
||||
### Activity → Raw Logs view
|
||||
|
||||
<!-- FNXC:TaskDetailPlannerChat 2026-06-30-22:30: Activity Current is the explicit operational steering-comment entry surface. Feed and Raw Logs remain read-only Activity segments, and the top-level Chat tab is intentionally separate planner-model conversation rather than steering. -->
|
||||
The **Activity** tab is the first task-detail tab and presents a segmented control for **Current**, **Feed**, and **Raw Logs**. Current 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 **Steering comment** composer sends 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 Current 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 a steering composer. On a `done` task, the composer switches to **Refinement request** copy; sending 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 Current 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.
|
||||
<!-- FNXC:TaskDetailActivity 2026-06-30-23:55: Activity Live is the explicit operational steering-comment entry surface, preserving the legacy internal `current` segment id. Feed and Raw Logs remain read-only Activity segments, the Activity-wide expand control is available on every segment, and the top-level Chat tab is intentionally separate planner-model conversation rather than steering. -->
|
||||
The **Activity** tab is the first task-detail tab and presents a segmented control for **Live**, **Feed**, and **Raw Logs**. 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 **Steering comment** composer sends 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 a steering composer. On a `done` task, the composer switches to **Refinement request** copy; sending 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 adjacent top-level **Chat** tab opens the planner-model conversation for the same task instead of posting steering comments. 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. When no planner-chat history has loaded yet, Chat shows a guided empty state with starter prompts for recent activity, current status/blockers, next best action, and plan/definition review; selecting one sends that prompt as an ordinary chat message through the task-context-aware planner-chat composer/stream path. The starter prompts disappear while history is loading or after conversation history exists, so Activity Current, Feed, Raw Logs, and the steering composer remain separate.
|
||||
The adjacent top-level **Chat** tab opens the planner-model conversation for the same task instead of posting steering comments. 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. When no planner-chat history has loaded yet, Chat shows a guided empty state with starter prompts for recent activity, current status/blockers, next best action, and plan/definition review; selecting one sends that prompt as an ordinary chat message through the task-context-aware planner-chat composer/stream path. The starter prompts disappear while history is loading or after conversation history exists, so Activity Live, Feed, Raw Logs, and the steering composer remain separate.
|
||||
|
||||
The **Raw Logs** segment is designed for debugging long-running and tool-heavy sessions, while legacy links that requested the former top-level Logs tab land on Activity → Feed:
|
||||
|
||||
|
||||
@@ -594,7 +594,7 @@ export function TaskChatTab({ task, projectId, active, addToast, onTaskUpdated,
|
||||
const isDoneTask = task.column === "done";
|
||||
/*
|
||||
* FNXC:TaskDetailActivity 2026-06-30-21:51:
|
||||
* Activity → Current is the operational steering surface for task execution. Show an explicit steering-comment affordance here while keeping the future top-level planner-model Chat tab out of scope; Feed and Raw Logs remain read-only Activity segments without this composer.
|
||||
* Activity → Live (legacy `current`) is the operational steering surface for task execution. Show an explicit steering-comment affordance here while keeping the top-level planner-model Chat tab separate; Feed and Raw Logs remain read-only Activity segments without this composer.
|
||||
*/
|
||||
const composerLabel = isDoneTask
|
||||
? t("taskChat.refinementComposerLabel", "Refinement request")
|
||||
@@ -851,11 +851,11 @@ export function TaskChatTab({ task, projectId, active, addToast, onTaskUpdated,
|
||||
type="button"
|
||||
className="btn btn-icon btn-sm task-chat-expand-toggle task-chat-expand-toggle--overlay"
|
||||
onClick={onToggleExpanded}
|
||||
aria-label={expanded ? t("taskChat.collapseChat", "Collapse chat") : t("taskChat.expandChat", "Expand chat to full modal")}
|
||||
aria-label={expanded ? t("taskChat.collapseActivity", "Collapse activity") : t("taskChat.expandActivity", "Expand activity to full modal")}
|
||||
aria-pressed={expanded}
|
||||
data-testid="task-chat-expand-toggle"
|
||||
>
|
||||
{/* FNXC:TaskChat 2026-06-13-00:00: FN-6425 refines FN-6405 by keeping the task-chat expand affordance icon-only and pinned to the chat view corner so transcript scrolling never removes access to expansion controls. */}
|
||||
{/* FNXC:TaskDetailActivity 2026-06-30-23:55: The legacy TaskChatTab expand affordance now uses Activity-wide copy when a caller still renders it; TaskDetailModal owns the visible Activity-level toggle across Live, Feed, and Raw Logs. */}
|
||||
{expanded ? <Minimize2 aria-hidden="true" /> : <Maximize2 aria-hidden="true" />}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
@@ -128,8 +128,8 @@ Task detail workflow badges share the board badge's slight token-based icon-to-l
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* Activity → Current mirrors the Raw Logs fill-height layout: the modal body does not scroll;
|
||||
the transcript owns internal scrolling while the composer stays visible. */
|
||||
/* Activity → Live and expanded Activity mirror the Raw Logs fill-height layout: the modal body does not scroll;
|
||||
each segment owns internal scrolling while composers and controls stay visible. */
|
||||
.detail-body--chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -818,6 +818,9 @@ Expanded chat should take over the modal except for the task title row, so users
|
||||
|
||||
FNXC:TaskDetailChat 2026-06-22-13:27:
|
||||
Expanded chat must also cover the task metadata row — priority, execution mode, provenance/Created by, and Created/Updated timestamps — so the chat grows all the way up to the title row instead of stopping below secondary metadata controls.
|
||||
|
||||
FNXC:TaskDetailActivity 2026-06-30-23:55:
|
||||
The same expanded-mode chrome collapse now applies to all Activity segments: Live, Feed, and Raw Logs. Keep the legacy `--chat-expanded` class for test and styling compatibility while treating it as Activity-expanded behavior.
|
||||
*/
|
||||
.task-detail-content--chat-expanded .detail-meta {
|
||||
display: none;
|
||||
@@ -849,6 +852,19 @@ Expanded chat must also cover the task metadata row — priority, execution mode
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.task-detail-content--chat-expanded .detail-activity {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.task-detail-content--chat-expanded .detail-activity-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
.detail-spec-edit-trigger {
|
||||
display: flex;
|
||||
@@ -2355,9 +2371,17 @@ FNXC:TaskDetailTabs 2026-06-26-00:35:
|
||||
|
||||
/* === Activity Segmented Control === */
|
||||
/*
|
||||
FNXC:TaskDetailActivity 2026-06-30-22:15:
|
||||
The Activity top-level tab owns Current, Feed, and Raw Logs as an in-content segmented control. Keep this selector visually quieter than the top-level tab strip and horizontally reachable on narrow modal and embedded task-detail surfaces.
|
||||
FNXC:TaskDetailActivity 2026-06-30-23:55:
|
||||
The Activity top-level tab owns Live (internal `current`), Feed, and Raw Logs as an in-content segmented control. Keep this selector visually quieter than the top-level tab strip and horizontally reachable on narrow modal and embedded task-detail surfaces; the Activity-wide expand control must sit beside it on every segment.
|
||||
*/
|
||||
.activity-toolbar {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.activity-segmented-control {
|
||||
display: inline-flex;
|
||||
max-inline-size: 100%;
|
||||
@@ -2366,7 +2390,6 @@ The Activity top-level tab owns Current, Feed, and Raw Logs as an in-content seg
|
||||
border: var(--border-width) solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: calc(var(--space-xs) / 4);
|
||||
margin-bottom: var(--space-md);
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overscroll-behavior-inline: contain;
|
||||
@@ -2406,6 +2429,13 @@ The Activity top-level tab owns Current, Feed, and Raw Logs as an in-content seg
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.activity-expand-toggle {
|
||||
flex: 0 0 auto;
|
||||
min-inline-size: var(--space-2xl);
|
||||
min-block-size: var(--space-2xl);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.detail-log-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2419,6 +2449,19 @@ The Activity top-level tab owns Current, Feed, and Raw Logs as an in-content seg
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.activity-toolbar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.activity-segmented-control {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.activity-expand-toggle {
|
||||
min-inline-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
min-block-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
}
|
||||
|
||||
.detail-log-loading {
|
||||
min-height: calc(var(--space-2xl) + var(--space-lg));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "./TaskDetailModal.css";
|
||||
import React, { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2 } from "lucide-react";
|
||||
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2 } from "lucide-react";
|
||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
@@ -200,13 +200,16 @@ FNXC:TaskDetailActivityTab 2026-06-30-00:00:
|
||||
The existing task activity/steering surface keeps the stable internal `chat` tab id for deep-link/plugin compatibility, but its top-level user-facing label is Activity. Activity is the implicit default for active task columns; done tasks keep Summary as their omitted-initial-tab landing surface so completed work still opens on the completion report while Activity remains first in tab order.
|
||||
|
||||
FNXC:TaskDetailPlannerChat 2026-06-30-22:30:
|
||||
Task detail now separates Activity from planner-model Chat. `chat` remains the legacy Activity id for old links and Activity → Current/Feed/Raw Logs/steering, while `planner-chat` is the new top-level Chat tab for task-aware planning conversation and must render immediately after Activity.
|
||||
Task detail now separates Activity from planner-model Chat. `chat` remains the legacy Activity id for old links and Activity → Live (internal `current`)/Feed/Raw Logs/steering, while `planner-chat` is the new top-level Chat tab for task-aware planning conversation and must render immediately after Activity.
|
||||
|
||||
FNXC:TaskDetailActivity 2026-06-30-15:50:
|
||||
Only an omitted initial tab is the implicit default. Preserve explicit `initialTab="chat"` requests from plugins and task-detail entrypoints so existing links continue to open Activity → Current. Legacy `initialTab="logs"` now routes to Activity → Feed, and Raw Logs remains an Activity segment.
|
||||
Only an omitted initial tab is the implicit default. Preserve explicit `initialTab="chat"` requests from plugins and task-detail entrypoints so existing links continue to open Activity → Live (internal `current`). Legacy `initialTab="logs"` now routes to Activity → Feed, and Raw Logs remains an Activity segment.
|
||||
|
||||
FNXC:TaskDetailActivity 2026-06-30-21:55:
|
||||
The first Activity segment keeps the stable Current label for legacy segment tests and links, but its embedded composer labels the operational steering-comment affordance explicitly. Do not reuse this segment as planner-model Chat conversation; that belongs to the `planner-chat` top-level tab.
|
||||
The first Activity segment keeps the stable internal `current` id for legacy segment tests and links, but its embedded composer labels the operational steering-comment affordance explicitly. Do not reuse this segment as planner-model Chat conversation; that belongs to the `planner-chat` top-level tab.
|
||||
|
||||
FNXC:TaskDetailActivity 2026-06-30-23:55:
|
||||
The first Activity segment is user-facing Live while legacy internals remain `current` and explicit `initialTab="chat"` continues landing there for compatibility.
|
||||
*/
|
||||
function resolveDefaultTab(initialTab: TabId | undefined, column: ColumnId): TabId {
|
||||
if (initialTab === "retries") {
|
||||
@@ -532,7 +535,7 @@ export function TaskDetailContent({
|
||||
const fileBrowser = useFileBrowser();
|
||||
const [activeTab, setActiveTab] = useState<TabId>(() => resolveDefaultTab(initialTab, task.column));
|
||||
const [activitySegment, setActivitySegment] = useState<ActivitySegment>(() => resolveDefaultActivitySegment(initialTab));
|
||||
const [chatExpanded, setChatExpanded] = useState(false);
|
||||
const [activityExpanded, setActivityExpanded] = useState(false);
|
||||
|
||||
// ── CLI agent session (U11) ────────────────────────────────────────────────
|
||||
const [cliSession, setCliSession] = useState<CliSessionSummaryRecord | null>(null);
|
||||
@@ -844,10 +847,14 @@ export function TaskDetailContent({
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== "chat" || activitySegment !== "current" || isEditing) {
|
||||
setChatExpanded(false);
|
||||
if (activeTab !== "chat" || isEditing) {
|
||||
setActivityExpanded(false);
|
||||
}
|
||||
}, [activeTab, activitySegment, isEditing]);
|
||||
}, [activeTab, isEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
setActivityExpanded(false);
|
||||
}, [task.id]);
|
||||
|
||||
const [editTitle, setEditTitle] = useState(task.title || "");
|
||||
const [editDescription, setEditDescription] = useState(task.description || "");
|
||||
@@ -2610,12 +2617,12 @@ export function TaskDetailContent({
|
||||
const autoMergeEnabled = autoMergeEnabledProp ?? (settings?.autoMerge ?? false);
|
||||
const effectiveAutoMerge = resolveEffectiveAutoMerge({ autoMerge: task.autoMerge }, { autoMerge: autoMergeEnabled });
|
||||
const isManualPrFlow = mergeStrategy === "pull-request" && !effectiveAutoMerge;
|
||||
const isChatExpanded = chatExpanded && activeTab === "chat" && !isEditing;
|
||||
const isActivityExpanded = activityExpanded && activeTab === "chat" && !isEditing;
|
||||
/*
|
||||
FNXC:TaskDetailChat 2026-06-30-23:30:
|
||||
Maximized Activity chat should reserve the detail surface for the header context and chat only. Do not mount branch-group chrome in this mode so its expand/promote controls are not hidden-but-focusable, while normal and embedded task details keep the BranchGroupCard behavior.
|
||||
FNXC:TaskDetailActivity 2026-06-30-23:55:
|
||||
Maximized Activity applies to Live, Feed, and Raw Logs, not only the legacy `current` chat segment. Reserve the detail surface for header context and Activity content, and do not mount branch-group chrome in this mode so expand/promote controls are not hidden-but-focusable.
|
||||
*/
|
||||
const shouldShowBranchGroupCard = Boolean(task.branchContext?.groupId && !isChatExpanded);
|
||||
const shouldShowBranchGroupCard = Boolean(task.branchContext?.groupId && !isActivityExpanded);
|
||||
|
||||
const taskActionMenuModel = useMemo(() => buildTaskActionMenuModel({
|
||||
task,
|
||||
@@ -2733,7 +2740,7 @@ export function TaskDetailContent({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`task-detail-content${embedded ? " task-detail-content--embedded" : ""}${isChatExpanded ? " task-detail-content--chat-expanded" : ""}`}
|
||||
className={`task-detail-content${embedded ? " task-detail-content--embedded" : ""}${isActivityExpanded ? " task-detail-content--chat-expanded" : ""}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
@@ -2813,7 +2820,7 @@ export function TaskDetailContent({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`detail-body${activeTab === "chat" && activitySegment === "raw-logs" && !isEditing ? " detail-body--agent-log" : ""}${activeTab === "chat" && activitySegment === "current" && !isEditing ? " detail-body--chat" : ""}`}>
|
||||
<div className={`detail-body${activeTab === "chat" && activitySegment === "raw-logs" && !isEditing ? " detail-body--agent-log" : ""}${activeTab === "chat" && (activitySegment === "current" || isActivityExpanded) && !isEditing ? " detail-body--chat" : ""}`}>
|
||||
{isEditing ? (
|
||||
<div className="modal-edit-form">
|
||||
<TaskForm
|
||||
@@ -3315,41 +3322,53 @@ export function TaskDetailContent({
|
||||
/>
|
||||
</div>
|
||||
) : activeTab === "chat" ? (
|
||||
<div className={`detail-section detail-section--activity${activitySegment === "current" ? " detail-section--chat" : ""}${activitySegment === "raw-logs" ? " detail-section--agent-log" : ""}`}>
|
||||
<div className={`detail-section detail-section--activity${activitySegment === "current" || isActivityExpanded ? " detail-section--chat" : ""}${activitySegment === "raw-logs" ? " detail-section--agent-log" : ""}`}>
|
||||
{/*
|
||||
FNXC:TaskDetailPlannerChat 2026-06-30-22:30:
|
||||
Activity owns the existing steering/current view, Feed, and Raw Logs inside one segmented control. The stable Activity tab id remains `chat`, legacy `logs` callers land on Feed, and Raw Logs is the only segment that enables raw agent-log fetching. Planner-model conversation belongs to the separate `planner-chat` tab and must not route into steering comments.
|
||||
|
||||
FNXC:TaskDetailActivity 2026-06-30-21:55:
|
||||
The first Activity segment keeps the stable Current label for legacy segment tests and links, but its embedded composer labels the operational steering-comment affordance explicitly. Do not reuse this segment as planner-model Chat conversation.
|
||||
FNXC:TaskDetailActivity 2026-06-30-23:55:
|
||||
The first Activity segment is user-facing Live but keeps the legacy `current` segment id. Activity expansion is segment-wide, so the same reachable toggle must remain present on Live, Feed, and Raw Logs without fetching Raw Logs outside the Raw Logs segment.
|
||||
*/}
|
||||
<div className="activity-segmented-control" role="tablist" aria-label={t("taskDetail.activity.segmentsLabel", "Activity views")}>
|
||||
<div className="activity-toolbar">
|
||||
<div className="activity-segmented-control" role="tablist" aria-label={t("taskDetail.activity.segmentsLabel", "Activity views")}>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activitySegment === "current"}
|
||||
className={`activity-segment${activitySegment === "current" ? " activity-segment-active" : ""}`}
|
||||
onClick={() => setActivitySegment("current")}
|
||||
>
|
||||
{t("taskDetail.activity.current", "Live")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activitySegment === "feed"}
|
||||
className={`activity-segment${activitySegment === "feed" ? " activity-segment-active" : ""}`}
|
||||
onClick={() => setActivitySegment("feed")}
|
||||
>
|
||||
{t("taskDetail.activity.feed", "Feed")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activitySegment === "raw-logs"}
|
||||
className={`activity-segment${activitySegment === "raw-logs" ? " activity-segment-active" : ""}`}
|
||||
onClick={() => setActivitySegment("raw-logs")}
|
||||
>
|
||||
{t("taskDetail.activity.rawLogs", "Raw Logs")}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activitySegment === "current"}
|
||||
className={`activity-segment${activitySegment === "current" ? " activity-segment-active" : ""}`}
|
||||
onClick={() => setActivitySegment("current")}
|
||||
className="btn btn-icon btn-sm activity-expand-toggle"
|
||||
onClick={() => setActivityExpanded((value) => !value)}
|
||||
aria-label={isActivityExpanded ? t("taskDetail.activity.collapse", "Collapse activity") : t("taskDetail.activity.expand", "Expand activity to full modal")}
|
||||
aria-pressed={isActivityExpanded}
|
||||
data-testid="task-chat-expand-toggle"
|
||||
>
|
||||
{t("taskDetail.activity.current", "Current")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activitySegment === "feed"}
|
||||
className={`activity-segment${activitySegment === "feed" ? " activity-segment-active" : ""}`}
|
||||
onClick={() => setActivitySegment("feed")}
|
||||
>
|
||||
{t("taskDetail.activity.feed", "Feed")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activitySegment === "raw-logs"}
|
||||
className={`activity-segment${activitySegment === "raw-logs" ? " activity-segment-active" : ""}`}
|
||||
onClick={() => setActivitySegment("raw-logs")}
|
||||
>
|
||||
{t("taskDetail.activity.rawLogs", "Raw Logs")}
|
||||
{isActivityExpanded ? <Minimize2 aria-hidden="true" /> : <Maximize2 aria-hidden="true" />}
|
||||
</button>
|
||||
</div>
|
||||
{activitySegment === "current" ? (
|
||||
@@ -3360,8 +3379,6 @@ export function TaskDetailContent({
|
||||
addToast={addToast}
|
||||
sessionLive={isCliSessionLive(cliSession)}
|
||||
onTaskUpdated={handleChatTaskUpdated}
|
||||
expanded={chatExpanded}
|
||||
onToggleExpanded={() => setChatExpanded((value) => !value)}
|
||||
effectiveModels={{
|
||||
triage: toTaskChatModelInfo(resolveEffectivePlanning(workingTask, agentLogEntries, settings)),
|
||||
executor: toTaskChatModelInfo(resolveEffectiveExecutor(workingTask, agentLogEntries, assignedAgent, settings)),
|
||||
|
||||
@@ -394,7 +394,7 @@ describe("TaskChatTab", () => {
|
||||
expect(document.querySelector(".task-chat-toolbar")).toBeNull();
|
||||
expect(toggle).toHaveClass("btn-icon");
|
||||
expect(toggle).toHaveClass("task-chat-expand-toggle--overlay");
|
||||
expect(toggle).toHaveAttribute("aria-label", "Expand chat to full modal");
|
||||
expect(toggle).toHaveAttribute("aria-label", "Expand activity to full modal");
|
||||
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
||||
expect(toggle).not.toHaveTextContent("Expand");
|
||||
expect(toggle).not.toHaveTextContent("Collapse");
|
||||
@@ -407,7 +407,7 @@ describe("TaskChatTab", () => {
|
||||
render(<TaskChatTab task={makeTask()} active addToast={vi.fn()} expanded onToggleExpanded={vi.fn()} />);
|
||||
|
||||
const toggle = screen.getByTestId("task-chat-expand-toggle");
|
||||
expect(toggle).toHaveAttribute("aria-label", "Collapse chat");
|
||||
expect(toggle).toHaveAttribute("aria-label", "Collapse activity");
|
||||
expect(toggle).toHaveAttribute("aria-pressed", "true");
|
||||
expect(toggle).not.toHaveTextContent("Collapse");
|
||||
expect(toggle).not.toHaveTextContent("Expand");
|
||||
@@ -1383,7 +1383,7 @@ describe("TaskChatTab", () => {
|
||||
const toggle = screen.getByTestId("task-chat-expand-toggle");
|
||||
expect(toggle).toBeInTheDocument();
|
||||
expect(toggle).toBeVisible();
|
||||
expect(toggle).toHaveAccessibleName("Expand chat to full modal");
|
||||
expect(toggle).toHaveAccessibleName("Expand activity to full modal");
|
||||
expect(toggle).not.toHaveTextContent("Expand");
|
||||
expect(transcript).not.toContainElement(toggle);
|
||||
});
|
||||
@@ -2566,15 +2566,15 @@ describe("TaskChatTab", () => {
|
||||
expect(mobileInputRule).toContain("min-height: calc(var(--space-2xl) + var(--space-lg))");
|
||||
});
|
||||
|
||||
it("keeps TaskDetailModal inline and expanded chat on the canonical TaskChatTab renderer", () => {
|
||||
it("keeps TaskDetailModal inline and expanded Activity on the canonical TaskChatTab renderer", () => {
|
||||
const source = readFileSync(resolve(__dirname, "../TaskDetailModal.tsx"), "utf8");
|
||||
const taskChatMounts = source.match(/<TaskChatTab\b/g) ?? [];
|
||||
|
||||
expect(source).toContain('import { TaskChatTab } from "./TaskChatTab"');
|
||||
expect(taskChatMounts).toHaveLength(1);
|
||||
expect(source).toContain("const isChatExpanded = chatExpanded && activeTab === \"chat\" && !isEditing");
|
||||
expect(source).toContain("const isActivityExpanded = activityExpanded && activeTab === \"chat\" && !isEditing");
|
||||
expect(source).toContain("task-detail-content--chat-expanded");
|
||||
expect(source).toContain("expanded={chatExpanded}");
|
||||
expect(source).toContain("setActivityExpanded((value) => !value)");
|
||||
});
|
||||
|
||||
it("widens task chat messages in narrow host containers without changing desktop layout", () => {
|
||||
|
||||
@@ -466,8 +466,9 @@ describe("TaskDetailModal", () => {
|
||||
expect(container.querySelector(".activity-segmented-control")).toBeTruthy();
|
||||
expect(screen.queryByText("Agent Log")).toBeNull();
|
||||
const segments = screen.getAllByRole("tab");
|
||||
expect(segments.map((segment) => segment.textContent)).toEqual(["Current", "Feed", "Raw Logs"]);
|
||||
expect(screen.getByRole("tab", { name: "Current" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(segments.map((segment) => segment.textContent)).toEqual(["Live", "Feed", "Raw Logs"]);
|
||||
expect(screen.queryByRole("tab", { name: "Current" })).toBeNull();
|
||||
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(container.querySelector(".detail-section--chat")).toBeTruthy();
|
||||
expect(container.querySelector("[data-testid='task-chat-tab']")).toBeTruthy();
|
||||
expect(container.querySelector(".detail-activity")).toBeNull();
|
||||
@@ -869,7 +870,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(mobileActionsRule).toContain("display: none");
|
||||
});
|
||||
|
||||
it("FN-6370/FN-6517 expands and collapses chat without leaving chrome hidden", () => {
|
||||
it("FN-6370/FN-6517 expands and collapses Activity Live without leaving chrome hidden", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nContent" })}
|
||||
@@ -896,7 +897,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(titleRow).toHaveTextContent("In Progress");
|
||||
expect(container.querySelector(".detail-tabs")).toBeTruthy();
|
||||
expect(container.querySelector(".modal-actions")).toBeTruthy();
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Collapse chat");
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Collapse activity");
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-pressed", "true");
|
||||
|
||||
fireEvent.click(screen.getByTestId("task-chat-expand-toggle"));
|
||||
@@ -904,11 +905,80 @@ describe("TaskDetailModal", () => {
|
||||
expect(titleRow).toHaveTextContent("FN-099");
|
||||
expect(container.querySelector(".detail-tabs")).toBeTruthy();
|
||||
expect(container.querySelector(".modal-actions")).toBeTruthy();
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Expand chat to full modal");
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Expand activity to full modal");
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-pressed", "false");
|
||||
});
|
||||
|
||||
it("FN-7320 removes branch group chrome only while Activity chat is expanded", () => {
|
||||
it("FN-7325 keeps Activity expansion available and sticky across Live, Feed, and Raw Logs", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
prompt: "# Hello\n\nContent",
|
||||
log: [{ timestamp: "2026-01-01T00:00:00Z", action: "Expanded feed entry", outcome: "visible" }],
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const content = container.querySelector(".task-detail-content");
|
||||
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Expand activity to full modal");
|
||||
|
||||
fireEvent.click(screen.getByTestId("task-chat-expand-toggle"));
|
||||
expect(content).toHaveClass("task-detail-content--chat-expanded");
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Collapse activity");
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||
expect(content).toHaveClass("task-detail-content--chat-expanded");
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-pressed", "true");
|
||||
expect(screen.getByText("Expanded feed entry")).toBeInTheDocument();
|
||||
expect(container.querySelector(".detail-activity-list")).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
|
||||
expect(content).toHaveClass("task-detail-content--chat-expanded");
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-pressed", "true");
|
||||
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("FN-7325 resets Activity expansion on task changes but preserves legacy logs routing", () => {
|
||||
const { container, rerender } = render(
|
||||
<TaskDetailContent
|
||||
task={makeTask({ id: "FN-099", prompt: "# Hello\n\nContent" })}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
initialTab="chat"
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId("task-chat-expand-toggle"));
|
||||
expect(container.querySelector(".task-detail-content")).toHaveClass("task-detail-content--chat-expanded");
|
||||
|
||||
rerender(
|
||||
<TaskDetailContent
|
||||
task={makeTask({ id: "FN-100", prompt: "# Next\n\nContent" })}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
initialTab="logs"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".task-detail-content")).not.toHaveClass("task-detail-content--chat-expanded");
|
||||
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Expand activity to full modal");
|
||||
});
|
||||
|
||||
it("FN-7320 removes branch group chrome only while Activity is expanded", () => {
|
||||
const branchContext = { groupId: "BG-7320", source: "planning", assignmentMode: "shared" } as const;
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
@@ -938,7 +1008,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(screen.getByRole("button", { name: "Mock branch group toggle BG-7320" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("FN-7320 expands Activity chat for tasks without branch groups without rendering branch shells", () => {
|
||||
it("FN-7320 expands Activity for tasks without branch groups without rendering branch shells", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nContent", branchContext: undefined })}
|
||||
@@ -987,7 +1057,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(container.querySelector(".modal-actions")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("FN-6370 resets expanded chat when the active tab changes", () => {
|
||||
it("FN-6370 resets expanded Activity when the active tab changes", () => {
|
||||
const { container, rerender } = render(
|
||||
<TaskDetailContent
|
||||
task={makeTask({ prompt: "# Hello\n\nContent" })}
|
||||
@@ -1012,7 +1082,7 @@ describe("TaskDetailModal", () => {
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
initialTab="logs"
|
||||
initialTab="definition"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -1020,7 +1090,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(screen.queryByTestId("task-chat-expand-toggle")).toBeNull();
|
||||
});
|
||||
|
||||
it("FN-6370 resets expanded chat when entering edit mode", () => {
|
||||
it("FN-6370 resets expanded Activity when entering edit mode", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ column: "triage", prompt: "# Hello\n\nContent" })}
|
||||
|
||||
@@ -2056,7 +2056,7 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Expand chat to full modal" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Expand activity to full modal" }));
|
||||
|
||||
expect(container.querySelector(".task-detail-content--chat-expanded")).toBeInTheDocument();
|
||||
expect(container.querySelector("h2.detail-title")).toHaveClass("detail-title--collapsed");
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("TaskDetailModal planner Chat tab", () => {
|
||||
renderTask("in-progress", "chat");
|
||||
|
||||
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
|
||||
expect(screen.getByRole("tab", { name: "Current" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
|
||||
});
|
||||
|
||||
it("routes explicit planner-chat requests to the new Chat tab", () => {
|
||||
@@ -442,7 +442,7 @@ describe("TaskDetailModal Activity feed loading", () => {
|
||||
});
|
||||
|
||||
describe("TaskDetailModal Chat task merge", () => {
|
||||
it("exposes the steering composer only in Activity Current and posts through task updates", async () => {
|
||||
it("exposes the steering composer only in Activity Live and posts through task updates", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { addSteeringComment } = await import("../../api");
|
||||
const onTaskUpdated = vi.fn();
|
||||
@@ -469,7 +469,7 @@ describe("TaskDetailModal Chat task merge", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("tab", { name: "Current" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getAllByRole("form", { name: "Steering comment" })).toHaveLength(1);
|
||||
expect(screen.getByText("Send operational guidance to the active task through steering comments.")).toBeInTheDocument();
|
||||
|
||||
@@ -481,7 +481,7 @@ describe("TaskDetailModal Chat task merge", () => {
|
||||
expect(screen.queryByRole("form", { name: "Steering comment" })).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("agent-log-viewer")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: "Current" }));
|
||||
await user.click(screen.getByRole("tab", { name: "Live" }));
|
||||
const input = screen.getByLabelText("Message active agent session");
|
||||
await user.type(input, "Please keep the current approach");
|
||||
await user.click(screen.getByRole("button", { name: "Send" }));
|
||||
|
||||
Reference in New Issue
Block a user