From c56e9cc32a7af0790af8f4b62315915a86e27c54 Mon Sep 17 00:00:00 2001 From: flamboh Date: Mon, 9 Nov 2026 09:13:24 +0000 Subject: [PATCH] fix(web): clarify file approvals and superseded attempt rows --- apps/web/src/components/chat/ChatComposer.tsx | 1 + .../components/chat/ComposerApprovalPanel.tsx | 25 ++++++++++++---- .../components/chat/MessagesTimeline.logic.ts | 27 ++++++++++++++--- .../src/components/chat/MessagesTimeline.tsx | 2 ++ .../chat/TimelinePendingApprovalRow.tsx | 30 +++++++++++++------ apps/web/src/filePathDisplay.ts | 17 +++++++++++ 6 files changed, 84 insertions(+), 18 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index c94c3d778a3b4..0d7c402f350e4 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -3314,6 +3314,7 @@ export function ChatComposer(props: ChatComposerProps) { !keepsApprovalKeys(document.activeElement); export function ComposerApprovalPanel({ approval, count, + workspaceRoot, disabled, isResponding, onRespond, }: { approval: PendingApproval; count: number; + workspaceRoot?: string | undefined; disabled: boolean; isResponding: boolean; onRespond: (requestId: RuntimeRequestId, decision: ProviderApprovalDecision) => Promise; @@ -50,6 +54,10 @@ export function ComposerApprovalPanel({ const backgroundThread = use(BackgroundThreadContext); const paneFocusedRef = useLayoutPaneFocusedRef(); const showShortcutHints = useSyncExternalStore(subscribeApprovalFocus, approvalKeysAvailable); + const detail = + approval.requestKind === "file-change" + ? formatWorkspaceRelativeDetail(approval.detail, workspaceRoot) + : approval.detail; const [expanded, setExpanded] = useState(true); const [alwaysAllow, setAlwaysAllow] = useState(false); const canRespond = @@ -100,7 +108,7 @@ export function ComposerApprovalPanel({ render={} />} icon={} title={title} - sub={expanded ? null : approval.detail} + sub={expanded ? null : detail} trailing={ <> {count > 1 ? : null} @@ -129,12 +137,19 @@ export function ComposerApprovalPanel({ /> -
-            {approval.detail}
-          
+
+ {detail} +
{approval.files?.map((file) => (
- {file.path} + + {formatWorkspaceRelativePath(file.path, workspaceRoot)} + {file.additions !== undefined ? `+${file.additions}` : null} diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index 5e3f00449f3cd..49dfddf973089 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -529,6 +529,7 @@ const LIVE_ACTIVITY_ROW_ID = "live-activity-row"; export type MessagesTimelineRow = MessagesTimelineRowContent & { readonly continuesWorkLog?: boolean; + readonly superseded?: boolean; /** A user prompt followed directly by another prompt sits in the same prompt stack. */ readonly continuesPromptStack?: boolean; }; @@ -2624,15 +2625,32 @@ export function deriveMessagesTimelineRows(input: { } } + const supersededRowIds = new Set(); + for (const fold of supersededFoldsByAnchorEntryId.values()) { + if (!input.expandedAttemptIds?.has(fold.attemptId)) continue; + for (const entryId of fold.hiddenEntryIds) { + supersededRowIds.add(entryId); + supersededRowIds.add(`work-toggle:${entryId}`); + supersededRowIds.add(`work-live:${entryId}`); + supersededRowIds.add(`${workGroupId(entryId)}:details`); + const entry = timelineEntryById.get(entryId); + if (entry?.kind === "message") { + supersededRowIds.add(`assistant-meta:${entry.message.id}`); + } + } + } const result = attachTrailingToolGroupsToAssistant( attachCreatedThreadSummaries(nextRows, timelineEntries, collapsedFoldMemberIds), ); return withPromptGrouping( - result.map((row, index) => - timelineRowIsWorkLog(row) && timelineRowIsWorkLog(result[index + 1]) + result.map((sourceRow, index) => { + const row = supersededRowIds.has(sourceRow.id) + ? { ...sourceRow, superseded: true } + : sourceRow; + return timelineRowIsWorkLog(row) && timelineRowIsWorkLog(result[index + 1]) ? { ...row, continuesWorkLog: true } - : row, - ), + : row; + }), ); } @@ -3486,6 +3504,7 @@ function withPreviousWindowItems( function isRowUnchanged(a: MessagesTimelineRow, b: MessagesTimelineRow): boolean { if (a.kind !== b.kind || a.id !== b.id) return false; if (a.continuesPromptStack !== b.continuesPromptStack) return false; + if (a.superseded !== b.superseded) return false; switch (a.kind) { case "background-activity": { diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 84b965523ec28..ae17678861084 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1644,6 +1644,7 @@ const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: Time return (
-
{file.path}
+
+ {formatWorkspaceRelativePath(file.path, ctx.workspaceRoot)} +
{diff ? (
           
@@ -94,8 +97,12 @@ export const TimelinePendingApprovalRow = memo(function TimelinePendingApprovalR
 }) {
   const [diffRequestId, setDiffRequestId] = useState(null);
   const diffOpen = diffRequestId === approval.requestId;
-  const { onRespondToApproval } = use(TimelineRowCtx);
+  const { onRespondToApproval, workspaceRoot } = use(TimelineRowCtx);
   const presentation = APPROVAL_PRESENTATION[approval.requestKind];
+  const detail =
+    approval.requestKind === "file-change"
+      ? formatWorkspaceRelativeDetail(approval.detail, workspaceRoot)
+      : approval.detail;
   const canRespond = approval.responseCapability === "live";
   const files = approval.requestKind === "file-change" ? (approval.files ?? []) : [];
   const diffFiles = files.filter((file, index) =>
@@ -119,14 +126,17 @@ export const TimelinePendingApprovalRow = memo(function TimelinePendingApprovalR
         />
       }
       label={
-        
-          {approval.detail || presentation.label}
-        
+          {detail || presentation.label}
+        
} >
@@ -158,13 +168,15 @@ export const TimelinePendingApprovalRow = memo(function TimelinePendingApprovalR )} - }> - {file.path} + } + > + {formatWorkspaceRelativePath(file.path, workspaceRoot)} {file.path} {file.additions !== undefined || file.deletions !== undefined ? ( - + +{file.additions ?? 0}{" "} −{file.deletions ?? 0} diff --git a/apps/web/src/filePathDisplay.ts b/apps/web/src/filePathDisplay.ts index 9d59a83630a5c..0cb16c9656bda 100644 --- a/apps/web/src/filePathDisplay.ts +++ b/apps/web/src/filePathDisplay.ts @@ -18,6 +18,23 @@ function stripRelativePrefixes(path: string): string { return path.replace(/^\.\/+/, "").replace(/^\/+/, ""); } +export function formatWorkspaceRelativeDetail( + detail: string | undefined, + workspaceRoot: string | undefined, +): string | undefined { + if (!detail || !workspaceRoot) return detail; + const root = trimTrailingPathSeparators(workspaceRoot); + const label = fileBasename(normalizePathSeparators(root)); + for (const prefix of new Set([root, normalizePathSeparators(root)])) { + const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + detail = detail.replace( + new RegExp(`(^|[\\s"'\\x60(])${escaped}([/\\\\])`, "g"), + (_match, boundary: string, separator: string) => `${boundary}${label}${separator}`, + ); + } + return detail; +} + export function formatWorkspaceRelativePath( pathWithPosition: string, workspaceRoot: string | undefined,