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:
committed by
GitHub
parent
68f1c1efe3
commit
a1badccddd
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user