FN-7307: add Activity segments to task details
Unify task-detail activity surfaces under one segmented Activity tab. - Add Current, Feed, and Raw Logs segments inside the Activity tab while preserving legacy chat/logs entrypoints. - Route log-based stall jumps and raw log loading through the new Activity segments. - Update task-detail styles, plugin modal types, and tests for the segmented Activity layout. - Add a minor changeset for the published CLI package. Files changed: .changeset/fn-7307-activity-segments.md | 7 ++ .../dashboard/app/components/TaskDetailModal.css | 42 +++++-- .../dashboard/app/components/TaskDetailModal.tsx | 122 +++++++++++-------- .../TaskDetailModal.attachments-and-tabs.test.tsx | 134 +++++++++++---------- .../__tests__/TaskDetailModal.css.test.ts | 11 ++ ...lModal.inline-editing-and-integrations.test.tsx | 7 +- ...skDetailModal.models-progress-workflow.test.tsx | 26 ++-- .../__tests__/TaskDetailModal.rendering.test.tsx | 3 +- .../components/__tests__/TaskDetailModal.test.tsx | 30 ++--- packages/dashboard/app/hooks/useModalManager.ts | 4 +- packages/dashboard/app/plugins/types.ts | 4 +- 11 files changed, 230 insertions(+), 160 deletions(-) Fusion-Task-Id: FN-7307 Fusion-Task-Lineage: 20ce70a6-2c94-4980-b528-ae4079b59016 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7307-activity-segments.md
Normal file
7
.changeset/fn-7307-activity-segments.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Add Activity segments for current task activity, Feed, and Raw Logs.
|
||||||
|
category: feature
|
||||||
|
dev: Task detail keeps legacy initialTab="logs" compatibility by routing to Activity → Feed.
|
||||||
@@ -119,7 +119,7 @@ Task detail workflow badges share the board badge's slight token-based icon-to-l
|
|||||||
background: var(--text-muted);
|
background: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When the Agent Log tab is active, switch to flex layout so the log section
|
/* When Activity → Raw Logs is active, switch to flex layout so the log section
|
||||||
can stretch to fill the remaining space above the action bar.
|
can stretch to fill the remaining space above the action bar.
|
||||||
The detail-body itself should NOT scroll; the agent-log-viewer scrolls instead. */
|
The detail-body itself should NOT scroll; the agent-log-viewer scrolls instead. */
|
||||||
.detail-body--agent-log {
|
.detail-body--agent-log {
|
||||||
@@ -128,7 +128,7 @@ Task detail workflow badges share the board badge's slight token-based icon-to-l
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat mirrors the Agent Log fill-height layout: the modal body does not scroll;
|
/* 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. */
|
the transcript owns internal scrolling while the composer stays visible. */
|
||||||
.detail-body--chat {
|
.detail-body--chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -793,7 +793,7 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P
|
|||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Agent Log tab: stretch to fill the remaining modal body height
|
/* Activity → Raw Logs: stretch to fill the remaining modal body height
|
||||||
so the log viewer uses all available space above the action bar. */
|
so the log viewer uses all available space above the action bar. */
|
||||||
.detail-section--agent-log {
|
.detail-section--agent-log {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2353,36 +2353,54 @@ FNXC:TaskDetailTabs 2026-06-26-00:35:
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Log Subview Toggle === */
|
/* === Activity Segmented Control === */
|
||||||
.log-subview-toggle {
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
.activity-segmented-control {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
max-inline-size: 100%;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
background: var(--card);
|
background: var(--card);
|
||||||
border: 1px solid var(--border);
|
border: var(--border-width) solid var(--border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
padding: 2px;
|
padding: calc(var(--space-xs) / 4);
|
||||||
margin-bottom: var(--space-md);
|
margin-bottom: var(--space-md);
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overscroll-behavior-inline: contain;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-subview-btn {
|
.activity-segment {
|
||||||
padding: 4px 12px;
|
flex: 0 0 auto;
|
||||||
|
padding: calc(var(--space-xs) / 2) var(--space-md);
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: var(--font-size-sm);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
transition: color var(--transition-fast), background var(--transition-fast);
|
transition: color var(--transition-fast), background var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-subview-btn:hover {
|
.activity-segment:hover {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: var(--card-hover);
|
background: var(--card-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-subview-btn-active {
|
.activity-segment:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--focus-ring-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-segment-active {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|||||||
@@ -192,24 +192,32 @@ function formatDurationCompact(ageMs: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TabId = "summary" | "definition" | "chat" | "logs" | "changes" | "review" | "pr" | "comments" | "model" | "workflow" | "documents" | "stats" | "routing" | "retries" | "terminal" | `plugin-${string}`;
|
type TabId = "summary" | "definition" | "chat" | "logs" | "changes" | "review" | "pr" | "comments" | "model" | "workflow" | "documents" | "stats" | "routing" | "retries" | "terminal" | `plugin-${string}`;
|
||||||
|
type ActivitySegment = "current" | "feed" | "raw-logs";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:TaskDetailActivityTab 2026-06-30-00:00:
|
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 every task column, including done tasks; Summary remains explicitly available for completed work until a later subtask adds the separate planner-model Chat surface.
|
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 every task column, including done tasks; Summary remains explicitly available for completed work until a later subtask adds the separate planner-model Chat surface.
|
||||||
|
|
||||||
FNXC:TaskDetailActivityTab 2026-06-30-00:00:
|
FNXC:TaskDetailActivity 2026-06-30-22:15:
|
||||||
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 the renamed Activity surface.
|
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 because Logs no longer renders as a top-level task-detail tab.
|
||||||
*/
|
*/
|
||||||
function resolveDefaultTab(initialTab: TabId | undefined, _column: ColumnId): TabId {
|
function resolveDefaultTab(initialTab: TabId | undefined, _column: ColumnId): TabId {
|
||||||
if (initialTab === "retries") {
|
if (initialTab === "retries") {
|
||||||
return "definition";
|
return "definition";
|
||||||
}
|
}
|
||||||
|
if (initialTab === "logs") {
|
||||||
|
return "chat";
|
||||||
|
}
|
||||||
if (initialTab) {
|
if (initialTab) {
|
||||||
return initialTab;
|
return initialTab;
|
||||||
}
|
}
|
||||||
return "chat";
|
return "chat";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveDefaultActivitySegment(initialTab: TabId | undefined): ActivitySegment {
|
||||||
|
return initialTab === "logs" ? "feed" : "current";
|
||||||
|
}
|
||||||
|
|
||||||
// Lazy-load the terminal so xterm + addons stay out of the main bundle (U11).
|
// Lazy-load the terminal so xterm + addons stay out of the main bundle (U11).
|
||||||
const LazySessionTerminal = lazy(() =>
|
const LazySessionTerminal = lazy(() =>
|
||||||
import("./SessionTerminal").then((m) => ({ default: m.SessionTerminal })),
|
import("./SessionTerminal").then((m) => ({ default: m.SessionTerminal })),
|
||||||
@@ -516,6 +524,7 @@ export function TaskDetailContent({
|
|||||||
const columnLabel = useColumnLabel();
|
const columnLabel = useColumnLabel();
|
||||||
const fileBrowser = useFileBrowser();
|
const fileBrowser = useFileBrowser();
|
||||||
const [activeTab, setActiveTab] = useState<TabId>(() => resolveDefaultTab(initialTab, task.column));
|
const [activeTab, setActiveTab] = useState<TabId>(() => resolveDefaultTab(initialTab, task.column));
|
||||||
|
const [activitySegment, setActivitySegment] = useState<ActivitySegment>(() => resolveDefaultActivitySegment(initialTab));
|
||||||
const [chatExpanded, setChatExpanded] = useState(false);
|
const [chatExpanded, setChatExpanded] = useState(false);
|
||||||
|
|
||||||
// ── CLI agent session (U11) ────────────────────────────────────────────────
|
// ── CLI agent session (U11) ────────────────────────────────────────────────
|
||||||
@@ -646,6 +655,7 @@ export function TaskDetailContent({
|
|||||||
// Sync activeTab when the caller changes initialTab (e.g. opening a different tab)
|
// Sync activeTab when the caller changes initialTab (e.g. opening a different tab)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveTab(resolveDefaultTab(initialTab, task.column));
|
setActiveTab(resolveDefaultTab(initialTab, task.column));
|
||||||
|
setActivitySegment(resolveDefaultActivitySegment(initialTab));
|
||||||
if (initialTab === "retries") {
|
if (initialTab === "retries") {
|
||||||
setRetriesExpanded(true);
|
setRetriesExpanded(true);
|
||||||
}
|
}
|
||||||
@@ -668,7 +678,6 @@ export function TaskDetailContent({
|
|||||||
setDescriptionExpanded(false);
|
setDescriptionExpanded(false);
|
||||||
}, [task.column, task.id]);
|
}, [task.column, task.id]);
|
||||||
|
|
||||||
const [logSubview, setLogSubview] = useState<"activity" | "agent-log">("activity");
|
|
||||||
const [highlightStallCode, setHighlightStallCode] = useState<string | null>(null);
|
const [highlightStallCode, setHighlightStallCode] = useState<string | null>(null);
|
||||||
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
||||||
const [titleOverflows, setTitleOverflows] = useState(false);
|
const [titleOverflows, setTitleOverflows] = useState(false);
|
||||||
@@ -807,7 +816,7 @@ export function TaskDetailContent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab !== "logs" || logSubview !== "activity") {
|
if (activeTab !== "chat" || activitySegment !== "feed") {
|
||||||
setHighlightStallCode(null);
|
setHighlightStallCode(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -820,7 +829,7 @@ export function TaskDetailContent({
|
|||||||
if (highlighted && typeof highlighted.scrollIntoView === "function") {
|
if (highlighted && typeof highlighted.scrollIntoView === "function") {
|
||||||
highlighted.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
highlighted.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||||
}
|
}
|
||||||
}, [activeTab, logSubview, highlightStallCode]);
|
}, [activeTab, activitySegment, highlightStallCode]);
|
||||||
const [refineFeedback, setRefineFeedback] = useState("");
|
const [refineFeedback, setRefineFeedback] = useState("");
|
||||||
const [isRefining, setIsRefining] = useState(false);
|
const [isRefining, setIsRefining] = useState(false);
|
||||||
|
|
||||||
@@ -828,10 +837,10 @@ export function TaskDetailContent({
|
|||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab !== "chat" || isEditing) {
|
if (activeTab !== "chat" || activitySegment !== "current" || isEditing) {
|
||||||
setChatExpanded(false);
|
setChatExpanded(false);
|
||||||
}
|
}
|
||||||
}, [activeTab, isEditing]);
|
}, [activeTab, activitySegment, isEditing]);
|
||||||
|
|
||||||
const [editTitle, setEditTitle] = useState(task.title || "");
|
const [editTitle, setEditTitle] = useState(task.title || "");
|
||||||
const [editDescription, setEditDescription] = useState(task.description || "");
|
const [editDescription, setEditDescription] = useState(task.description || "");
|
||||||
@@ -955,7 +964,7 @@ export function TaskDetailContent({
|
|||||||
activeTaskIdRef.current = task.id;
|
activeTaskIdRef.current = task.id;
|
||||||
}, [task.id]);
|
}, [task.id]);
|
||||||
|
|
||||||
// Merged project settings for effective model resolution in Agent Log header
|
// Merged project settings for effective model resolution in Raw Logs header
|
||||||
const [settings, setSettings] = useState<Settings | undefined>(undefined);
|
const [settings, setSettings] = useState<Settings | undefined>(undefined);
|
||||||
const [globalSettings, setGlobalSettings] = useState<GlobalSettings | null>(null);
|
const [globalSettings, setGlobalSettings] = useState<GlobalSettings | null>(null);
|
||||||
|
|
||||||
@@ -1012,7 +1021,7 @@ export function TaskDetailContent({
|
|||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
/*
|
/*
|
||||||
FNXC:ModelResolution 2026-06-27-10:52:
|
FNXC:ModelResolution 2026-06-27-10:52:
|
||||||
Task-detail model displays are task-scoped because project model lanes moved into workflow setting values. Fetch the effective settings for the selected task so Workflow, Chat, Agent Log, and Model editor surfaces resolve the same Executor/Reviewer/Planning models the engine uses.
|
Task-detail model displays are task-scoped because project model lanes moved into workflow setting values. Fetch the effective settings for the selected task so Workflow, Activity → Raw Logs, and Model editor surfaces resolve the same Executor/Reviewer/Planning models the engine uses.
|
||||||
*/
|
*/
|
||||||
fetchTaskEffectiveSettings(task.id, projectId)
|
fetchTaskEffectiveSettings(task.id, projectId)
|
||||||
.catch(() => fetchSettings(projectId))
|
.catch(() => fetchSettings(projectId))
|
||||||
@@ -1774,7 +1783,7 @@ export function TaskDetailContent({
|
|||||||
loadingMore: agentLogLoadingMore,
|
loadingMore: agentLogLoadingMore,
|
||||||
} = useAgentLogs(
|
} = useAgentLogs(
|
||||||
task.id,
|
task.id,
|
||||||
activeTab === "logs" && logSubview === "agent-log",
|
activeTab === "chat" && activitySegment === "raw-logs",
|
||||||
projectId,
|
projectId,
|
||||||
);
|
);
|
||||||
const requestClose = useCallback(() => {
|
const requestClose = useCallback(() => {
|
||||||
@@ -2792,7 +2801,7 @@ export function TaskDetailContent({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`detail-body${activeTab === "logs" && logSubview === "agent-log" && !isEditing ? " detail-body--agent-log" : ""}${activeTab === "chat" && !isEditing ? " detail-body--chat" : ""}`}>
|
<div className={`detail-body${activeTab === "chat" && activitySegment === "raw-logs" && !isEditing ? " detail-body--agent-log" : ""}${activeTab === "chat" && activitySegment === "current" && !isEditing ? " detail-body--chat" : ""}`}>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="modal-edit-form">
|
<div className="modal-edit-form">
|
||||||
<TaskForm
|
<TaskForm
|
||||||
@@ -3159,12 +3168,6 @@ export function TaskDetailContent({
|
|||||||
>
|
>
|
||||||
{t("taskDetail.tabs.definition", "Plan")}
|
{t("taskDetail.tabs.definition", "Plan")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className={`detail-tab${activeTab === "logs" ? " detail-tab-active" : ""}`}
|
|
||||||
onClick={() => setActiveTab("logs")}
|
|
||||||
>
|
|
||||||
{t("taskDetail.tabs.logs", "Logs")}
|
|
||||||
</button>
|
|
||||||
{(task.column === "in-progress" || task.column === "in-review" || task.column === "done") && (
|
{(task.column === "in-progress" || task.column === "in-review" || task.column === "done") && (
|
||||||
<button
|
<button
|
||||||
className={`detail-tab${activeTab === "changes" ? " detail-tab-active" : ""}`}
|
className={`detail-tab${activeTab === "changes" ? " detail-tab-active" : ""}`}
|
||||||
@@ -3282,41 +3285,58 @@ export function TaskDetailContent({
|
|||||||
<TaskSummaryTab task={workingTask} pricingOverrides={globalSettings?.modelPricingOverrides} />
|
<TaskSummaryTab task={workingTask} pricingOverrides={globalSettings?.modelPricingOverrides} />
|
||||||
</div>
|
</div>
|
||||||
) : activeTab === "chat" ? (
|
) : activeTab === "chat" ? (
|
||||||
<div className="detail-section detail-section--chat">
|
<div className={`detail-section detail-section--activity${activitySegment === "current" ? " detail-section--chat" : ""}${activitySegment === "raw-logs" ? " detail-section--agent-log" : ""}`}>
|
||||||
<TaskChatTab
|
{/*
|
||||||
task={workingTask}
|
FNXC:TaskDetailActivity 2026-06-30-22:15:
|
||||||
projectId={projectId}
|
Activity owns the existing steering/current view, Feed, and Raw Logs inside one segmented control. The later planner-model Chat tab is intentionally out of scope, so the stable top-level tab id remains `chat` while the legacy `logs` id lands on the Feed segment.
|
||||||
active={activeTab === "chat"}
|
*/}
|
||||||
addToast={addToast}
|
<div className="activity-segmented-control" role="tablist" aria-label={t("taskDetail.activity.segmentsLabel", "Activity views")}>
|
||||||
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)),
|
|
||||||
reviewer: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)),
|
|
||||||
merger: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : activeTab === "logs" ? (
|
|
||||||
<div className={`detail-section${logSubview === "agent-log" ? " detail-section--agent-log" : ""}`}>
|
|
||||||
<div className="log-subview-toggle">
|
|
||||||
<button
|
<button
|
||||||
className={`log-subview-btn${logSubview === "activity" ? " log-subview-btn-active" : ""}`}
|
type="button"
|
||||||
onClick={() => setLogSubview("activity")}
|
role="tab"
|
||||||
|
aria-selected={activitySegment === "current"}
|
||||||
|
className={`activity-segment${activitySegment === "current" ? " activity-segment-active" : ""}`}
|
||||||
|
onClick={() => setActivitySegment("current")}
|
||||||
>
|
>
|
||||||
{t("taskDetail.logs.activity", "Activity")}
|
{t("taskDetail.activity.current", "Current")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`log-subview-btn${logSubview === "agent-log" ? " log-subview-btn-active" : ""}`}
|
type="button"
|
||||||
onClick={() => setLogSubview("agent-log")}
|
role="tab"
|
||||||
|
aria-selected={activitySegment === "feed"}
|
||||||
|
className={`activity-segment${activitySegment === "feed" ? " activity-segment-active" : ""}`}
|
||||||
|
onClick={() => setActivitySegment("feed")}
|
||||||
>
|
>
|
||||||
{t("taskDetail.logs.agentLog", "Agent Log")}
|
{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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{logSubview === "agent-log" ? (
|
{activitySegment === "current" ? (
|
||||||
|
<TaskChatTab
|
||||||
|
task={workingTask}
|
||||||
|
projectId={projectId}
|
||||||
|
active={activeTab === "chat" && activitySegment === "current"}
|
||||||
|
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)),
|
||||||
|
reviewer: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)),
|
||||||
|
merger: toTaskChatModelInfo(resolveEffectiveValidator(workingTask, agentLogEntries, assignedAgent, settings)),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : activitySegment === "raw-logs" ? (
|
||||||
<AgentLogViewer
|
<AgentLogViewer
|
||||||
entries={agentLogEntries}
|
entries={agentLogEntries}
|
||||||
loading={agentLogLoading}
|
loading={agentLogLoading}
|
||||||
@@ -3329,8 +3349,8 @@ export function TaskDetailContent({
|
|||||||
totalCount={agentLogTotal}
|
totalCount={agentLogTotal}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="detail-activity">
|
<div className="detail-activity" role="tabpanel">
|
||||||
<h4>{t("taskDetail.logs.activityHeading", "Activity")}</h4>
|
<h4>{t("taskDetail.activity.feedHeading", "Feed")}</h4>
|
||||||
{(workingTask as typeof workingTask & { activityLogTruncatedCount?: number }).activityLogTruncatedCount ? (
|
{(workingTask as typeof workingTask & { activityLogTruncatedCount?: number }).activityLogTruncatedCount ? (
|
||||||
<div className="detail-log-truncated">
|
<div className="detail-log-truncated">
|
||||||
{t("taskDetail.logs.truncated", "Showing the most recent {{count}} activity entries.", { count: workingTask.log.length })}
|
{t("taskDetail.logs.truncated", "Showing the most recent {{count}} activity entries.", { count: workingTask.log.length })}
|
||||||
@@ -3426,8 +3446,8 @@ export function TaskDetailContent({
|
|||||||
type="button"
|
type="button"
|
||||||
className="btn btn-sm detail-in-review-stall-jump"
|
className="btn btn-sm detail-in-review-stall-jump"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab("logs");
|
setActiveTab("chat");
|
||||||
setLogSubview("activity");
|
setActivitySegment("feed");
|
||||||
setHighlightStallCode(workingTask.inReviewStall?.code ?? null);
|
setHighlightStallCode(workingTask.inReviewStall?.code ?? null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -3474,8 +3494,8 @@ export function TaskDetailContent({
|
|||||||
type="button"
|
type="button"
|
||||||
className="btn btn-sm detail-in-review-stall-jump"
|
className="btn btn-sm detail-in-review-stall-jump"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setActiveTab("logs");
|
setActiveTab("chat");
|
||||||
setLogSubview("activity");
|
setActivitySegment("feed");
|
||||||
setHighlightStallCode(workingTask.stalePausedReview?.code ?? null);
|
setHighlightStallCode(workingTask.stalePausedReview?.code ?? null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -368,8 +368,9 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Click Logs tab — Activity is the default subview
|
// Click Activity tab — Activity is the default subview
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
|
||||||
const activityList = container.querySelector(".detail-activity-list");
|
const activityList = container.querySelector(".detail-activity-list");
|
||||||
expect(activityList).toBeTruthy();
|
expect(activityList).toBeTruthy();
|
||||||
@@ -453,27 +454,23 @@ describe("TaskDetailModal", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText("Plan")).toBeTruthy();
|
expect(screen.getByText("Plan")).toBeTruthy();
|
||||||
expect(screen.getByText("Logs")).toBeTruthy();
|
expect(screen.queryByRole("button", { name: "Logs" })).toBeNull();
|
||||||
// Logs subview controls should NOT be visible on the default Activity tab.
|
expect(container.querySelector(".activity-segmented-control")).toBeTruthy();
|
||||||
expect(container.querySelector(".log-subview-toggle")).toBeNull();
|
|
||||||
expect(screen.queryByText("Agent Log")).toBeNull();
|
expect(screen.queryByText("Agent Log")).toBeNull();
|
||||||
// Chat content should be visible by default.
|
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(container.querySelector(".detail-section--chat")).toBeTruthy();
|
expect(container.querySelector(".detail-section--chat")).toBeTruthy();
|
||||||
expect(container.querySelector("[data-testid='task-chat-tab']")).toBeTruthy();
|
expect(container.querySelector("[data-testid='task-chat-tab']")).toBeTruthy();
|
||||||
// Activity section should NOT be visible initially.
|
|
||||||
expect(container.querySelector(".detail-activity")).toBeNull();
|
expect(container.querySelector(".detail-activity")).toBeNull();
|
||||||
// Agent log viewer should not be visible.
|
|
||||||
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
|
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
|
||||||
|
|
||||||
// After clicking Logs tab, the subview toggle buttons should appear.
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
||||||
const logSubviewToggle = container.querySelector(".log-subview-toggle");
|
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
|
||||||
expect(logSubviewToggle).toBeTruthy();
|
|
||||||
expect(logSubviewToggle!.textContent).toContain("Activity");
|
|
||||||
expect(logSubviewToggle!.textContent).toContain("Agent Log");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("switches to Activity subview via Logs tab and shows activity feed", () => {
|
it("switches to Feed segment via Activity tab and shows activity feed", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={makeTask({
|
task={makeTask({
|
||||||
@@ -491,8 +488,9 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Click Logs tab — Activity is the default subview
|
// Click Activity tab — Activity is the default subview
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
|
||||||
// Activity section should be visible
|
// Activity section should be visible
|
||||||
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
||||||
@@ -502,7 +500,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(container.querySelector(".markdown-body")).toBeNull();
|
expect(container.querySelector(".markdown-body")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Activity subview renders log entries correctly", () => {
|
it("Feed segment renders log entries correctly", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={makeTask({
|
task={makeTask({
|
||||||
@@ -521,8 +519,9 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Click Logs tab — Activity is the default subview
|
// Click Activity tab — Activity is the default subview
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
|
||||||
const activityList = container.querySelector(".detail-activity-list");
|
const activityList = container.querySelector(".detail-activity-list");
|
||||||
expect(activityList).toBeTruthy();
|
expect(activityList).toBeTruthy();
|
||||||
@@ -538,7 +537,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(logEntries[2].textContent).toContain("Created task");
|
expect(logEntries[2].textContent).toContain("Created task");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Activity subview keeps action/outcome rendering intact", () => {
|
it("Feed segment keeps action/outcome rendering intact", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={makeTask({
|
task={makeTask({
|
||||||
@@ -556,7 +555,8 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
|
||||||
const actions = container.querySelectorAll(".detail-log-action");
|
const actions = container.querySelectorAll(".detail-log-action");
|
||||||
const outcomes = container.querySelectorAll(".detail-log-outcome");
|
const outcomes = container.querySelectorAll(".detail-log-outcome");
|
||||||
@@ -581,7 +581,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(timestampRule).not.toContain("color: var(--text);");
|
expect(timestampRule).not.toContain("color: var(--text);");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Activity subview shows empty state when no logs", () => {
|
it("Feed segment shows empty state when no logs", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={makeTask({ log: [] })}
|
task={makeTask({ log: [] })}
|
||||||
@@ -594,8 +594,9 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Click Logs tab — Activity is the default subview
|
// Click Activity tab — Activity is the default subview
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
|
||||||
// Activity section should be visible
|
// Activity section should be visible
|
||||||
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
||||||
@@ -606,7 +607,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(container.querySelector(".detail-activity-list")).toBeNull();
|
expect(container.querySelector(".detail-activity-list")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can switch between all tabs and Logs subviews", () => {
|
it("can switch between all tabs and Activity segments", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={makeTask({
|
task={makeTask({
|
||||||
@@ -627,18 +628,19 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
||||||
expect(container.querySelector(".detail-activity")).toBeNull();
|
expect(container.querySelector(".detail-activity")).toBeNull();
|
||||||
|
|
||||||
// Switch to Logs tab (Activity subview is default)
|
// Switch to Activity tab (Feed segment is default)
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
||||||
expect(container.querySelector(".markdown-body")).toBeNull();
|
expect(container.querySelector(".markdown-body")).toBeNull();
|
||||||
|
|
||||||
// Switch to Agent Log subview within Logs tab
|
// Switch to Raw Activity segment within Activity tab
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByText("Raw Logs"));
|
||||||
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
|
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
|
||||||
expect(container.querySelector(".detail-activity")).toBeNull();
|
expect(container.querySelector(".detail-activity")).toBeNull();
|
||||||
|
|
||||||
// Switch back to Activity subview within Logs tab.
|
// Switch back to Feed segment within Activity tab.
|
||||||
fireEvent.click(container.querySelector<HTMLButtonElement>(".log-subview-toggle .log-subview-btn")!);
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
||||||
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
|
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
|
||||||
|
|
||||||
@@ -654,7 +656,7 @@ describe("TaskDetailModal", () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("switches to Agent Log subview via Logs tab and back", async () => {
|
it("switches to Raw Activity segment via Activity tab and back", async () => {
|
||||||
const { useAgentLogs } = await import("../../hooks/useAgentLogs");
|
const { useAgentLogs } = await import("../../hooks/useAgentLogs");
|
||||||
const mockUseAgentLogs = vi.mocked(useAgentLogs);
|
const mockUseAgentLogs = vi.mocked(useAgentLogs);
|
||||||
|
|
||||||
@@ -670,9 +672,10 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Click Logs tab, then Agent Log subview
|
// Click Activity tab, then Raw Activity segment
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
fireEvent.click(screen.getByText("Raw Logs"));
|
||||||
|
|
||||||
// Agent log viewer should appear
|
// Agent log viewer should appear
|
||||||
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
|
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeTruthy();
|
||||||
@@ -687,7 +690,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
|
expect(container.querySelector("[data-testid='agent-log-viewer']")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes enabled=true to useAgentLogs only when Logs → Agent Log subview is active", async () => {
|
it("passes enabled=true to useAgentLogs only when Activity → Raw Logs segment is active", async () => {
|
||||||
const { useAgentLogs } = await import("../../hooks/useAgentLogs");
|
const { useAgentLogs } = await import("../../hooks/useAgentLogs");
|
||||||
const mockUseAgentLogs = vi.mocked(useAgentLogs);
|
const mockUseAgentLogs = vi.mocked(useAgentLogs);
|
||||||
mockUseAgentLogs.mockClear();
|
mockUseAgentLogs.mockClear();
|
||||||
@@ -708,13 +711,14 @@ describe("TaskDetailModal", () => {
|
|||||||
const initialCall = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1];
|
const initialCall = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1];
|
||||||
expect(initialCall[1]).toBe(false);
|
expect(initialCall[1]).toBe(false);
|
||||||
|
|
||||||
// Switch to Logs tab (Activity subview is default) — enabled should still be false
|
// Switch to Activity tab (Feed segment is default) — enabled should still be false
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
const afterLogsClick = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1];
|
const afterLogsClick = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1];
|
||||||
expect(afterLogsClick[1]).toBe(false);
|
expect(afterLogsClick[1]).toBe(false);
|
||||||
|
|
||||||
// Switch to Agent Log subview — enabled should become true
|
// Switch to Raw Activity segment — enabled should become true
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByText("Raw Logs"));
|
||||||
const afterAgentLog = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1];
|
const afterAgentLog = mockUseAgentLogs.mock.calls[mockUseAgentLogs.mock.calls.length - 1];
|
||||||
expect(afterAgentLog[1]).toBe(true);
|
expect(afterAgentLog[1]).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -743,7 +747,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(container.querySelector(".markdown-body")).toBeNull();
|
expect(container.querySelector(".markdown-body")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows correct top-level tabs including Logs", async () => {
|
it("shows correct top-level tabs without Logs", async () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={makeTask()}
|
task={makeTask()}
|
||||||
@@ -757,18 +761,19 @@ describe("TaskDetailModal", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// For an in-progress task (no workflow steps, no merge commit), the
|
// For an in-progress task (no workflow steps, no merge commit), the
|
||||||
// top-level tabs are: Activity, Plan, Logs, Changes, Review, Comments,
|
// top-level tabs are: Activity, Plan, Changes, Review, Comments,
|
||||||
// Artifacts, Model, Workflow, Stats, Routing.
|
// Artifacts, Model, Workflow, Stats, Routing.
|
||||||
const tabTexts = ["Activity", "Plan", "Logs", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing"];
|
const tabTexts = ["Activity", "Plan", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing"];
|
||||||
const tabs = screen.getAllByRole("button").filter((b) =>
|
const tabs = screen.getAllByRole("button").filter((b) =>
|
||||||
tabTexts.includes(b.textContent || "")
|
tabTexts.includes(b.textContent || "")
|
||||||
);
|
);
|
||||||
expect(tabs.map((tab) => tab.textContent)).toEqual(tabTexts);
|
expect(tabs.map((tab) => tab.textContent)).toEqual(tabTexts);
|
||||||
expect(tabs[0].textContent).toBe("Activity");
|
expect(tabs[0].textContent).toBe("Activity");
|
||||||
expect(tabs[1].textContent).toBe("Plan");
|
expect(tabs[1].textContent).toBe("Plan");
|
||||||
expect(tabs[2].textContent).toBe("Logs");
|
expect(tabs[2].textContent).toBe("Changes");
|
||||||
|
expect(screen.queryByRole("button", { name: "Logs" })).toBeNull();
|
||||||
|
|
||||||
expect(container.querySelectorAll(".detail-tab").length).toBe(11);
|
expect(container.querySelectorAll(".detail-tab").length).toBe(10);
|
||||||
// Workflow tab should always appear even when no workflow steps are configured
|
// Workflow tab should always appear even when no workflow steps are configured
|
||||||
expect(screen.getByText("Workflow")).toBeInTheDocument();
|
expect(screen.getByText("Workflow")).toBeInTheDocument();
|
||||||
// Commits tab should NOT appear for non-done tasks
|
// Commits tab should NOT appear for non-done tasks
|
||||||
@@ -985,10 +990,11 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: "Logs" })).toHaveClass("detail-tab-active");
|
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
|
||||||
|
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
|
||||||
expect(container.querySelector(".detail-tabs .detail-tab:first-child")).toHaveTextContent("Activity");
|
expect(container.querySelector(".detail-tabs .detail-tab:first-child")).toHaveTextContent("Activity");
|
||||||
expect(container.querySelector(".detail-tabs .detail-tab:first-child")).not.toHaveClass("detail-tab-active");
|
|
||||||
expect(container.querySelector(".detail-section--chat")).toBeNull();
|
expect(container.querySelector(".detail-section--chat")).toBeNull();
|
||||||
|
expect(container.querySelector(".detail-activity")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("FN-6574 renders Definition-only content when initialTab requests definition", () => {
|
it("FN-6574 renders Definition-only content when initialTab requests definition", () => {
|
||||||
@@ -1040,8 +1046,9 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(chatSection).toBeTruthy();
|
expect(chatSection).toBeTruthy();
|
||||||
expect(chatSection!.querySelector("[data-testid='task-chat-tab']")).toBeTruthy();
|
expect(chatSection!.querySelector("[data-testid='task-chat-tab']")).toBeTruthy();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Logs" }));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
fireEvent.click(screen.getByText("Raw Logs"));
|
||||||
expect(container.querySelector(".detail-body--chat")).toBeNull();
|
expect(container.querySelector(".detail-body--chat")).toBeNull();
|
||||||
expect(container.querySelector(".detail-section--chat")).toBeNull();
|
expect(container.querySelector(".detail-section--chat")).toBeNull();
|
||||||
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
|
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
|
||||||
@@ -1069,8 +1076,8 @@ describe("TaskDetailModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Agent Log full-height layout", () => {
|
describe("Raw Logs full-height layout", () => {
|
||||||
it("applies detail-body--agent-log class when Logs → Agent Log subview is active", () => {
|
it("applies detail-body--agent-log class when Activity → Raw Logs segment is active", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={makeTask({ prompt: "# Hello\n\nContent" })}
|
task={makeTask({ prompt: "# Hello\n\nContent" })}
|
||||||
@@ -1086,11 +1093,12 @@ describe("TaskDetailModal", () => {
|
|||||||
// Initially, detail-body should NOT have the agent-log modifier
|
// Initially, detail-body should NOT have the agent-log modifier
|
||||||
expect(container.querySelector(".detail-body--agent-log")).toBeNull();
|
expect(container.querySelector(".detail-body--agent-log")).toBeNull();
|
||||||
|
|
||||||
// Switch to Logs tab, then Agent Log subview
|
// Switch to Activity tab, then Raw Activity segment
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
expect(container.querySelector(".detail-body--agent-log")).toBeNull(); // Activity subview default
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
expect(container.querySelector(".detail-body--agent-log")).toBeNull(); // Feed segment default
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByText("Raw Logs"));
|
||||||
|
|
||||||
// detail-body should now have the agent-log modifier class
|
// detail-body should now have the agent-log modifier class
|
||||||
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
|
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
|
||||||
@@ -1115,9 +1123,10 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Switch to Logs tab, then Agent Log subview
|
// Switch to Activity tab, then Raw Activity segment
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
fireEvent.click(screen.getByText("Raw Logs"));
|
||||||
|
|
||||||
// The section wrapping AgentLogViewer should have the full-height class
|
// The section wrapping AgentLogViewer should have the full-height class
|
||||||
const section = container.querySelector(".detail-section--agent-log");
|
const section = container.querySelector(".detail-section--agent-log");
|
||||||
@@ -1138,9 +1147,10 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Switch to Logs tab, then Agent Log subview first
|
// Switch to Activity tab, then Raw Activity segment first
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
fireEvent.click(screen.getByText("Raw Logs"));
|
||||||
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
|
expect(container.querySelector(".detail-body--agent-log")).toBeTruthy();
|
||||||
|
|
||||||
// Now enter edit mode via the pencil button in the header
|
// Now enter edit mode via the pencil button in the header
|
||||||
|
|||||||
@@ -15,4 +15,15 @@ describe("TaskDetailModal CSS contract", () => {
|
|||||||
expect(css).toMatch(/\.detail-tabs\s*\{[^}]*touch-action\s*:\s*pan-x\s+pan-y\s*;/);
|
expect(css).toMatch(/\.detail-tabs\s*\{[^}]*touch-action\s*:\s*pan-x\s+pan-y\s*;/);
|
||||||
expect(css).toMatch(/\.detail-tab\s*\{[^}]*flex-shrink\s*:\s*0\s*;/);
|
expect(css).toMatch(/\.detail-tab\s*\{[^}]*flex-shrink\s*:\s*0\s*;/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("FN-7307 keeps Activity segments reachable on narrow task-detail surfaces", async () => {
|
||||||
|
const css = await loadAllAppCssBaseOnly();
|
||||||
|
|
||||||
|
expect(css).toMatch(/\.activity-segmented-control\s*\{[^}]*max-inline-size\s*:\s*100%\s*;/);
|
||||||
|
expect(css).toMatch(/\.activity-segmented-control\s*\{[^}]*overflow-x\s*:\s*auto\s*;/);
|
||||||
|
expect(css).toMatch(/\.activity-segmented-control\s*\{[^}]*touch-action\s*:\s*pan-x\s+pan-y\s*;/);
|
||||||
|
expect(css).toMatch(/\.activity-segment\s*\{[^}]*flex\s*:\s*0\s+0\s+auto\s*;/);
|
||||||
|
expect(css).not.toContain(".log-subview-toggle");
|
||||||
|
expect(css).not.toContain(".log-subview-btn");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2112,7 +2112,8 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
||||||
}, { timeout: 3000 });
|
}, { timeout: 3000 });
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
|
||||||
const activityList = container.querySelector(".detail-activity-list");
|
const activityList = container.querySelector(".detail-activity-list");
|
||||||
expect(activityList).toBeTruthy();
|
expect(activityList).toBeTruthy();
|
||||||
@@ -2258,9 +2259,9 @@ describe("TaskDetailModal", () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only standard tabs should be visible (Definition, Logs, etc.)
|
// Only standard tabs should be visible (Activity, Plan, etc.) without the legacy top-level Logs tab.
|
||||||
expect(screen.getByText("Plan")).toBeDefined();
|
expect(screen.getByText("Plan")).toBeDefined();
|
||||||
expect(screen.getByText("Logs")).toBeDefined();
|
expect(screen.queryByRole("button", { name: "Logs" })).toBeNull();
|
||||||
// Plugin tabs should not exist
|
// Plugin tabs should not exist
|
||||||
expect(screen.queryByText("Plugin A Tab")).toBeNull();
|
expect(screen.queryByText("Plugin A Tab")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { TaskDetailModal, TaskDetailContent } from "../TaskDetailModal";
|
|||||||
setupTaskDetailModalHooks();
|
setupTaskDetailModalHooks();
|
||||||
|
|
||||||
describe("TaskDetailModal", () => {
|
describe("TaskDetailModal", () => {
|
||||||
describe("Agent Log model resolution", () => {
|
describe("Raw Logs model resolution", () => {
|
||||||
// AgentLogViewer only renders the model header when entries.length > 0,
|
// AgentLogViewer only renders the model header when entries.length > 0,
|
||||||
// so we mock useAgentLogs to return at least one entry.
|
// so we mock useAgentLogs to return at least one entry.
|
||||||
const mockLogEntry = { timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const };
|
const mockLogEntry = { timestamp: "2026-01-01T00:00:00Z", taskId: "FN-099", text: "hello", type: "text" as const };
|
||||||
@@ -100,8 +100,8 @@ describe("TaskDetailModal", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openAgentLogAndExpandModelDetails(container: HTMLElement) {
|
async function openAgentLogAndExpandModelDetails(container: HTMLElement) {
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||||
@@ -116,7 +116,7 @@ describe("TaskDetailModal", () => {
|
|||||||
return container.querySelector("[data-testid='agent-log-model-header']") as HTMLElement;
|
return container.querySelector("[data-testid='agent-log-model-header']") as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
it("uses task effective settings success path for Agent Log model display", async () => {
|
it("uses task effective settings success path for Raw Logs model display", async () => {
|
||||||
const { fetchTaskEffectiveSettings, fetchSettings } = await import("../../api");
|
const { fetchTaskEffectiveSettings, fetchSettings } = await import("../../api");
|
||||||
const { useAgentLogs } = await import("../../hooks/useAgentLogs");
|
const { useAgentLogs } = await import("../../hooks/useAgentLogs");
|
||||||
|
|
||||||
@@ -574,8 +574,8 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||||
expect(header).toBeTruthy();
|
expect(header).toBeTruthy();
|
||||||
@@ -631,8 +631,8 @@ describe("TaskDetailModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||||
expect(header).toBeTruthy();
|
expect(header).toBeTruthy();
|
||||||
@@ -872,7 +872,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(segments[1].getAttribute("data-tooltip")).toBe("Add tests (in-progress)");
|
expect(segments[1].getAttribute("data-tooltip")).toBe("Add tests (in-progress)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("step progress only renders in Definition tab, not in Agent Log subview", () => {
|
it("step progress only renders in Definition tab, not in Raw Logs segment", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
initialTab="definition"
|
initialTab="definition"
|
||||||
@@ -894,11 +894,11 @@ describe("TaskDetailModal", () => {
|
|||||||
// Should be visible in Definition tab
|
// Should be visible in Definition tab
|
||||||
expect(container.querySelector(".detail-step-progress")).toBeTruthy();
|
expect(container.querySelector(".detail-step-progress")).toBeTruthy();
|
||||||
|
|
||||||
// Switch to Logs tab, then Agent Log subview
|
// Switch to Activity tab, then Raw Logs segment
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
fireEvent.click(screen.getByText("Agent Log"));
|
fireEvent.click(screen.getByRole("tab", { name: "Raw Logs" }));
|
||||||
|
|
||||||
// Should not be visible in Agent Log subview
|
// Should not be visible in Raw Logs segment
|
||||||
expect(container.querySelector(".detail-step-progress")).toBeNull();
|
expect(container.querySelector(".detail-step-progress")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2428,7 +2428,8 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
expect(container.querySelector(".markdown-body")).toBeTruthy();
|
||||||
}, { timeout: 3000 });
|
}, { timeout: 3000 });
|
||||||
|
|
||||||
fireEvent.click(screen.getByText("Logs"));
|
fireEvent.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
fireEvent.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
|
||||||
const activityList = container.querySelector(".detail-activity-list");
|
const activityList = container.querySelector(".detail-activity-list");
|
||||||
expect(activityList).toBeTruthy();
|
expect(activityList).toBeTruthy();
|
||||||
|
|||||||
@@ -265,8 +265,8 @@ describe("TaskDetailModal GitHub tracking CTA", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("TaskDetailModal Logs activity loading", () => {
|
describe("TaskDetailModal Activity feed loading", () => {
|
||||||
function renderLogsModal(task: ReturnType<typeof makeTask> | Record<string, unknown>) {
|
function renderActivityFeedModal(task: ReturnType<typeof makeTask> | Record<string, unknown>) {
|
||||||
return render(
|
return render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
task={task as any}
|
task={task as any}
|
||||||
@@ -295,13 +295,13 @@ describe("TaskDetailModal Logs activity loading", () => {
|
|||||||
vi.mocked(fetchTaskDetail).mockReset();
|
vi.mocked(fetchTaskDetail).mockReset();
|
||||||
vi.mocked(fetchTaskDetail).mockImplementationOnce(() => new Promise(() => {}));
|
vi.mocked(fetchTaskDetail).mockImplementationOnce(() => new Promise(() => {}));
|
||||||
|
|
||||||
renderLogsModal(makeSlimTask());
|
renderActivityFeedModal(makeSlimTask());
|
||||||
|
|
||||||
expect(await screen.findByRole("status")).toHaveTextContent("Loading activity…");
|
expect(await screen.findByRole("status")).toHaveTextContent("Loading activity…");
|
||||||
expect(screen.queryByText("(no activity)")).not.toBeInTheDocument();
|
expect(screen.queryByText("(no activity)")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows activity loading when switching to Logs before slim task detail resolves", async () => {
|
it("shows activity loading when switching to Activity Feed before slim task detail resolves", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const { fetchTaskDetail } = await import("../../api");
|
const { fetchTaskDetail } = await import("../../api");
|
||||||
vi.mocked(fetchTaskDetail).mockReset();
|
vi.mocked(fetchTaskDetail).mockReset();
|
||||||
@@ -320,7 +320,8 @@ describe("TaskDetailModal Logs activity loading", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Logs" }));
|
await user.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
|
await user.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
expect(await screen.findByRole("status")).toHaveTextContent("Loading activity…");
|
expect(await screen.findByRole("status")).toHaveTextContent("Loading activity…");
|
||||||
expect(screen.queryByText("(no activity)")).not.toBeInTheDocument();
|
expect(screen.queryByText("(no activity)")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -330,7 +331,7 @@ describe("TaskDetailModal Logs activity loading", () => {
|
|||||||
vi.mocked(fetchTaskDetail).mockReset();
|
vi.mocked(fetchTaskDetail).mockReset();
|
||||||
vi.mocked(fetchTaskDetail).mockResolvedValueOnce(makeTask({ id: "FN-6040", prompt: "# Loaded", log: [] }));
|
vi.mocked(fetchTaskDetail).mockResolvedValueOnce(makeTask({ id: "FN-6040", prompt: "# Loaded", log: [] }));
|
||||||
|
|
||||||
renderLogsModal(makeSlimTask());
|
renderActivityFeedModal(makeSlimTask());
|
||||||
|
|
||||||
expect(await screen.findByText("(no activity)")).toBeInTheDocument();
|
expect(await screen.findByText("(no activity)")).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
||||||
@@ -348,7 +349,7 @@ describe("TaskDetailModal Logs activity loading", () => {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { container } = renderLogsModal(makeSlimTask());
|
const { container } = renderActivityFeedModal(makeSlimTask());
|
||||||
|
|
||||||
await screen.findByText("newer entry");
|
await screen.findByText("newer entry");
|
||||||
const actions = Array.from(container.querySelectorAll(".detail-log-action")).map((node) => node.textContent);
|
const actions = Array.from(container.querySelectorAll(".detail-log-action")).map((node) => node.textContent);
|
||||||
@@ -366,7 +367,7 @@ describe("TaskDetailModal Logs activity loading", () => {
|
|||||||
activityLogTruncatedCount: 25,
|
activityLogTruncatedCount: 25,
|
||||||
} as any));
|
} as any));
|
||||||
|
|
||||||
renderLogsModal(makeSlimTask());
|
renderActivityFeedModal(makeSlimTask());
|
||||||
|
|
||||||
expect(await screen.findByText("Showing the most recent 1 activity entries.")).toBeInTheDocument();
|
expect(await screen.findByText("Showing the most recent 1 activity entries.")).toBeInTheDocument();
|
||||||
expect(screen.getByText("kept entry")).toBeInTheDocument();
|
expect(screen.getByText("kept entry")).toBeInTheDocument();
|
||||||
@@ -430,8 +431,8 @@ describe("TaskDetailModal Chat task merge", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("TaskDetailModal Logs agent loading", () => {
|
describe("TaskDetailModal Raw Logs agent loading", () => {
|
||||||
it("shows the Agent Log loading indicator when entering the subview", async () => {
|
it("shows the Raw Logs loading indicator when entering the segment", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const { useAgentLogs } = await import("../../hooks/useAgentLogs");
|
const { useAgentLogs } = await import("../../hooks/useAgentLogs");
|
||||||
const mockUseAgentLogs = vi.mocked(useAgentLogs);
|
const mockUseAgentLogs = vi.mocked(useAgentLogs);
|
||||||
@@ -458,8 +459,9 @@ describe("TaskDetailModal Logs agent loading", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Logs" }));
|
await user.click(screen.getByRole("button", { name: "Activity" }));
|
||||||
await user.click(screen.getByRole("button", { name: "Agent Log" }));
|
await user.click(screen.getByRole("tab", { name: "Feed" }));
|
||||||
|
await user.click(screen.getByRole("tab", { name: "Raw Logs" }));
|
||||||
|
|
||||||
expect(screen.getByText("Loading agent logs…")).toBeInTheDocument();
|
expect(screen.getByText("Loading agent logs…")).toBeInTheDocument();
|
||||||
expect(screen.queryByText("No agent output yet.")).not.toBeInTheDocument();
|
expect(screen.queryByText("No agent output yet.")).not.toBeInTheDocument();
|
||||||
@@ -688,8 +690,8 @@ describe("TaskDetailModal in-review stall diagnostics", () => {
|
|||||||
expect(screen.getByText("Open the Review tab to see which step is blocking, then fix the failure or override the step.")).toBeInTheDocument();
|
expect(screen.getByText("Open the Review tab to see which step is blocking, then fix the failure or override the step.")).toBeInTheDocument();
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "View activity log" }));
|
await user.click(screen.getByRole("button", { name: "View activity log" }));
|
||||||
expect(screen.getByRole("button", { name: "Logs" })).toHaveClass("detail-tab-active");
|
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
|
||||||
expect(document.querySelector(".log-subview-toggle .log-subview-btn-active")).toHaveTextContent("Activity");
|
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
|
||||||
const highlighted = document.querySelector(".detail-log-entry--stall-highlight .detail-log-action");
|
const highlighted = document.querySelector(".detail-log-entry--stall-highlight .detail-log-action");
|
||||||
expect(highlighted?.textContent).toContain("In-review stall surfaced [merge-blocker]");
|
expect(highlighted?.textContent).toContain("In-review stall surfaced [merge-blocker]");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import type { ToastType } from "./useToast";
|
|||||||
import { removeScopedItem } from "../utils/projectStorage";
|
import { removeScopedItem } from "../utils/projectStorage";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:TaskDetailActivityTab 2026-06-30-00:00:
|
FNXC:TaskDetailActivity 2026-06-30-22:15:
|
||||||
Keep `chat` as the public initial-tab id for the renamed Activity task-detail tab so existing dashboard callers and deep links remain compatible until the future planner Chat tab ships under its own contract.
|
Keep `chat` as the public initial-tab id for the renamed Activity task-detail tab so existing dashboard callers and deep links remain compatible until the future planner Chat tab ships under its own contract. Legacy `logs` callers are also preserved by TaskDetailModal, which opens Activity → Feed instead of rendering a top-level Logs tab.
|
||||||
*/
|
*/
|
||||||
export type DetailTaskTab =
|
export type DetailTaskTab =
|
||||||
| "summary"
|
| "summary"
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
|
|||||||
/**
|
/**
|
||||||
* Tab identifiers for the task detail modal. Mirrors the dashboard's local enum.
|
* Tab identifiers for the task detail modal. Mirrors the dashboard's local enum.
|
||||||
*
|
*
|
||||||
* FNXC:TaskDetailActivityTab 2026-06-30-00:00:
|
* FNXC:TaskDetailActivity 2026-06-30-22:15:
|
||||||
* Plugins should continue passing `chat` to open the renamed Activity tab; the id is stable compatibility surface, while the top-level label is no longer Chat.
|
* Plugins should continue passing `chat` to open the renamed Activity tab; the id is stable compatibility surface, while the top-level label is no longer Chat. Legacy `logs` requests remain accepted by the host and route to Activity → Feed because Logs is no longer a visible top-level tab.
|
||||||
*/
|
*/
|
||||||
export type DetailTaskTab = "summary" | "chat" | "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "pr" | "retries";
|
export type DetailTaskTab = "summary" | "chat" | "definition" | "logs" | "changes" | "comments" | "model" | "workflow" | "pr" | "retries";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user