mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
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:
1 parent
a96cb271ea
commit
61aae371bd
3 files changed
+38
-16
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in new issue
Block a user