From 61aae371bd026dbd37492a3a4a06bd5310ab1687 Mon Sep 17 00:00:00 2001 From: scotttong Date: Fri, 31 Jul 2026 09:53:36 -0700 Subject: [PATCH] =?UTF-8?q?feat(ui):=20task-chat=20per-request=20mode=20ch?= =?UTF-8?q?ips=20=E2=80=94=20drop=20the=20header=20mode=20badge,=20tag=20e?= =?UTF-8?q?ach=20agent=20reply=20with=20the=20mode=20its=20request=20ran?= =?UTF-8?q?=20under?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header's Ask/Plan badge read as a task-global setting, but mode is a per-request choice: with the redesign flag ON the badge is gone from the issue header, and each agent reply's avatar row now shows the work mode that was in effect when its run started, reconstructed from the activity feed's workMode change history (issue.updated details + _previous). Flag OFF is unchanged: the legacy header badge stays and the new workModeChanges prop is ignored by IssueChatThread. Co-Authored-By: Claude Fable 5 --- ui/src/components/TaskChatThread.tsx | 33 +++++++++++++------ .../components/task-chat/task-chat-adapter.ts | 8 ++--- ui/src/pages/IssueDetail.tsx | 13 ++++++-- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/ui/src/components/TaskChatThread.tsx b/ui/src/components/TaskChatThread.tsx index 0a41b36d387..e373d9ed886 100644 --- a/ui/src/components/TaskChatThread.tsx +++ b/ui/src/components/TaskChatThread.tsx @@ -18,7 +18,8 @@ import { TaskChatInteractionCard } from "@/components/task-chat/TaskChatInteract import { TaskChatThreadView } from "@/components/task-chat/TaskChatThreadView"; import { TaskChatComposer } from "@/components/task-chat/TaskChatComposer"; import { useIssuePlanDocument } from "@/hooks/useIssuePlanDocument"; -import { latestSameRunHandoffTimestamp } from "@/lib/issue-chat-messages"; +import { latestSameRunHandoffTimestamp, type IssueChatComment } from "@/lib/issue-chat-messages"; +import { workModeInEffectAt } from "@/lib/issue-timeline-events"; import { workModeMetaFor } from "@/lib/work-mode-meta"; function toMs(value: Date | string | null | undefined): number { @@ -85,12 +86,30 @@ export function TaskChatThread(props: TaskChatThreadProps) { onSubmitInteractionVerdicts, externalReferences, threadHeader, + workModeChanges, } = props; - const agentModeLabel = workModeMetaFor(issueWorkMode).label; + const linkedRunMetaById = useMemo(() => { + const map = new Map[number]>(); + for (const run of linkedRuns ?? []) map.set(run.runId, run); + return map; + }, [linkedRuns]); + + // Each agent reply is tagged with the mode its request ran under: the + // issue's work mode at the reply's run start (comment.runId linkage), + // reconstructed from the activity feed's work-mode switch history — not the + // issue's current mode, which the user may have changed since. + const agentModeLabelFor = useCallback( + (comment: IssueChatComment) => { + const runMeta = comment.runId ? linkedRunMetaById.get(comment.runId) : undefined; + const atMs = toMs(runMeta?.startedAt ?? runMeta?.createdAt ?? comment.createdAt); + return workModeMetaFor(workModeInEffectAt(workModeChanges ?? [], atMs, issueWorkMode)).label; + }, + [linkedRunMetaById, workModeChanges, issueWorkMode], + ); const commentItems = useMemo( - () => commentsToTaskChatItems(comments, { agentMap, userLabelMap, currentUserId, agentModeLabel }), - [comments, agentMap, userLabelMap, currentUserId, agentModeLabel], + () => commentsToTaskChatItems(comments, { agentMap, userLabelMap, currentUserId, agentModeLabelFor }), + [comments, agentMap, userLabelMap, currentUserId, agentModeLabelFor], ); // Every run we might need a transcript for (history + live), deduped by id. @@ -162,12 +181,6 @@ export function TaskChatThread(props: TaskChatThreadProps) { return map; }, [comments]); - const linkedRunMetaById = useMemo(() => { - const map = new Map[number]>(); - for (const run of linkedRuns ?? []) map.set(run.runId, run); - return map; - }, [linkedRuns]); - const { data: planDocument } = useIssuePlanDocument(issueId); // Comments, interactions, and the plan-doc marker merged into one diff --git a/ui/src/components/task-chat/task-chat-adapter.ts b/ui/src/components/task-chat/task-chat-adapter.ts index 106cfc634fc..e77036ffd07 100644 --- a/ui/src/components/task-chat/task-chat-adapter.ts +++ b/ui/src/components/task-chat/task-chat-adapter.ts @@ -17,10 +17,10 @@ export interface TaskChatAdapterContext { currentUserId?: string | null; /** * Capitalized mode chip for agent-authored bubbles ("Agent mode" / "Plan - * mode" / "Ask mode") — the issue's work mode at render time (per-comment - * mode attribution is Tier-B, out of scope here). + * mode" / "Ask mode") — resolved per comment, so each reply is tagged with + * the mode its request actually ran under (not the issue's current mode). */ - agentModeLabel?: string; + agentModeLabelFor?: (comment: IssueChatComment) => string | undefined; } function effectiveAgentId(comment: IssueChatComment): string | null { @@ -74,7 +74,7 @@ export function commentsToTaskChatItems( timestamp: formatTimestamp(comment.createdAt), optimistic, agentIcon, - modeLabel: kind === "agent" ? ctx.agentModeLabel : undefined, + modeLabel: kind === "agent" ? ctx.agentModeLabelFor?.(comment) : undefined, }); } return items; diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index ee73e650313..09c82413d85 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -26,7 +26,7 @@ import { useToastActions } from "../context/ToastContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { assigneeValueFromSelection, formatAssigneeUserLabel, formatUserLabel, suggestedCommentAssigneeValue } from "../lib/assignees"; import { buildCompanyUserInlineOptions, buildCompanyUserLabelMap, buildCompanyUserProfileMap, buildMarkdownMentionOptions, isAgentTaskTarget } from "../lib/company-members"; -import { extractIssueTimelineEvents } from "../lib/issue-timeline-events"; +import { extractIssueTimelineEvents, extractIssueWorkModeChanges } from "../lib/issue-timeline-events"; import { queryKeys } from "../lib/queryKeys"; import { keepPreviousDataForSameQueryTail } from "../lib/query-placeholder-data"; import { collectLiveIssueIds } from "../lib/liveIssueIds"; @@ -1176,6 +1176,10 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ () => extractIssueTimelineEvents(resolvedActivity), [resolvedActivity], ); + const workModeChanges = useMemo( + () => extractIssueWorkModeChanges(resolvedActivity), + [resolvedActivity], + ); const loadOlderButton = hasOlderComments ? (
@@ -1214,6 +1218,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ feedbackTermsUrl={feedbackTermsUrl} linkedRuns={timelineRuns} timelineEvents={timelineEvents} + workModeChanges={workModeChanges} liveRuns={resolvedLiveRuns} activeRun={resolvedActiveRun} issueId={issueId} @@ -4197,7 +4202,11 @@ export function IssueDetail() { ) : null} - {issue.workMode === "ask" || issue.workMode === "planning" ? (() => { + {/* Task Chat Redesign: no mode chip in the header — mode is a + per-request choice made in the composer, and each agent reply + carries its own mode chip; a header chip would misread as a + task-global setting. Flag OFF keeps the legacy badge. */} + {!taskChatShellEnabled && (issue.workMode === "ask" || issue.workMode === "planning") ? (() => { const workModeMeta = workModeMetaFor(issue.workMode); const WorkModeIcon = workModeMeta.icon; return (