dsh-blue/blue--packages-transcript ↗★ 1
@dsh-blue/blue-transcript
Blue terminal UI transcript layer: folds session events into transcript items and renders them through dsh-blue-core
安装
此插件尚未提供可验证的 bundle,或兼容性检查未通过。请先阅读仓库说明。 阅读完整 README ↗
说明文档
阅读完整 README ↗@dsh-blue/blue-transcript
English | 中文
Blue terminal UI transcript layer over dsh-blue-core: a pure fold from session events to transcript items (user/assistant/tool), the components that render them, and the Cordis plugin mounting them on blueScreen. The package imports no pi-tui — components either return styled ANSI lines directly or delegate to the blueComponents factory.
The fold
src/fold.ts is a pure, UI-free pipeline. TranscriptFolder.apply(event) folds one SessionEvent (from @deepseek-ai/dsh-session, type-only) and reports the updates it produced — the created or mutated TranscriptItems in mount order; foldSessionEvents(events) is the one-shot form.
user/message→ user item (text blocks joined, images as[image]; image blocks are also kept asImageAttachmentRef[]on the item so the component can lazily load the real bytes through the optionalattachmentsservice — resolved withctx.get, not injected — and render actual images (capped at 12 rows) or keep the[image]placeholder when the service is absent or a load fails). Synthetic user messages —source.kind !== 'user', the harness's ContextFormed injections (runtime-context snapshots, AGENTS.md instructions, plugin notices) — fold to nothing at all: zero presentation, zero placeholder, live and replay alike (the D28 ruling, the S19 rule pulled into the S17 dogfood).assistant/chunktext deltas accumulate into one streaming assistant item per step while reasoning deltas open a sibling streaming thinking item (the S17 kimi split: thinking mounts as its own block above the answer, created only once its accumulated text holds something visible — encrypted or whitespace-only reasoning mounts nothing, the kimi guard); the closingassistant/messagefinalizes both from the authoritative assembled message, creating the finalized pair ahead of the assistant item when the step streamed nothing (replay converges with live mount order, D16).tool/call+tool/resultpair bycallIdinto one tool item: generic presentation with ellipsized arguments and a one-line result summary (a stringmetapresentation payload wins over the model-facing result text); the fold also keeps the unsummarized result text asfullTextfor expansion. An unpaired result still renders — excepttodo_write, whose calls and results fold to nothing because the todo pane owns that presentation (the user's S13 dogfood ruling; kimi keeps the call headline and drops only the body).- Turn/step boundaries, request records, log-only markers, and merge-extended unknown types render nothing — with two exceptions: a
turn/endwhose reason iserrorfolds into an error item rendered as the✗ request failed ()row in error red, so a dead or misrouted endpoint is never silent (the S23 ruling), and one whose reason isabortedorinterruptedfolds into an interrupted item rendered as the single error-red⏹ interruptedrow — the visible tombstone of an Esc-cut stream or a crash-recovery close (the S24a dogfood ruling; the closing unit also settles any still-streaming thinking block into its folded form, so no ghost spinner survives the cut).
Components and mounting
src/components.ts implements BlueComponent: UserMessageComponent (roleUser ❯ gutter), AssistantMessageComponent (Markdown delegated to blueComponents.createMarkdown — pi-tui's Markdown with setText caching, rendered at the content width behind the kimi message chrome: a blank separator row, the text-colored ● bullet on the first line, and two-column MESSAGE_INDENT continuations — the S18 assistant half, pulled into the S17 dogfood by the user's margin ruling; growing text renders bare — the streaming ▌ cursor retired with the S17 third dogfood ruling, kimi has none and the activity pane's composing row is the signal; the step's reasoning is not part of it), and ToolCallComponent (primary ○ running bullet or success/error ●, plus an indented textMuted ⎿ summary; setExpanded switches it between the summary and the unsummarized fullText). src/thinking.ts adds the ThinkingComponent (the kimi ThinkingComponent port, S17): live it renders a blank row, the muted braille spinner with its thinking... label (80 ms, module-level replaceable timers setThinkingTimers, a caller-injected redraw nudge), and the wrapped reasoning's rolling two-line tail; the closing assistant/message finalizes it in place (never a remount) into the muted ● bullet with the full italic body, folded to the first two lines plus a textMuted ... (N more lines, ctrl+o to expand) hint until the shared Ctrl-O expansion opens it — and a finalized block whose authoritative reasoning is blank renders zero rows. THINKING_PREVIEW_LINES = 2 ships beside it, and src/spinners.ts holds the shared frame cycles (braille 80 ms, moon 120 ms — the moon glyph is two cells wide, measured through blueComponents.visibleWidth, never assumed). Text measurement, wrapping, and truncation come from the blueComponents pure functions (visibleWidth / wrapText / truncateToWidth); ellipsize lives in src/fold.ts and is re-exported from the package root.
Render intents
ctx.blueIntents (src/intents.ts, BlueIntentsService) is the render-intent registry: register({ intent, create(props) }) returns an idempotent disposer (a duplicate intent throws BlueIntentsError('DUPLICATE_INTENT')), and resolve(intent) walks exact match → the 'generic' entry → the first registered entry, never throwing for an unknown intent (NO_INTENTS fires only on an empty registry). The mounter resolves tool-item components through it; the built-in 'generic' presenter (the ToolCallComponent above) is registered first in apply, making the generic presentation the plain baseline. Registrations do not retrofit already-mounted items — components resolve at mount time, so a newly registered intent applies to subsequent items. The plugin injects the host 'tools' service, and fold items carry parsedArguments, rawResult (a reconstructed ToolResult), and a view (ToolCallView at call time, replaced by a ToolResultView when the tool's presentResult returns one) resolved through the contained pure resolvers in src/present.ts — unknown tool, missing presenter, or a throwing presenter all degrade to generic. The Ctrl-O collapse toggle is generalized to any intent component exposing setExpanded.
Two intent presenters ship as subpath plugins: ./intent-diff (blue-intent-diff, inject ['blueIntents', 'blueTheme', 'blueComponents']) registers the 'diff' intent — a DiffCardComponent rendering FileDiff pairs through a pure LCS line-diff (src/line-diff.ts), colored with the theme's diff tokens, collapsed at 12 rows per file and 200 expanded; and ./intent-terminal (blue-intent-terminal) registers the 'terminal' intent — a TerminalCardComponent with a description/cwd/$ command header, output rows capped at 10 collapsed and 120 expanded, and exit badges colored via error/warning.
Long-session window and step folding
src/window.ts keeps the newest completed turns mounted (DEFAULT_WINDOW_TURNS = 15; module-level setters exist for tests). After each folded event — snapshot replay and live feed alike — the mounter silently evicts the older turns' items and components. Within a turn, the arrival of the next step/start folds the previous step's tool items into a single step-summary item (a … step N · Tool ×M line); a turn's final step stays expanded (kimi-parity: the visible tail of each turn keeps its tool cards). Step folding is module-toggleable via setStepFoldingEnabled. Mounter bookkeeping keeps item→component entries rather than a flat disposers array, so folding and eviction retire exactly the superseded components. tests/perf.spec.ts folds and mounts a synthetic 200-turn stream: the window keeps roughly 90 components mounted versus 1200 unwindowed, both in the tens of milliseconds (logged, no timing assertions).
The status line is an extension seam, not a fixed component. src/status.ts provides the blueStatus service (register(entry) with a unique id and a priority; duplicate ids throw BlueStatusError with code DUPLICATE_ENTRY) and the FooterShellComponent it feeds; the plugin's apply mounts the shell once via blueScreen.addBottomChild(footer, 'bottom') — the S12 kimi dock order pins the two-row status to the terminal's last rows beneath the editor, so the pull-up dialog panels leave it visible below them (the dock renders bottom children in mount order with the pinned tail last, and the bundle patch pins that order through the blueComponents activation round — sibling patch rows mount concurrently, so row order alone does not order the dock). The shell lays the registered entries over at most two bands — each entry picks its band with row (1, the default, or 2) and its side with align ('left', the default, or 'right'; dishonest values clamp into range). Within one band's left cluster the entries sit in ascending priority (ties keep registration order) joined by a two-space slot gap — the S15 kimi footer identity: no · and no separator color, each entry carries its own greyscale tier (the bundled slots run three: model/context in the full text foreground, cwd/git muted, tips textMuted; D27). A right cluster right-aligns after a minimum two-space gap and starves before the left cluster yields under width pressure. Every entry is offered the width remaining on its cluster and truncates its own text to that budget (an entry wider than a whole row, or one returning '', is skipped without leaving gap residue). Zero rows render when nothing is registered or visible; registrations and disposals nudge a re-render. The entries themselves ship as subpath plugins so the composing bundle lists them as its own patch rows: ./status-basic (blue-status-basic, priority 0: the model name alone in text — the agent-status text retired with S15, a running agent being the activity spinner's job and kimi's footer carrying no status text either — from the live selection ref first — ctx.get('blueSession')?.modelRef?.current.model, so a /model switch shows immediately via 'blue/model-changed' — then session.requestHeader()?.config.model ?? agent.options.model ?? agent.options.provider ?? 'no model' before the app publishes the ref), ./status-cwd (blue-status-cwd, priority 5 in muted: the session's working directory, home-shortened and abbreviated to its last three segments under a leading … — the kimi shortenCwd port — read from the durable session header with a process.cwd() fallback and refreshed on 'blue/session-changed'), ./status-git (blue-status-git, priority 10 in muted: the full kimi badge branch [+a -d ↑e↓f] — diff counts from git diff --numstat HEAD -- over a dirty tree, ahead/behind markers from the porcelain branch header, the bare ± when the numstat probe itself fails on a dirty tree — served from a TTL cache (branch 5 s, status 15 s) probed lazily per render for the session's cwd, hidden outside a repository), ./status-tips (blue-status-tips, priority 30, right-aligned on band 1 in textMuted: the rotating teaching tip, up to two tips joined by ' | ' when the width allows — the rotation spreads the STATUS_TIPS pool by smooth weighted round-robin, the nginx SWRR algorithm as kimi-code, with solo tips never pairing — advanced by an explicit 10 s ticker because Blue's renders are strictly event-driven where kimi leans on unrelated redraws), and ./status-context (blue-status-context, priority 20, right-aligned on band 2 in text: the latest step's context occupancy as context: N% (K/M) when the session's request context advertises a context window — N the occupancy share rounded up, K and M the 1024-base-abbreviated counts — degrading to ctx N / ctx N.Nk without one; from the newest assistant/message usage's inputTokens + cacheReadTokens + cacheWriteTokens).
A downstream contribution is a BlueStatusEntry (src/types.ts): { id, priority, row?, align?, render(width) }, where id is a stable dotted plugin-owned string, priority orders the cluster (the bundled entries take 0/5/10/20/30, leaving room between and after), row picks the band (1 or 2), align picks the side ('left' or 'right'; a right cluster right-aligns and yields first), and render(width) returns one styled line (ANSI allowed) whose visible width stays within the offered budget — or '' to occupy nothing, not even a separator slot, this frame. ctx.blueStatus.register(entry) returns an idempotent disposer; wrap the registration in ctx.effect so unloading the plugin fiber unregisters the entry.
Three dock pane plugins ship as further subpath entries; each mounts a passive bottom child through blueScreen.addBottomChild — gutter-wrapped by the core GutterComponent, the D29 one-column inset — and renders zero rows while it has nothing to show. ./pane-activity (blue-pane-activity, inject ['blueScreen', 'blueTheme', 'blueComponents']) is the S17 kimi mode machine (the resolveActivityPaneMode port) over the attached session's event stream: waiting and tool render the moon spinner with a rotating teaching tip (the S15 footer pool through the same SWRR expansion, a fresh tip on each loading-kind change — kimi working-tips semantics; rc.7 has no step-retry event, so kimi's retry detail is cropped), composing the braille working... row — primary frame, plain label, the tip riding when the width allows (full kimi parity: kimi's assistant block has no cursor, so its pane spinner is the composing signal; the user's second dogfood ruling restored the row after a first round dropped it), thinking an empty pane (the spinner belongs to the transcript's thinking block), idle the kimi Spacer(1) one-row placeholder — always present, so the dock never jumps at the activity edges — and a dialog panel occupying the editor slot ('blue/editor-slot-swapped') hides the pane outright. The phase comes from src/phase.ts's StreamingPhaseTracker — the fold's streaming stage as a standalone pure machine, seeded from the durable snapshot on attach (a resumed mid-stream agent lands in the right phase at once) and fed the live session/event feed filtered to the attached session; the frame timer runs at the style's interval (moon 120 ms, braille 80 ms) only while a spinner state is live, behind the module-level replaceable setActivityTimers, and the rows' width math goes through the live visibleWidth (the moon glyph is two cells wide). ./pane-todo (blue-pane-todo, inject ['blueScreen', 'blueTheme', 'blueKeymap', 'blueComponents']) folds the session's whole-list todo/write snapshots last-write-wins (the durable events snapshot is scanned first on every attach, then the live feed); the fold hides the todo_write tool calls from the stream, so the pane is the list's only surface. Both states sit under the kimi todo frame (S13, the user's dogfood rulings) — a flat full-width ─ rule, a bold primary Todo title, and two-column-indented rows with the kimi three-state markers ✓ success / ● bold primary / ○ dim, completed content muted with strikethrough, no side bars, rounded corners, or bottom border. Long lists fold by the kimi selector: at most five rows show — every in_progress entry first, then the earliest pending and the latest completed filling the rest (one slot reserved for the completed side when both exist) — under a muted … +N more (2 done · 1 pending) · ctrl+t to expand footer counting the hidden entries. The global handler-carrying action blue.todo.toggle (Ctrl-T) switches to the full list with an all N items · ctrl+t to collapse footer; the expansion persists across writes (kimi setTodos semantics) and resets on a session change or a settled list. A list whose every entry completed cl