FN-7335: default task details to Activity first
Default task detail opens now prioritize Activity while preserving an opt-in Chat-first mode. - Add a project setting and dashboard plumbing for Chat-first task detail ordering. - Default non-done task detail opens to Activity/Live while keeping explicit Chat, Activity, and Logs links stable. - Update Settings, docs, changeset, and regression coverage for Activity-first and Chat-first behavior. Files changed: .changeset/activity-first-task-detail.md | 7 +++ docs/dashboard-guide.md | 6 +- .../core/src/__tests__/settings-defaults.test.ts | 13 ++++ packages/core/src/settings-schema.ts | 5 ++ packages/core/src/types.ts | 5 ++ packages/dashboard/app/App.tsx | 7 ++- packages/dashboard/app/components/AppModals.tsx | 2 + packages/dashboard/app/components/ListView.tsx | 3 + .../dashboard/app/components/SettingsModal.tsx | 6 ++ .../dashboard/app/components/TaskDetailModal.tsx | 71 ++++++++++++++------- .../TaskDetailModal.attachments-and-tabs.test.tsx | 19 ++++-- ...etailModal.responsive-and-dependencies.test.tsx | 2 +- .../TaskDetailModal.task-activity-chat.test.tsx | 72 +++++++++++++++++++--- .../components/__tests__/TaskDetailModal.test.tsx | 3 + .../app/components/dashboard/MainContent.tsx | 3 + .../dashboard/app/components/dashboard/types.ts | 1 + .../settings/sections/AppearanceSection.tsx | 8 +++ .../sections/__tests__/AppearanceSection.test.tsx | 20 ++++++ .../app/components/useRightDockController.tsx | 2 + packages/dashboard/app/hooks/useAppSettings.ts | 9 +++ 20 files changed, 220 insertions(+), 44 deletions(-) Fusion-Task-Id: FN-7335 Fusion-Task-Lineage: d983c41a-f2f5-4e53-aee4-3ec3a4c057a3 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/activity-first-task-detail.md
Normal file
7
.changeset/activity-first-task-detail.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Make task details open Activity first by default with an opt-in Chat-first setting.
|
||||
category: feature
|
||||
dev: Adds project setting `taskDetailChatFirst` and exposes it in Settings → Appearance.
|
||||
@@ -1119,10 +1119,10 @@ Recommended workflow: ordinary chains stay as `Blocks N` so noise stays low, hig
|
||||
### Activity → Raw Logs view
|
||||
|
||||
<!-- 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. -->
|
||||
<!-- FNXC:TaskDetailPlannerChat 2026-06-30-23:58: Task Detail is Chat-first for active tasks: Chat renders before Activity and defaults open for non-done tasks, while Activity keeps the legacy `chat` id for explicit links and operational Live/Feed/Raw Logs history. -->
|
||||
The **Activity** tab is the second task-detail tab, after **Chat**, 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.
|
||||
<!-- FNXC:TaskDetailActivityFirst 2026-06-30-23:59: Task Detail is Activity-first by default for active tasks: Activity renders before planner Chat and omitted non-done opens land on Activity → Live. Settings → Appearance → Open task details with Chat first restores the previous Chat-first order/default without changing explicit Activity, Chat, or Logs links. -->
|
||||
The **Activity** tab is the first task-detail tab by default 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 preceding 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. Opening the tab with no existing history does not create a database chat row; when no planner-chat history is found, Chat shows a guided empty state with starter prompts for recent activity, current status/blockers, next best action, and plan/definition review. Selecting a starter creates/resumes the planner chat and sends that prompt as an ordinary chat message through the task-context-aware planner-chat composer/stream path. 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. Planner Chat defaults to focused mode, keeps its composer visible at the bottom while only the transcript scrolls, and on narrow/mobile task-detail layouts collapses nonessential rows above the chat until the user selects the Chat collapse control.
|
||||
The top-level **Chat** tab opens the planner-model conversation for the same task instead of posting steering comments. It appears after Activity by default, or before Activity when **Settings → Appearance → Open task details with Chat first** is enabled. Each send includes server-built, bounded context for the task id, status/column/progress/current step, dependencies, recent activity/comment excerpts, prompt/plan content, and available source/review state; unavailable sections are labeled so the planner states uncertainty rather than inventing execution evidence. Opening the tab with no existing history does not create a database chat row; when no planner-chat history is found, Chat shows a guided empty state with starter prompts for recent activity, current status/blockers, next best action, and plan/definition review. Selecting a starter creates/resumes the planner chat and sends that prompt as an ordinary chat message through the task-context-aware planner-chat composer/stream path. 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. Planner Chat defaults to focused mode, keeps its composer visible at the bottom while only the transcript scrolls, and on narrow/mobile task-detail layouts collapses nonessential rows above the chat until the user selects the Chat collapse control.
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -163,6 +163,19 @@ describe("settings defaults invariants", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("taskDetailChatFirst default", () => {
|
||||
it("keeps taskDetailChatFirst explicitly false in project defaults", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.taskDetailChatFirst).toBe(false);
|
||||
expect("taskDetailChatFirst" in DEFAULT_PROJECT_SETTINGS).toBe(true);
|
||||
expect(PROJECT_SETTINGS_KEYS).toContain("taskDetailChatFirst");
|
||||
});
|
||||
|
||||
it("keeps taskDetailChatFirst project-scoped only", () => {
|
||||
expect("taskDetailChatFirst" in DEFAULT_GLOBAL_SETTINGS).toBe(false);
|
||||
expect(GLOBAL_SETTINGS_KEYS).not.toContain("taskDetailChatFirst");
|
||||
});
|
||||
});
|
||||
|
||||
describe("quickChatCloseOnOutsideClick default", () => {
|
||||
it("keeps Quick Chat outside-click dismissal explicitly true in project defaults", () => {
|
||||
expect(DEFAULT_PROJECT_SETTINGS.quickChatCloseOnOutsideClick).toBe(true);
|
||||
|
||||
@@ -332,6 +332,11 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
Default off preserves current mobile board-card task detail behavior. The dashboard only consults this project setting for mobile board-card clicks without a deep tab, and reuses the existing task pop-out surface without changing desktop/right-dock routing.
|
||||
*/
|
||||
openMobileTasksInPopup: false,
|
||||
/*
|
||||
FNXC:TaskDetailActivityFirst 2026-06-30-23:59:
|
||||
Project task-detail defaults are Activity-first unless this opt-in is true. Keeping the default false preserves explicit deep-link ids while making omitted non-done task opens land on Activity → Live.
|
||||
*/
|
||||
taskDetailChatFirst: false,
|
||||
executorAllowSiblingBranchRename: false,
|
||||
worktreeNaming: "random",
|
||||
worktrunk: {
|
||||
|
||||
@@ -3736,6 +3736,11 @@ export interface ProjectSettings {
|
||||
* This project-scoped setting is default-off so mobile board navigation is unchanged until operators opt in. When enabled, it applies only to mobile viewport board-card clicks with no deep initial tab and reuses the existing task pop-out/FloatingWindow path; desktop/tablet right-dock routing and all non-board task-open paths remain governed by their existing settings and handlers.
|
||||
*/
|
||||
openMobileTasksInPopup?: boolean;
|
||||
/**
|
||||
* FNXC:TaskDetailActivityFirst 2026-06-30-23:59:
|
||||
* Default-off keeps task details Activity-first so omitted non-done opens land on the legacy `chat` Activity → Live surface. Operators can set true to restore Chat-first ordering/default while explicit Activity/Chat/Logs deep links remain stable.
|
||||
*/
|
||||
taskDetailChatFirst?: boolean;
|
||||
/** When true, restores the legacy behavior of silently creating sibling
|
||||
* branches like `fusion/FN-123-2` when the canonical task branch is already
|
||||
* checked out elsewhere. Default: false. */
|
||||
|
||||
@@ -560,6 +560,7 @@ function AppInner() {
|
||||
capacityRiskTodoThreshold,
|
||||
openTasksInRightSidebar,
|
||||
openMobileTasksInPopup,
|
||||
taskDetailChatFirst,
|
||||
quickChatButtonMode,
|
||||
quickChatCloseOnOutsideClick,
|
||||
dismissModalsOnOutsideClick,
|
||||
@@ -1135,7 +1136,7 @@ function AppInner() {
|
||||
|
||||
// Props for the extracted <MainContent> switch (see components/dashboard/MainContent.tsx).
|
||||
// Every value is passed by its App name; the switch renders the same subtrees as before.
|
||||
const rightDock = useRightDockController({ active: rightDockActive, projectId: currentProject?.id, addToast, settingsLoaded, researchReadinessVersion, goalAnchorId, tasks: isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : tasks, workflowSteps, subscribePluginEvents, openDetailTask, openFileInBrowser, onMoveTask: moveTask, onDeleteTask: deleteTask, onArchiveTask: archiveTask, onMergeTask: mergeTask, onRetryTask: retryTask, onResetTask: resetTask, onDuplicateTask: duplicateTask, onTaskUpdated: (task: Task) => ingestCreatedTasks([task]), openSettings: (section?: string) => openSettingsWithNav(section as SectionId), onOpenUsage: openUsageWithNav, onOpenActivityLog: openActivityLogWithNav, onOpenGitHubImport: openGitHubImportWithNav, onOpenGitManager: openGitManagerWithNav, onOpenSchedules: openSchedulesWithNav, onSendSelectionToTask: modalManager.openNewTaskWithDescription, onCreateTaskFromInsight: handleInsightTaskCreate, onNavigateToMission: handleOpenMission, onTaskCreated: (task: Task) => ingestCreatedTasks([task]), prAuthAvailable, autoMerge, visibilityOptions: { experimentalFeatures: { insights: insightsEnabled, memoryView: memoryEnabled, devServerView: devServerEnabled, researchView: researchEnabled, evalsView: evalsEnabled, goalsView: goalsEnabled }, showSkillsTab: skillsEnabled, todosEnabled, pluginDashboardViews }, footerVisible: executorFooterVisible });
|
||||
const rightDock = useRightDockController({ active: rightDockActive, projectId: currentProject?.id, addToast, settingsLoaded, researchReadinessVersion, goalAnchorId, tasks: isRemote && remoteData.tasks.length > 0 ? remoteData.tasks : tasks, workflowSteps, subscribePluginEvents, openDetailTask, openFileInBrowser, onMoveTask: moveTask, onDeleteTask: deleteTask, onArchiveTask: archiveTask, onMergeTask: mergeTask, onRetryTask: retryTask, onResetTask: resetTask, onDuplicateTask: duplicateTask, onTaskUpdated: (task: Task) => ingestCreatedTasks([task]), openSettings: (section?: string) => openSettingsWithNav(section as SectionId), onOpenUsage: openUsageWithNav, onOpenActivityLog: openActivityLogWithNav, onOpenGitHubImport: openGitHubImportWithNav, onOpenGitManager: openGitManagerWithNav, onOpenSchedules: openSchedulesWithNav, onSendSelectionToTask: modalManager.openNewTaskWithDescription, onCreateTaskFromInsight: handleInsightTaskCreate, onNavigateToMission: handleOpenMission, onTaskCreated: (task: Task) => ingestCreatedTasks([task]), prAuthAvailable, autoMerge, taskDetailChatFirst, visibilityOptions: { experimentalFeatures: { insights: insightsEnabled, memoryView: memoryEnabled, devServerView: devServerEnabled, researchView: researchEnabled, evalsView: evalsEnabled, goalsView: goalsEnabled }, showSkillsTab: skillsEnabled, todosEnabled, pluginDashboardViews }, footerVisible: executorFooterVisible });
|
||||
|
||||
/*
|
||||
FNXC:OpenTasksInRightSidebar 2026-06-28-00:00:
|
||||
@@ -1219,6 +1220,7 @@ function AppInner() {
|
||||
autoMerge,
|
||||
mergeStrategy,
|
||||
settingsLoaded,
|
||||
taskDetailChatFirst,
|
||||
skillsEnabled,
|
||||
experimentalFeatures,
|
||||
setQuickChatOpen,
|
||||
@@ -1657,6 +1659,7 @@ function AppInner() {
|
||||
addToast={addToast}
|
||||
prAuthAvailable={prAuthAvailable}
|
||||
autoMergeEnabled={autoMerge}
|
||||
taskDetailChatFirst={taskDetailChatFirst}
|
||||
/>
|
||||
</FloatingWindow>
|
||||
);
|
||||
@@ -1682,7 +1685,7 @@ function AppInner() {
|
||||
onSubtaskBreakdown={subtaskBreakdownEnabled ? openSubtaskBreakdownWithNav : undefined}
|
||||
taskOperations={{ moveTask, deleteTask, mergeTask, archiveTask, retryTask, resetTask, duplicateTask }}
|
||||
deepLink={{ handleDetailClose }}
|
||||
settings={{ prAuthAvailable, autoMerge, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors, resolvedThemeMode, setThemeMode, setColorTheme, setDashboardFontScalePct, setShadcnCustomColors, setQuickChatButtonModeImmediate }}
|
||||
settings={{ prAuthAvailable, autoMerge, taskDetailChatFirst, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors, resolvedThemeMode, setThemeMode, setColorTheme, setDashboardFontScalePct, setShadcnCustomColors, setQuickChatButtonModeImmediate }}
|
||||
onSettingsClose={handleSettingsCloseWithNav}
|
||||
onReopenOnboarding={reopenOnboardingWithNav}
|
||||
onOpenApprovals={(_approvalId) => handleTaskViewChange("mailbox")}
|
||||
|
||||
@@ -73,6 +73,7 @@ interface AppModalsProps {
|
||||
settings: {
|
||||
prAuthAvailable: boolean;
|
||||
autoMerge: boolean;
|
||||
taskDetailChatFirst: boolean;
|
||||
themeMode: ThemeMode;
|
||||
colorTheme: ColorTheme;
|
||||
dashboardFontScalePct: number;
|
||||
@@ -328,6 +329,7 @@ export function AppModals({
|
||||
addToast={addToast}
|
||||
prAuthAvailable={settings.prAuthAvailable}
|
||||
autoMergeEnabled={settings.autoMerge}
|
||||
taskDetailChatFirst={settings.taskDetailChatFirst}
|
||||
onOpenWorkflowEditor={() => modalManager.openWorkflowEditor()}
|
||||
initialTab={modalManager.detailTaskInitialTab}
|
||||
/>
|
||||
|
||||
@@ -262,6 +262,7 @@ interface ListViewProps {
|
||||
lastFetchTimeMs?: number;
|
||||
prAuthAvailable?: boolean;
|
||||
autoMerge?: boolean;
|
||||
taskDetailChatFirst?: boolean;
|
||||
/** Project merge strategy so list context menus match Task Detail before a PR exists. */
|
||||
mergeStrategy?: string;
|
||||
onOpenWorkflowEditor?: (workflowId?: string) => void;
|
||||
@@ -334,6 +335,7 @@ export function ListView({
|
||||
lastFetchTimeMs,
|
||||
prAuthAvailable,
|
||||
autoMerge,
|
||||
taskDetailChatFirst = false,
|
||||
mergeStrategy = "direct",
|
||||
onOpenWorkflowEditor,
|
||||
onCreateWorkflow,
|
||||
@@ -2830,6 +2832,7 @@ export function ListView({
|
||||
addToast={addToast}
|
||||
prAuthAvailable={prAuthAvailable}
|
||||
autoMergeEnabled={autoMerge}
|
||||
taskDetailChatFirst={taskDetailChatFirst}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -740,6 +740,7 @@ export function SettingsModal({
|
||||
showWorktreeGrouping: false,
|
||||
openTasksInRightSidebar: false,
|
||||
openMobileTasksInPopup: false,
|
||||
taskDetailChatFirst: false,
|
||||
executorAllowSiblingBranchRename: false,
|
||||
worktreeNaming: "random",
|
||||
worktreeCopyFiles: [],
|
||||
@@ -1001,6 +1002,11 @@ export function SettingsModal({
|
||||
...s,
|
||||
ignoreHiddenOverlapPaths: s.ignoreHiddenOverlapPaths ?? true,
|
||||
allowAbsoluteFileBrowserPaths: s.allowAbsoluteFileBrowserPaths === true,
|
||||
/*
|
||||
FNXC:TaskDetailActivityFirst 2026-06-30-23:59:
|
||||
The Settings form normalizes missing taskDetailChatFirst to false so new and upgraded projects show the Activity-first default until an operator explicitly opts into Chat-first.
|
||||
*/
|
||||
taskDetailChatFirst: s.taskDetailChatFirst === true,
|
||||
mergeIntegrationWorktree: normalizeMergeIntegrationWorktreeMode(s.mergeIntegrationWorktree),
|
||||
mergeAdvanceAutoSync: normalizeMergeAdvanceAutoSyncMode(s.mergeAdvanceAutoSync),
|
||||
maxAutoMergeRetries: resolveMaxAutoMergeRetriesForSettingsForm(s),
|
||||
|
||||
@@ -202,8 +202,8 @@ The existing task activity/steering surface keeps the stable internal `chat` tab
|
||||
FNXC:TaskDetailPlannerChat 2026-06-30-22:30:
|
||||
Task detail 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 top-level Chat tab for task-aware planning conversation.
|
||||
|
||||
FNXC:TaskDetailPlannerChat 2026-06-30-23:58:
|
||||
Task details should prioritize task-aware Chat: render Chat before Activity, default omitted non-done task opens to `planner-chat`, and reserve expanded/focused layout behavior for the planner Chat surface rather than the legacy Activity `chat` id.
|
||||
FNXC:TaskDetailActivityFirst 2026-06-30-23:59:
|
||||
Task details are Activity-first by default: render Activity before planner Chat and make omitted non-done opens land on Activity → Live. The project `taskDetailChatFirst` setting restores Chat-first ordering/default when true; explicit `initialTab` deep links always win.
|
||||
|
||||
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 → Live (internal `current`). Legacy `initialTab="logs"` now routes to Activity → Feed, and Raw Logs remains an Activity segment.
|
||||
@@ -214,7 +214,7 @@ The first Activity segment keeps the stable internal `current` id for legacy seg
|
||||
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 {
|
||||
function resolveDefaultTab(initialTab: TabId | undefined, column: ColumnId, taskDetailChatFirst = false): TabId {
|
||||
if (initialTab === "retries") {
|
||||
return "definition";
|
||||
}
|
||||
@@ -224,7 +224,10 @@ function resolveDefaultTab(initialTab: TabId | undefined, column: ColumnId): Tab
|
||||
if (initialTab) {
|
||||
return initialTab;
|
||||
}
|
||||
return column === "done" ? "summary" : "planner-chat";
|
||||
if (column === "done") {
|
||||
return "summary";
|
||||
}
|
||||
return taskDetailChatFirst ? "planner-chat" : "chat";
|
||||
}
|
||||
|
||||
function resolveDefaultActivitySegment(initialTab: TabId | undefined): ActivitySegment {
|
||||
@@ -322,6 +325,8 @@ export interface TaskDetailModalProps {
|
||||
initialTab?: TabId;
|
||||
/** Mobile-only header affordance mode. */
|
||||
mobileHeaderMode?: "close" | "back";
|
||||
/** Project setting: true restores Chat-first tab order/default; false or missing uses Activity-first. */
|
||||
taskDetailChatFirst?: boolean;
|
||||
/** Pre-resolved workflow field defs for this task's workflow (U13/KTD-14).
|
||||
* When provided (e.g. threaded from a Board that already holds the payload)
|
||||
* the modal skips its own board-workflows fetch entirely. Falls back to the
|
||||
@@ -522,10 +527,11 @@ export function TaskDetailContent({
|
||||
autoMergeEnabled: autoMergeEnabledProp,
|
||||
onOpenWorkflowEditor,
|
||||
/**
|
||||
* FNXC:TaskDetailPlannerChat 2026-06-30-22:30:
|
||||
* The Activity tab is still addressed as `chat` internally so existing callers and deep links do not break; the visible Chat tab uses `planner-chat` for planner-model conversation.
|
||||
* FNXC:TaskDetailActivityFirst 2026-06-30-23:59:
|
||||
* The Activity tab is still addressed as `chat` internally so existing callers and deep links do not break; the visible Chat tab uses `planner-chat` and only becomes the omitted non-done default when taskDetailChatFirst is true.
|
||||
*/
|
||||
initialTab,
|
||||
taskDetailChatFirst = false,
|
||||
mobileHeaderMode = "close",
|
||||
embedded = false,
|
||||
onRequestClose,
|
||||
@@ -536,7 +542,7 @@ export function TaskDetailContent({
|
||||
const { t } = useTranslation("app");
|
||||
const columnLabel = useColumnLabel();
|
||||
const fileBrowser = useFileBrowser();
|
||||
const [activeTab, setActiveTab] = useState<TabId>(() => resolveDefaultTab(initialTab, task.column));
|
||||
const [activeTab, setActiveTab] = useState<TabId>(() => resolveDefaultTab(initialTab, task.column, taskDetailChatFirst));
|
||||
const [activitySegment, setActivitySegment] = useState<ActivitySegment>(() => resolveDefaultActivitySegment(initialTab));
|
||||
const [activityExpanded, setActivityExpanded] = useState(false);
|
||||
const [plannerChatExpanded, setPlannerChatExpanded] = useState(true);
|
||||
@@ -668,12 +674,12 @@ export function TaskDetailContent({
|
||||
|
||||
// Sync activeTab when the caller changes initialTab (e.g. opening a different tab)
|
||||
useEffect(() => {
|
||||
setActiveTab(resolveDefaultTab(initialTab, task.column));
|
||||
setActiveTab(resolveDefaultTab(initialTab, task.column, taskDetailChatFirst));
|
||||
setActivitySegment(resolveDefaultActivitySegment(initialTab));
|
||||
if (initialTab === "retries") {
|
||||
setRetriesExpanded(true);
|
||||
}
|
||||
}, [initialTab, task.column]);
|
||||
}, [initialTab, task.column, taskDetailChatFirst]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === "pr" && task.column !== "in-review") {
|
||||
@@ -3171,21 +3177,40 @@ export function TaskDetailContent({
|
||||
<>
|
||||
<div className="detail-tabs">
|
||||
{/*
|
||||
FNXC:TaskDetailPlannerChat 2026-06-30-23:58:
|
||||
Chat is the first visible task-detail tab and maps to `planner-chat` so omitted non-done task opens prioritize task-aware planning conversation. Activity stays immediately after Chat with the legacy `chat` id for explicit `initialTab="chat"` callers and operational steering/feed/raw-log history.
|
||||
FNXC:TaskDetailActivityFirst 2026-06-30-23:59:
|
||||
Activity is first/default for omitted non-done task opens unless the project setting taskDetailChatFirst is true. Keep both stable ids (`chat` for Activity, `planner-chat` for Chat) so explicit deep links and plugin callers retain their destinations.
|
||||
*/}
|
||||
<button
|
||||
className={`detail-tab${activeTab === "planner-chat" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("planner-chat")}
|
||||
>
|
||||
{t("taskDetail.tabs.chat", "Chat")}
|
||||
</button>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "chat" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("chat")}
|
||||
>
|
||||
{t("taskDetail.tabs.activity", "Activity")}
|
||||
</button>
|
||||
{taskDetailChatFirst ? (
|
||||
<>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "planner-chat" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("planner-chat")}
|
||||
>
|
||||
{t("taskDetail.tabs.chat", "Chat")}
|
||||
</button>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "chat" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("chat")}
|
||||
>
|
||||
{t("taskDetail.tabs.activity", "Activity")}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "chat" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("chat")}
|
||||
>
|
||||
{t("taskDetail.tabs.activity", "Activity")}
|
||||
</button>
|
||||
<button
|
||||
className={`detail-tab${activeTab === "planner-chat" ? " detail-tab-active" : ""}`}
|
||||
onClick={() => setActiveTab("planner-chat")}
|
||||
>
|
||||
{t("taskDetail.tabs.chat", "Chat")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{task.column === "done" && (
|
||||
<button
|
||||
className={`detail-tab${activeTab === "summary" ? " detail-tab-active" : ""}`}
|
||||
|
||||
@@ -451,10 +451,11 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
|
||||
describe("tab toggle", () => {
|
||||
it("defaults non-done omitted tabs to planner Chat", () => {
|
||||
it("restores planner Chat as the omitted non-done default when Chat-first is enabled", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nContent" })}
|
||||
taskDetailChatFirst
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -808,15 +809,15 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
// For an in-progress task (no workflow steps, no merge commit), the
|
||||
// top-level tabs are: Chat, Activity, Plan, Changes, Review, Comments,
|
||||
// top-level tabs are: Activity, Chat, Plan, Changes, Review, Comments,
|
||||
// Artifacts, Model, Workflow, Stats, Routing.
|
||||
const tabTexts = ["Chat", "Activity", "Plan", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing"];
|
||||
const tabTexts = ["Activity", "Chat", "Plan", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing"];
|
||||
const tabs = screen.getAllByRole("button").filter((b) =>
|
||||
tabTexts.includes(b.textContent || "")
|
||||
);
|
||||
expect(tabs.map((tab) => tab.textContent)).toEqual(tabTexts);
|
||||
expect(tabs[0].textContent).toBe("Chat");
|
||||
expect(tabs[1].textContent).toBe("Activity");
|
||||
expect(tabs[0].textContent).toBe("Activity");
|
||||
expect(tabs[1].textContent).toBe("Chat");
|
||||
expect(tabs[2].textContent).toBe("Plan");
|
||||
expect(tabs[3].textContent).toBe("Changes");
|
||||
expect(screen.queryByRole("button", { name: "Logs" })).toBeNull();
|
||||
@@ -925,6 +926,7 @@ describe("TaskDetailModal", () => {
|
||||
prompt: "# Hello\n\nContent",
|
||||
log: [{ timestamp: "2026-01-01T00:00:00Z", action: "Expanded feed entry", outcome: "visible" }],
|
||||
})}
|
||||
taskDetailChatFirst
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -996,6 +998,7 @@ describe("TaskDetailModal", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nContent", branchContext })}
|
||||
taskDetailChatFirst
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -1027,6 +1030,7 @@ describe("TaskDetailModal", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nContent", branchContext: undefined })}
|
||||
taskDetailChatFirst
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -1129,10 +1133,11 @@ describe("TaskDetailModal", () => {
|
||||
expect(screen.queryByTestId("task-chat-expand-toggle")).toBeNull();
|
||||
});
|
||||
|
||||
it("FN-6532 defaults to planner Chat first while preserving explicit Activity requests", () => {
|
||||
it("FN-6532 restores planner Chat first when Chat-first is enabled while preserving explicit Activity requests", () => {
|
||||
const { container, rerender } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nContent" })}
|
||||
taskDetailChatFirst
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -1161,6 +1166,7 @@ describe("TaskDetailModal", () => {
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nContent" })}
|
||||
initialTab="logs"
|
||||
taskDetailChatFirst
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -1210,6 +1216,7 @@ describe("TaskDetailModal", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ prompt: "# Hello\n\nContent" })}
|
||||
taskDetailChatFirst
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
|
||||
@@ -358,8 +358,8 @@ describe("TaskDetailModal", () => {
|
||||
expect(container.querySelectorAll(".detail-timestamp-item").length).toBe(2);
|
||||
const tabs = container.querySelectorAll(".detail-tab");
|
||||
expect(Array.from(tabs).map((tab) => tab.textContent?.trim())).toEqual([
|
||||
"Chat",
|
||||
"Activity",
|
||||
"Chat",
|
||||
"Plan",
|
||||
"Changes",
|
||||
"Review",
|
||||
|
||||
@@ -66,7 +66,7 @@ function mockRawLogs(entries: AgentLogEntry[]) {
|
||||
}
|
||||
|
||||
describe("TaskDetailModal Activity and planner Chat tab integration", () => {
|
||||
it("keeps Chat first while Activity segments Live, Feed, and Raw Logs without duplicate panels on desktop", async () => {
|
||||
it("defaults to Activity first while segmenting Live, Feed, and Raw Logs without duplicate panels on desktop", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRawLogs([
|
||||
{ timestamp: "2026-06-30T20:03:00.000Z", taskId: "FN-7315", type: "text", agent: "executor", text: "raw executor line" },
|
||||
@@ -74,15 +74,10 @@ describe("TaskDetailModal Activity and planner Chat tab integration", () => {
|
||||
|
||||
renderModal();
|
||||
|
||||
expect(topLevelTabLabels().slice(0, 2)).toEqual(["Chat", "Activity"]);
|
||||
expect(topLevelTabLabels().slice(0, 2)).toEqual(["Activity", "Chat"]);
|
||||
expect(screen.getAllByRole("button", { name: "Chat" })).toHaveLength(1);
|
||||
expect(screen.getByRole("button", { name: "Chat" })).toHaveClass("detail-tab-active");
|
||||
expect(screen.getByTestId("task-planner-chat-panel")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("tab", { name: "Live" })).not.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Activity" }));
|
||||
|
||||
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
|
||||
expect(screen.queryByTestId("task-planner-chat-panel")).not.toBeInTheDocument();
|
||||
expect(activitySegmentButtons().map((button) => button.textContent?.trim())).toEqual(["Live", "Feed", "Raw Logs"]);
|
||||
expect(activitySegmentButtons().every((button) => (button.textContent ?? "").trim().length > 0)).toBe(true);
|
||||
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
|
||||
@@ -109,6 +104,65 @@ describe("TaskDetailModal Activity and planner Chat tab integration", () => {
|
||||
expect(screen.getByText("raw executor line")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("restores Chat-first ordering and omitted non-done default when the project setting is enabled", () => {
|
||||
mockRawLogs([]);
|
||||
|
||||
renderModal({ taskDetailChatFirst: true });
|
||||
|
||||
expect(topLevelTabLabels().slice(0, 2)).toEqual(["Chat", "Activity"]);
|
||||
expect(screen.getByRole("button", { name: "Chat" })).toHaveClass("detail-tab-active");
|
||||
expect(screen.getByTestId("task-planner-chat-panel")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("tablist", { name: "Activity views" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps explicit Activity, planner Chat, and Logs deep links stable across the ordering setting", () => {
|
||||
mockRawLogs([]);
|
||||
|
||||
const { rerender } = renderModal({ initialTab: "chat", taskDetailChatFirst: true });
|
||||
|
||||
expect(topLevelTabLabels().slice(0, 2)).toEqual(["Chat", "Activity"]);
|
||||
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
|
||||
expect(screen.getByRole("tab", { name: "Live" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.queryByTestId("task-planner-chat-panel")).not.toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-7315", column: "in-progress" as any, log: [], steeringComments: [] })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
initialTab="planner-chat"
|
||||
taskDetailChatFirst={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(topLevelTabLabels().slice(0, 2)).toEqual(["Activity", "Chat"]);
|
||||
expect(screen.getByRole("button", { name: "Chat" })).toHaveClass("detail-tab-active");
|
||||
expect(screen.getByTestId("task-planner-chat-panel")).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-7315", column: "in-progress" as any, log: [{ timestamp: "2026-06-30T20:01:00.000Z", action: "Posted update" }], steeringComments: [] })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
initialTab="logs"
|
||||
taskDetailChatFirst={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(topLevelTabLabels().slice(0, 2)).toEqual(["Chat", "Activity"]);
|
||||
expect(screen.getByRole("button", { name: "Activity" })).toHaveClass("detail-tab-active");
|
||||
expect(screen.getByRole("tab", { name: "Feed" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(screen.getByRole("heading", { name: "Feed" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("preserves Summary as the done-task mobile default while Activity and Chat remain first", async () => {
|
||||
const user = userEvent.setup();
|
||||
const originalInnerWidth = window.innerWidth;
|
||||
@@ -130,7 +184,7 @@ describe("TaskDetailModal Activity and planner Chat tab integration", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(topLevelTabLabels().slice(0, 3)).toEqual(["Chat", "Activity", "Summary"]);
|
||||
expect(topLevelTabLabels().slice(0, 3)).toEqual(["Activity", "Chat", "Summary"]);
|
||||
expect(screen.getByRole("button", { name: "Summary" })).toHaveClass("detail-tab-active");
|
||||
expect(screen.queryByRole("tab", { name: "Live" })).not.toBeInTheDocument();
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ describe("TaskDetailModal planner Chat tab", () => {
|
||||
return render(
|
||||
<TaskDetailModal
|
||||
initialTab={initialTab}
|
||||
taskDetailChatFirst
|
||||
task={makeTask({ column })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
@@ -155,6 +156,7 @@ describe("TaskDetailModal planner Chat tab", () => {
|
||||
const { container, rerender } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-7324-A", column: "todo" as any })}
|
||||
taskDetailChatFirst
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
@@ -171,6 +173,7 @@ describe("TaskDetailModal planner Chat tab", () => {
|
||||
rerender(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-7324-B", column: "todo" as any })}
|
||||
taskDetailChatFirst
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
|
||||
@@ -73,6 +73,7 @@ export function MainContent({
|
||||
autoMerge,
|
||||
mergeStrategy,
|
||||
settingsLoaded,
|
||||
taskDetailChatFirst,
|
||||
skillsEnabled,
|
||||
experimentalFeatures,
|
||||
setQuickChatOpen,
|
||||
@@ -768,6 +769,7 @@ export function MainContent({
|
||||
addToast={addToast}
|
||||
prAuthAvailable={prAuthAvailable}
|
||||
autoMergeEnabled={autoMerge}
|
||||
taskDetailChatFirst={taskDetailChatFirst}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -865,6 +867,7 @@ export function MainContent({
|
||||
lastFetchTimeMs={lastFetchTimeMs}
|
||||
prAuthAvailable={prAuthAvailable}
|
||||
autoMerge={autoMerge}
|
||||
taskDetailChatFirst={taskDetailChatFirst}
|
||||
mergeStrategy={mergeStrategy}
|
||||
onOpenWorkflowEditor={openWorkflowEditorWithNav}
|
||||
onCreateWorkflow={openCreateWorkflowWithNav}
|
||||
|
||||
@@ -113,6 +113,7 @@ export interface MainContentProps {
|
||||
autoMerge: boolean;
|
||||
mergeStrategy: string;
|
||||
settingsLoaded: boolean;
|
||||
taskDetailChatFirst: boolean;
|
||||
skillsEnabled: boolean;
|
||||
experimentalFeatures: Record<string, boolean>;
|
||||
setQuickChatOpen: Dispatch<SetStateAction<boolean>>;
|
||||
|
||||
@@ -51,6 +51,14 @@ export function AppearanceSection({ scopeBanner, form, setForm, themeMode, color
|
||||
</label>
|
||||
<small className="form-text text-muted">{t("settings.appearance.openMobileTasksInPopupHelp", "When enabled, mobile board task-card clicks use the existing task popup instead of the full-screen task panel. Desktop, right-sidebar, and non-board task opens keep their current behavior.")}</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
{/* FNXC:TaskDetailActivityFirst 2026-06-30-23:59: The project setting is opt-in because task details now default to Activity-first; explicit Activity/Chat/Logs links keep their destination regardless of this checkbox. */}
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" checked={form.taskDetailChatFirst === true} onChange={(e) => setForm((f) => ({ ...f, taskDetailChatFirst: e.target.checked }))}/>
|
||||
<span>{t("settings.appearance.taskDetailChatFirst", "Open task details with Chat first")}</span>
|
||||
</label>
|
||||
<small className="form-text text-muted">{t("settings.appearance.taskDetailChatFirstHelp", "Off by default: task details list Activity first and omitted non-done opens land on Activity. Turn on to restore Chat-first order/default; explicit Chat links still work either way.")}</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" checked={sessionBannersHidden} onChange={(e) => setSessionBannersHidden(e.target.checked)}/>
|
||||
|
||||
@@ -21,6 +21,7 @@ function renderAppearanceSection(formOverrides: Partial<Settings> = {}) {
|
||||
autoMerge: true,
|
||||
openTasksInRightSidebar: false,
|
||||
openMobileTasksInPopup: false,
|
||||
taskDetailChatFirst: false,
|
||||
...formOverrides,
|
||||
} as SettingsFormState;
|
||||
const setForm = vi.fn((updater: SettingsFormState | ((previous: SettingsFormState) => SettingsFormState)) => {
|
||||
@@ -79,4 +80,23 @@ describe("AppearanceSection", () => {
|
||||
|
||||
expect(screen.getByLabelText("Open mobile tasks as popups")).toBeChecked();
|
||||
});
|
||||
|
||||
it("renders task detail Chat-first as unchecked by default and updates it", () => {
|
||||
const { setForm, getForm } = renderAppearanceSection();
|
||||
|
||||
const checkbox = screen.getByLabelText("Open task details with Chat first");
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(screen.getByText(/Off by default: task details list Activity first/)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(setForm).toHaveBeenCalledTimes(1);
|
||||
expect(getForm().taskDetailChatFirst).toBe(true);
|
||||
});
|
||||
|
||||
it("reflects a persisted enabled task detail Chat-first value", () => {
|
||||
renderAppearanceSection({ taskDetailChatFirst: true });
|
||||
|
||||
expect(screen.getByLabelText("Open task details with Chat first")).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface RightDockControllerInput {
|
||||
onTaskCreated: (task: Task) => void;
|
||||
prAuthAvailable: boolean;
|
||||
autoMerge: boolean;
|
||||
taskDetailChatFirst: boolean;
|
||||
visibilityOptions: OverflowViewVisibilityOptions;
|
||||
footerVisible: boolean;
|
||||
}
|
||||
@@ -225,6 +226,7 @@ export function useRightDockController(input: RightDockControllerInput): RightDo
|
||||
addToast={input.addToast}
|
||||
prAuthAvailable={input.prAuthAvailable}
|
||||
autoMergeEnabled={input.autoMerge}
|
||||
taskDetailChatFirst={input.taskDetailChatFirst}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface UseAppSettingsResult {
|
||||
capacityRiskTodoThreshold: number;
|
||||
openTasksInRightSidebar: boolean;
|
||||
openMobileTasksInPopup: boolean;
|
||||
taskDetailChatFirst: boolean;
|
||||
quickChatButtonMode: QuickChatButtonMode;
|
||||
quickChatCloseOnOutsideClick: boolean;
|
||||
dismissModalsOnOutsideClick: boolean;
|
||||
@@ -66,6 +67,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
const [capacityRiskTodoThreshold, setCapacityRiskTodoThreshold] = useState(20);
|
||||
const [openTasksInRightSidebar, setOpenTasksInRightSidebar] = useState(false);
|
||||
const [openMobileTasksInPopup, setOpenMobileTasksInPopup] = useState(false);
|
||||
const [taskDetailChatFirst, setTaskDetailChatFirst] = useState(false);
|
||||
const [quickChatButtonMode, setQuickChatButtonMode] = useState<QuickChatButtonMode>("off");
|
||||
const [quickChatCloseOnOutsideClick, setQuickChatCloseOnOutsideClick] = useState(true);
|
||||
const [dismissModalsOnOutsideClick, setDismissModalsOnOutsideClick] = useState(false);
|
||||
@@ -132,6 +134,11 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
setCapacityRiskTodoThreshold(settings.capacityRiskTodoThreshold ?? 20);
|
||||
setOpenTasksInRightSidebar(settings.openTasksInRightSidebar === true);
|
||||
setOpenMobileTasksInPopup(settings.openMobileTasksInPopup === true);
|
||||
/*
|
||||
FNXC:TaskDetailActivityFirst 2026-06-30-23:59:
|
||||
App-level task-detail hosts need the project setting so Activity-first is the missing/false default and Chat-first is restored only by explicit opt-in.
|
||||
*/
|
||||
setTaskDetailChatFirst(settings.taskDetailChatFirst === true);
|
||||
setExperimentalFeatures(settings.experimentalFeatures ?? {});
|
||||
const features = settings.experimentalFeatures ?? {};
|
||||
/*
|
||||
@@ -160,6 +167,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
setDevServerEnabled(false);
|
||||
setOpenTasksInRightSidebar(false);
|
||||
setOpenMobileTasksInPopup(false);
|
||||
setTaskDetailChatFirst(false);
|
||||
setQuickChatCloseOnOutsideClick(true);
|
||||
setDismissModalsOnOutsideClick(false);
|
||||
setTodosEnabled(true);
|
||||
@@ -264,6 +272,7 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
capacityRiskTodoThreshold,
|
||||
openTasksInRightSidebar,
|
||||
openMobileTasksInPopup,
|
||||
taskDetailChatFirst,
|
||||
quickChatButtonMode,
|
||||
quickChatCloseOnOutsideClick,
|
||||
dismissModalsOnOutsideClick,
|
||||
|
||||
Reference in New Issue
Block a user