feat(chat): prompt navigator list preview, prompt filtering, shell status fix (#2211)

* feat(chat): prompt navigator list preview with prompt filtering

The hover preview is now an interactive scrolling mini-list of prompts:
rows render as bordered two-line cards, the highlighted row stays inside
a center dead zone and the list glides only near the window edges, wheel
steps the highlight, and the panel stays open when the pointer moves into
it so a click can be corrected inside the list.

Rail entries are filtered to real prompts: previews are built from
normalized user display parts (synthetic context stripped), fully
synthetic user messages are excluded, and shell-mode messages show their
extracted command via the shared shell bridge helpers.

* fix(chat): render shell command status transitions

The injected /shell text part carries live state in shellAction, which
the render-relevant part comparator ignored — a running→completed update
reached the store but never re-rendered the message row until the next
send. Compare shellAction command/output/status for text parts.

* fix(sync): stream shell bridge part updates while running

Streaming suspension keeps part updates out of the static message records
while an assistant message streams, relying on the live streaming-tail
path to render it. Shell-mode bridge messages are hidden from the
timeline and rendered inside the user row, so they have no live path —
suspension froze their output chunks and left the card without a Show
output action until the run finished. Exempt shell bridges (single bash
tool part parented to a synthetic shell-marker user message) from
suspension; their updates arrive at command-output pace, not delta pace.

* feat(chat): syntax-highlight shell command card

Render the shell-mode command and its output through the shared
WorkerHighlightedCode (Shiki) with bash grammar, matching the bash tool
part presentation, instead of plain pre blocks.
This commit is contained in:
Bohdan Triapitsyn
2026-07-14 00:59:07 +03:00
committed by GitHub
parent 68f1c1efe3
commit a1badccddd
7 changed files with 434 additions and 97 deletions
@@ -20,6 +20,12 @@ import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
import { useSessionParts } from '@/sync/sync-context';
import { isMobileSurfaceRuntime } from '@/lib/runtimeSurface';
import type { ReviewTransferDirection } from '@/lib/reviewFlow';
import {
USER_SHELL_MARKER,
isUserShellMarkerMessage,
getShellBridgeAssistantDetails,
type ShellBridgeDetails,
} from './lib/shellBridge';
const MESSAGE_LIST_VIRTUALIZE_THRESHOLD = 5;
const EMPTY_STATIC_ENTRY_MESSAGES: ChatMessageEntry[] = [];
@@ -122,8 +128,6 @@ const useStableEvent = <TArgs extends unknown[], TResult>(handler: (...args: TAr
return React.useCallback((...args: TArgs) => handlerRef.current(...args), []);
};
const USER_SHELL_MARKER = 'The following tool was executed by the user';
const resolveMessageRole = (message: ChatMessageEntry): string | null => {
const info = message.info as unknown as { clientRole?: string | null | undefined; role?: string | null | undefined };
return (typeof info.clientRole === 'string' ? info.clientRole : null)
@@ -216,72 +220,6 @@ const isInsideStuckSticky = (node: HTMLElement, container: HTMLElement, containe
return false;
};
const isUserShellMarkerMessage = (message: ChatMessageEntry | undefined): boolean => {
if (!message) return false;
if (resolveMessageRole(message) !== 'user') return false;
return message.parts.some((part) => {
if (part?.type !== 'text') return false;
const text = (part as unknown as { text?: unknown }).text;
const synthetic = (part as unknown as { synthetic?: unknown }).synthetic;
return synthetic === true && typeof text === 'string' && text.trim().startsWith(USER_SHELL_MARKER);
});
};
type ShellBridgeDetails = {
command?: string;
output?: string;
status?: string;
};
const getShellBridgeAssistantDetails = (message: ChatMessageEntry, expectedParentId: string | null): { hide: boolean; details: ShellBridgeDetails | null } => {
if (resolveMessageRole(message) !== 'assistant') {
return { hide: false, details: null };
}
if (expectedParentId && getMessageParentId(message) !== expectedParentId) {
return { hide: false, details: null };
}
if (message.parts.length !== 1) {
return { hide: false, details: null };
}
const part = message.parts[0] as unknown as {
type?: unknown;
tool?: unknown;
state?: {
status?: unknown;
input?: { command?: unknown };
output?: unknown;
metadata?: { output?: unknown };
};
};
if (part?.type !== 'tool') {
return { hide: false, details: null };
}
const toolName = typeof part.tool === 'string' ? part.tool.toLowerCase() : '';
if (toolName !== 'bash') {
return { hide: false, details: null };
}
const command = typeof part.state?.input?.command === 'string' ? part.state.input.command : undefined;
const output =
(typeof part.state?.output === 'string' ? part.state.output : undefined)
?? (typeof part.state?.metadata?.output === 'string' ? part.state.metadata.output : undefined);
const status = typeof part.state?.status === 'string' ? part.state.status : undefined;
return {
hide: true,
details: {
command,
output,
status,
},
};
};
const readTaskSessionId = (toolPart: Part): string | null => {
const partRecord = toolPart as unknown as {