feat(ui): task-chat per-request mode chips — drop the header mode badge, tag each agent reply with the mode its request ran under

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 <noreply@anthropic.com>
This commit is contained in:
scotttongandClaude Fable 5 committed 2026-08-07 16:55:49 -07:00
1 parent a96cb271ea
commit 61aae371bd
3 files changed
+38 -16

No files matched your search

+23 -10
View File
@@ -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<string, NonNullable<TaskChatThreadProps["linkedRuns"]>[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<string, NonNullable<TaskChatThreadProps["linkedRuns"]>[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
@@ -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;
+11 -2
View File
@@ -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 ? (
<div className="flex justify-center">
@@ -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() {
</Badge>
) : 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 (