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
+28 -1
View File
@@ -2512,11 +2512,37 @@ export function dropCachedSessionMessageRecordsSnapshots(
}
}
// Shell-mode bridge messages (single bash tool part parented to a synthetic
// shell-marker user message) are hidden from the timeline and rendered inside
// the user row, so they never go through the live streaming-tail path. Their
// part updates (output chunks, running→completed) must not be suspended, or
// the shell card freezes until the next full snapshot rebuild.
const USER_SHELL_MARKER = "The following tool was executed by the user"
const isSuspendExemptShellBridge = (state: State, info: Message, parts: Part[] | undefined): boolean => {
if (!parts || parts.length !== 1) return false
const part = parts[0] as { type?: unknown; tool?: unknown }
if (part?.type !== "tool" || typeof part.tool !== "string" || part.tool.toLowerCase() !== "bash") return false
const parentID = (info as { parentID?: unknown }).parentID
if (typeof parentID !== "string" || parentID.length === 0) return false
const parentParts = state.part[parentID]
if (!parentParts) return false
return parentParts.some((parentPart) => {
if (parentPart?.type !== "text") return false
if ((parentPart as { synthetic?: boolean }).synthetic !== true) return false
const text = (parentPart as { text?: unknown }).text
return typeof text === "string" && text.trim().startsWith(USER_SHELL_MARKER)
})
}
const snapshotPartsMatchState = (snapshot: SessionMessageRecordsSnapshot, state: State): boolean => {
for (const record of snapshot.list) {
if (snapshot.suspendPartUpdates) {
const suspendedID = snapshot.suspendedPartUpdatesMessageID
if (!suspendedID || record.info.id === suspendedID) {
if (
(!suspendedID || record.info.id === suspendedID)
&& !isSuspendExemptShellBridge(state, record.info, state.part[record.info.id])
) {
continue
}
}
@@ -2594,6 +2620,7 @@ export function buildSessionMessageRecordsSnapshot(
const shouldSuspendParts = suspendPartUpdates
&& previousRecord
&& (!suspendedPartUpdatesMessageID || message.id === suspendedPartUpdatesMessageID)
&& !isSuspendExemptShellBridge(state, message, state.part[message.id])
const parts = shouldSuspendParts
? previousRecord.parts
: (state.part[message.id] ?? EMPTY_PARTS)