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
@@ -0,0 +1,113 @@
import type { ChatMessageEntry } from './turns/types';
export 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)
?? (typeof info.role === 'string' ? info.role : null)
?? null;
};
const getMessageParentId = (message: ChatMessageEntry): string | null => {
const info = message.info as unknown as { parentID?: unknown };
return typeof info.parentID === 'string' && info.parentID.length > 0 ? info.parentID : null;
};
export 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);
});
};
export type ShellBridgeDetails = {
command?: string;
output?: string;
status?: string;
};
export 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,
},
};
};
/**
* Finds the shell command a user shell-mode message executed by locating its
* assistant bridge message (single bash tool part parented to the user
* message) among the following entries.
*/
export const findShellCommandForMessage = (
messages: ChatMessageEntry[],
userIndex: number,
): string | null => {
const userMessage = messages[userIndex];
if (!userMessage) return null;
const userId = userMessage.info.id;
for (let index = userIndex + 1; index < messages.length; index += 1) {
const candidate = messages[index];
if (resolveMessageRole(candidate) === 'user') {
break;
}
const { hide, details } = getShellBridgeAssistantDetails(candidate, userId);
if (hide) {
const command = typeof details?.command === 'string' ? details.command.trim() : '';
return command.length > 0 ? command : null;
}
}
return null;
};