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 (