@oh-my-pi/pi-wire 18.15.0 → 18.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.17.0] - 2026-11-06
6
+
7
+ ### Added
8
+
9
+ - Added shell trace visualization: `ShellTrace` model and TSP `shell` kind for shell runs, node state, and layout.
10
+ - Code views gain score-based line shading, labeled collapsible folds with notes, tones, and hover cards, and an overview strip.
11
+
12
+ ## [18.16.0] - 2026-11-02
13
+
14
+ ### Added
15
+
16
+ - Added the collab envelope helpers `packEnvelope`, `unpackEnvelope` and `rewriteEnvelopePeer`
17
+
5
18
  ## [18.14.2] - 2026-10-31
6
19
 
7
20
  ### Added
@@ -430,6 +430,19 @@ export declare const COLLAB_PROTO = 3;
430
430
  export declare const INTENT_FIELD = "i";
431
431
  /** Plaintext envelope prefix: `[4B uint32 BE peerId][sealed payload]`. */
432
432
  export declare const ENVELOPE_HEADER_LENGTH = 4;
433
+ /**
434
+ * Prefix `sealed` with its envelope header. Host→relay: peerId 0 broadcasts to
435
+ * all guests, peerId N targets guest N. Guest→relay: always 0; the relay
436
+ * rewrites it to the sender's id.
437
+ */
438
+ export declare function packEnvelope(peerId: number, sealed: Uint8Array): Uint8Array<ArrayBuffer>;
439
+ /** Split an envelope into its peerId and sealed payload; `null` when shorter than the header. */
440
+ export declare function unpackEnvelope(data: Uint8Array): {
441
+ peerId: number;
442
+ payload: Uint8Array;
443
+ } | null;
444
+ /** Rewrite the peerId in place without copying the payload. */
445
+ export declare function rewriteEnvelopePeer(data: Uint8Array, peerId: number): void;
433
446
  export declare const ROOM_ID_BYTES = 16;
434
447
  /** AES-256-GCM room key; the seal key for every collab frame. */
435
448
  export declare const ROOM_KEY_BYTES = 32;
@@ -461,5 +474,7 @@ export type RelayControlToGuest = {
461
474
  t: "room-closed";
462
475
  };
463
476
  export type RelayControlMessage = RelayControlToHost | RelayControlToGuest;
477
+ export * from "./shell-trace.js";
478
+ export * from "./shell-viz.js";
464
479
  export * from "./stream.js";
465
480
  export * from "./tsp.js";
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `ShellTrace`: the live model of one embedded-shell run, produced by pi-shell's
3
+ * `RunTrace` (`crates/pi-shell/src/bash/trace.rs`), polled over napi, carried in
4
+ * the bash tool's `details.trace`, and forwarded verbatim to Tern as the `shell`
5
+ * node's props. Every offset is a BYTE offset; UIs convert bytes to characters
6
+ * when the script is not ASCII. Fields at their default are omitted.
7
+ */
8
+ /** NodeKind of the engine's AST, snake_case. `assign` and `word` are not statements. */
9
+ export type ShellTraceNodeKind = "and_or" | "pipeline" | "simple" | "brace" | "subshell" | "if" | "while" | "until" | "for" | "arith_for" | "select" | "case" | "arith" | "cond" | "func_def" | "coproc" | "assign" | "word";
10
+ /** How the engine resolved a simple command's name. */
11
+ export type ShellTraceResolved = "function" | "intrinsic" | "tool" | "native" | "device" | "self_shell" | "external" | "not_found" | "none";
12
+ /** Derived at snapshot time (contract §3); always present. */
13
+ export type ShellTraceState = "pending" | "running" | "ok" | "fail" | "skipped";
14
+ export interface ShellTraceRedir {
15
+ fd: number;
16
+ op: string;
17
+ target: string;
18
+ }
19
+ export interface ShellTraceDiag {
20
+ code: string;
21
+ message: string;
22
+ }
23
+ /** One AST node of a unit; the unit's `nodes` index is the AstNodeId. */
24
+ export interface ShellTraceNode {
25
+ k: ShellTraceNodeKind;
26
+ /** Byte span relative to the unit source. */
27
+ lo: number;
28
+ hi: number;
29
+ /** Parent node id; absent for the root. */
30
+ p?: number;
31
+ state: ShellTraceState;
32
+ /** CmdStart count. */
33
+ runs?: number;
34
+ /** CmdStart − CmdEnd. */
35
+ live?: number;
36
+ /** Status of the last CmdEnd. */
37
+ st?: number;
38
+ /** CmdEnds with a non-zero status. */
39
+ fails?: number;
40
+ /** Sum of `dur_ns` over the ended runs. */
41
+ ns?: number;
42
+ /** `t_ns` of the last CmdStart, relative to RunStart. */
43
+ at?: number;
44
+ /** Last CmdExpanded argv (each arg ≤ 256 bytes, ≤ 64 args). */
45
+ argv?: string[];
46
+ /** The full argv length when `argv` was cut. */
47
+ argc?: number;
48
+ /** Last CmdExpanded assigns. */
49
+ env?: [string, string][];
50
+ res?: ShellTraceResolved;
51
+ redirs?: ShellTraceRedir[];
52
+ /** Pipestatus of the last CmdEnd. */
53
+ pipe?: number[];
54
+ /** Last IterStart n. */
55
+ iter?: number;
56
+ /** LoopEnd iterations. */
57
+ iters?: number;
58
+ /** Last Branch decision. */
59
+ taken?: boolean;
60
+ /** A Background event named this node. */
61
+ bg?: boolean;
62
+ /** Last Spawn pid. */
63
+ pid?: number;
64
+ /** Actual last command completion timestamp and trace revision. */
65
+ end_ns?: number;
66
+ end_seq?: number;
67
+ }
68
+ /** One compiled unit (a top-level command, or an eval/function body). */
69
+ export interface ShellTraceUnit {
70
+ /** Byte offset of the unit inside `script`; absent when the unit is not part of the script. */
71
+ at?: number;
72
+ /** 1-based script line. */
73
+ line: number;
74
+ /** Exact UTF-8 source byte length, including heredocs; absent in older traces. */
75
+ bytes?: number;
76
+ /** The unit's source; present only for unlocated units. */
77
+ source?: string;
78
+ /** Opaque canonical native syntax archive. Only pi-shell may interpret it. */
79
+ syntax?: unknown;
80
+ /** Compiler-known wrapper bytes removed from this unit's displayed source. */
81
+ projection?: number;
82
+ /** Script a callout (`timeout`, `git`, `xargs`, …) ran for its operands; its calling command stands for it. */
83
+ callout?: boolean;
84
+ nodes: ShellTraceNode[];
85
+ }
86
+ /** Exact output/execution identity; repeated visits to one node have distinct seq values. */
87
+ export interface ShellTraceCommandTag {
88
+ /** Actual engine field: descendant vprocs without a root run queue use 0.
89
+ * Do not reject those tags; capture generation and explicit fork events
90
+ * establish run ownership, while vpid/seq distinguish the producer. */
91
+ run: number;
92
+ vpid: number;
93
+ seq: number;
94
+ }
95
+ /** Proven byte fragment of a decoded scalar spanning different producers.
96
+ * The containing Output has no single primary producer; these at-most-four
97
+ * bytes retain the exact contributing executions without guessing ownership. */
98
+ export interface ShellTraceOutputContributor {
99
+ t_ns: number;
100
+ bytes: number[];
101
+ command?: ShellTraceCommandTag;
102
+ vpid?: number;
103
+ u?: number;
104
+ n?: number;
105
+ }
106
+ /** Bounded replay journal entry. Missing u/n explicitly means unknown attribution. */
107
+ export interface ShellTraceHistoryEvent {
108
+ /** Trace revision, including coalesced output growth. */
109
+ seq: number;
110
+ /** Measured monotonic engine/host nanoseconds since RunStart, like node.at. */
111
+ t_ns: number;
112
+ /** Original engine event sequence; absent for Output callbacks. */
113
+ event_seq?: number;
114
+ /** Zero-based actual minimizer segment submission; lifecycle entries remain real engine events. */
115
+ segment?: number;
116
+ /** The last real root RunEnd finalized the entire logical segmented call. */
117
+ logicalEnd?: boolean;
118
+ /** Actual host outcome after output/close draining, without replacing engine status. */
119
+ logicalStatus?: number;
120
+ /** Actual emitting vproc, present on semantic entries even without a command tag. */
121
+ vpid?: number;
122
+ kind: "CmdStart" | "CmdExpanded" | "CmdEnd" | "Pipeline" | "Branch" | "IterStart" | "LoopEnd" | "Background" | "FuncEnter" | "FuncExit" | "Assign" | "RunStart" | "RunEnd" | "Output" | "Diag" | "Exit" | "ErrExit" | "Spawn";
123
+ u?: number;
124
+ n?: number;
125
+ command?: ShellTraceCommandTag;
126
+ status?: number;
127
+ dur_ns?: number;
128
+ pipestatus?: number[];
129
+ argv?: string[];
130
+ assigns?: [string, string][];
131
+ resolved?: ShellTraceResolved;
132
+ redirs?: ShellTraceRedir[];
133
+ name?: string;
134
+ value?: string;
135
+ full_len?: number;
136
+ scope?: string;
137
+ iteration?: number;
138
+ iterations?: number;
139
+ taken?: boolean;
140
+ construct?: string;
141
+ text?: string;
142
+ contributors?: ShellTraceOutputContributor[];
143
+ /** Final visible capture descriptor; merged stderr uses stdout's descriptor. */
144
+ fd?: number;
145
+ pid?: number;
146
+ }
147
+ export interface ShellTrace {
148
+ v: 1;
149
+ /** Semantic/output revision, including assignments/coalesced bytes; monotonic poll key. */
150
+ seq: number;
151
+ /** Root RunEnd status; absent while running. */
152
+ status?: number;
153
+ /** Actual root vproc, even if history trimming removed its RunStart. */
154
+ rootVpid?: number;
155
+ /** Run clock (ns since RunStart) at the last folded event. */
156
+ t_ns: number;
157
+ /** The command text given at construction. */
158
+ script: string;
159
+ /** Compiled units, in announce order. */
160
+ units: ShellTraceUnit[];
161
+ /** Newest ≤20,000 semantic/output records, ≤2 MiB visible output and ≤8 MiB retained payload.
162
+ * Adjacent output from the same execution may coalesce; its seq changes
163
+ * when text grows. Consumers must replace the full journal on revision. */
164
+ history?: ShellTraceHistoryEvent[];
165
+ /** True when any replay record or output byte was evicted. */
166
+ historyTruncated?: boolean;
167
+ /** The last ≤ 8 diagnostics. */
168
+ diags?: ShellTraceDiag[];
169
+ }
170
+ /** Validate an untrusted persisted/native trace, including nested nodes and replay entries.
171
+ * Callers should memoize by input reference; validating a journal is linear in its retained size.
172
+ * This guard never caches mutable input objects as trusted. */
173
+ export declare function isShellTrace(value: unknown): value is ShellTrace;
@@ -0,0 +1,114 @@
1
+ import type { ShellTraceState } from "./shell-trace.js";
2
+ /** Supported shell presentation modes. */
3
+ export type ShellVizView = "raw" | "pseudo";
4
+ /** Independently toggleable, non-executing presentation transformations. */
5
+ export type ShellVizLayer = "color" | "inline" | "idioms" | "effects" | "unwrap";
6
+ /** Every layer is explicit so persisted preferences have deterministic meaning. */
7
+ export interface ShellVizLayers {
8
+ color: boolean;
9
+ inline: boolean;
10
+ idioms: boolean;
11
+ effects: boolean;
12
+ unwrap: boolean;
13
+ }
14
+ /** Per-tool-call preferences. Node identifiers are opaque native `unit:source:node` keys. */
15
+ export interface ShellVizPreferences {
16
+ view: ShellVizView;
17
+ layers: ShellVizLayers;
18
+ split: boolean;
19
+ /** Number of retained history records applied; absent means live. */
20
+ cursor?: number;
21
+ playing: boolean;
22
+ speed: number;
23
+ selectedNode?: string;
24
+ expandedNodes: string[];
25
+ }
26
+ /** Semantic text. All ranges are UTF-16 [lo, hi) in the original outermost source. */
27
+ export interface ShellVizSegment {
28
+ text: string;
29
+ /** True only on an executing leaf span; dim other spans while presentation.running. */
30
+ active?: boolean;
31
+ /** Actual governing leaf outcome, including failures inside otherwise successful pipelines/chains. */
32
+ state?: ShellTraceState;
33
+ /** Actual last leaf CmdEnd clock/sequence; identity is executionNode (or node) + executionSeq. */
34
+ executedAtNs?: number;
35
+ executionSeq?: number;
36
+ /** Space-separated semantic tokens; structural chip/badge tokens survive Color off. */
37
+ style?: string;
38
+ /** Source-token identity used only for selection and source links. */
39
+ node?: string;
40
+ /** Actual command owning state/completion when this token has a different source identity. */
41
+ executionNode?: string;
42
+ range?: [number, number];
43
+ tip?: string;
44
+ /** Complete original text revealed by inspection or expansion. */
45
+ expanded?: string;
46
+ /** Embedded language, with semantic tokenization already performed. */
47
+ language?: string;
48
+ }
49
+ /** Heuristic effect annotation, never a guarantee of safety or completeness. */
50
+ export interface ShellVizEffect {
51
+ kind: "network" | "write" | "destructive" | "secret" | "privilege" | "background";
52
+ glyph: string;
53
+ label: string;
54
+ why: string;
55
+ }
56
+ /** One physical Raw/Pseudo presentation row; structural line breaks are separate rows, not terminal LF text. */
57
+ export interface ShellVizRow {
58
+ id: string;
59
+ depth: number;
60
+ node?: string;
61
+ range?: [number, number];
62
+ /** This row contains an active executable leaf, not merely an enclosing scope. */
63
+ active?: boolean;
64
+ role?: "stmt" | "head" | "mid" | "tail" | "raw" | "document";
65
+ state?: ShellTraceState;
66
+ segments: ShellVizSegment[];
67
+ effects: ShellVizEffect[];
68
+ /** Actual last leaf completion, never copied onto enclosing scope/scaffold rows. */
69
+ executedAtNs?: number;
70
+ executionSeq?: number;
71
+ /** Human-readable runtime metadata, not a machine-readable protocol. */
72
+ meta?: string;
73
+ detail?: string;
74
+ /** Latest real variable values whose last setter is represented by this row; never a detached watch strip. */
75
+ annotations?: ShellVizWatch[];
76
+ }
77
+ /** Latest observed setter of one scoped variable, never a statically guessed runtime value. */
78
+ export interface ShellVizWatch {
79
+ name: string;
80
+ value: string;
81
+ fullLength?: number;
82
+ scope?: string;
83
+ /** Proven scope/process identity; unknown binding identity is not replaced with a guessed function frame. */
84
+ scopeId?: string;
85
+ /** Owning executable/header AST node, promoted from assignment/word event ids. Absent means unlocated. */
86
+ node?: string;
87
+ /** Actual setter span in original outer-script UTF-16 coordinates; absent for unlocated dynamic source. */
88
+ range?: [number, number];
89
+ /** Retained journal seq of the actual last setter, so replay moves or removes the annotation. */
90
+ setterSeq?: number;
91
+ }
92
+ /** Browser-independent presentation shared by ANSI and native GUI renderers. */
93
+ export interface ShellVizPresentation {
94
+ preferences: ShellVizPreferences;
95
+ rows: ShellVizRow[];
96
+ /** Raw counterpart for Pseudo split; empty when that pane is not requested. */
97
+ original: ShellVizRow[];
98
+ /** Actual captured stdout/stderr through the replay cursor, in event order. */
99
+ output: string;
100
+ /** At least one actual executable leaf is running; dim all inactive content. */
101
+ running?: boolean;
102
+ /** Native run has not ended, even when currently between executable leaves. */
103
+ executing: boolean;
104
+ /** Canonical latest observed values; consumers render row.annotations, not a detached strip. */
105
+ watches: ShellVizWatch[];
106
+ timeline: {
107
+ length: number;
108
+ cursor: number;
109
+ elapsedNs: number;
110
+ durationNs: number;
111
+ trimmed?: boolean;
112
+ };
113
+ message?: string;
114
+ }
@@ -17,6 +17,7 @@
17
17
  * Unknown fields and verbs are ignored in both directions, so every addition
18
18
  * here is optional.
19
19
  */
20
+ import type { ShellVizPresentation } from "./shell-viz.js";
20
21
  /** Protocol version this build speaks. */
21
22
  export declare const TSP_VERSION = 1;
22
23
  /** APC identifier: every TSP message body starts with `tsp;`. */
@@ -28,7 +29,7 @@ export declare const TSP_DEFAULT_CREDITS = 2;
28
29
  /** One-letter message verb. */
29
30
  export type TspVerb = "q" | "o" | "f" | "b" | "x" | "r" | "e";
30
31
  /** Every component kind in the v1 vocabulary. */
31
- export declare const TSP_KINDS: readonly ["col", "row", "card", "section", "rule", "spacer", "text", "md", "code", "diff", "ansi", "math", "image", "media", "kv", "table", "tree", "json", "badge", "kbd", "icon", "spinner", "shimmer", "elapsed", "progress", "rate", "list", "item", "tabs", "editor", "input", "button", "status", "seg", "overlay", "toast", "rows", "picker", "prefs", "tool", "checklist", "agent", "chart", "meter", "effort"];
32
+ export declare const TSP_KINDS: readonly ["col", "row", "card", "section", "rule", "spacer", "text", "md", "code", "diff", "ansi", "math", "image", "media", "kv", "table", "tree", "json", "badge", "kbd", "icon", "spinner", "shimmer", "elapsed", "progress", "rate", "list", "item", "tabs", "editor", "input", "button", "status", "seg", "overlay", "toast", "rows", "picker", "prefs", "tool", "checklist", "agent", "chart", "meter", "effort", "shell"];
32
33
  export type TspKind = (typeof TSP_KINDS)[number];
33
34
  /** Semantic colour of a node's chrome. */
34
35
  export type TspTone = "neutral" | "accent" | "info" | "success" | "warning" | "error" | "pending" | "muted" | "user";
@@ -170,6 +171,33 @@ export interface TspMarkdownProps {
170
171
  /** Literal runs drawn as these spans wherever they occur in prose (a sent prompt's chip tokens). */
171
172
  marks?: readonly TspSpan[];
172
173
  }
174
+ /** One marked line of a {@link TspCodeProps.marks}. */
175
+ export interface TspCodeMark {
176
+ line: number;
177
+ tone?: TspTone;
178
+ heat?: number;
179
+ /**
180
+ * Scores in [0, 1] washing the row on the `heat` ramp in place of `heat`'s wash: one flat, several a gradient across
181
+ * the row, an evenly spaced stop per score from left to right. `heat` still sizes the line's overview bar.
182
+ */
183
+ wash?: readonly number[];
184
+ }
185
+ /** A run of lines a {@link TspCodeProps} draws as one row until it is opened. */
186
+ export interface TspCodeFold {
187
+ /** First and last folded line, in the gutter's numbering. */
188
+ from: number;
189
+ to: number;
190
+ /** The folded row's text, in the code's face (default `… [lines {from}-{to}]`, `… [line {from}]` for one line). */
191
+ label?: string;
192
+ /** What the fold hides, after a sparkline of its lines' mark `heat`. */
193
+ note?: TspText;
194
+ /** `error` tints the row; any tone colors the note. */
195
+ tone?: TspTone;
196
+ /** Drawn open; a click on the folded row (or on an open fold's head) toggles it in place. */
197
+ open?: boolean;
198
+ /** A card shown while the pointer rests on the folded row, as the common {@link TspCommonProps.hovercard}. */
199
+ hovercard?: TspPropNode;
200
+ }
173
201
  export interface TspCodeProps {
174
202
  text?: string;
175
203
  /** The language of text that is no file's (`bash`, `json`); wins over `path`. */
@@ -186,13 +214,23 @@ export interface TspCodeProps {
186
214
  */
187
215
  nums?: readonly number[];
188
216
  numbers?: boolean;
189
- marks?: readonly {
190
- line: number;
191
- tone: TspTone;
192
- }[];
217
+ /**
218
+ * Marked lines (`line` is the drawn line number). `heat` in [0, 1] washes the row in a heat ramp, `wash` runs that
219
+ * ramp across it; `tone` draws a bar, and any tone but `accent` also tints an unwashed row. A mark without `heat`
220
+ * or `wash` has tone `accent` by default; one with only those draws no bar.
221
+ */
222
+ marks?: readonly TspCodeMark[];
193
223
  wrap?: boolean;
194
224
  /** Clamp to the first `lines` drawn lines under a "N more lines" button; gives way inside an open disclosure. */
195
225
  preview?: TspPreview;
226
+ /** Runs of lines drawn folded: sorted, not overlapping; marks inside apply once a fold is open. */
227
+ folds?: readonly TspCodeFold[];
228
+ /**
229
+ * An overview strip above the lines: one bar per line (its height the mark's `heat`, its color the mark's tone),
230
+ * a band under it over the lines outside folds, and a bracket over the lines in view. A click or drag on it
231
+ * scrolls the code there.
232
+ */
233
+ overview?: boolean;
196
234
  }
197
235
  export interface TspDiffHunk {
198
236
  oldStart: number;
@@ -930,6 +968,27 @@ export interface TspChartProps {
930
968
  /** Pixel-free size hint: cell size step for heatmaps, height in lines for bars/spark. */
931
969
  size?: "sm" | "md" | "lg";
932
970
  }
971
+ /** Engine-produced shell presentation; hosts render rows and forward controls, never syntax or history. */
972
+ export interface TspShellProps {
973
+ /** Canonical semantic presentation, shared with the ANSI inspector. */
974
+ presentation: ShellVizPresentation;
975
+ /** Show replay controls only in the explicitly opened inspector. */
976
+ inspector?: boolean;
977
+ /** Changed only by explicit seek/reset/live-mode transitions, clearing native highlight trails. */
978
+ replayEpoch?: number;
979
+ /** This mounted call received a real execution-start event, not a settled historical load. */
980
+ liveOrigin?: boolean;
981
+ /** Working directory shown ahead of the script when it differs from the project's. */
982
+ cwd?: string;
983
+ lang?: "bash" | "zsh";
984
+ /**
985
+ * Clamp the command to its first `lines` drawn lines under a "N more lines" button; gives way inside an open
986
+ * disclosure, and never applies in the inspector. The Raw/Pseudo switch and menu stay on the first line.
987
+ */
988
+ preview?: {
989
+ lines: number;
990
+ };
991
+ }
933
992
  /** Kind-specific props, indexed by kind. */
934
993
  export interface TspPropsByKind {
935
994
  col: TspColProps;
@@ -977,6 +1036,7 @@ export interface TspPropsByKind {
977
1036
  chart: TspChartProps;
978
1037
  meter: TspMeterProps;
979
1038
  effort: TspEffortProps;
1039
+ shell: TspShellProps;
980
1040
  }
981
1041
  /**
982
1042
  * Props of a node of kind `K`: its kind-specific props plus the common ones.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-wire",
4
- "version": "18.15.0",
4
+ "version": "18.17.0",
5
5
  "description": "Shared wire protocol types for omp packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -27,7 +27,7 @@
27
27
  "types": "./dist/types/index.d.ts",
28
28
  "scripts": {
29
29
  "check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
30
- "check:types": "tsgo -p tsconfig.json --noEmit",
30
+ "check:types": "bun check --no-script -p tsconfig.json",
31
31
  "lint": "oxlint .",
32
32
  "fix": "oxlint --fix --fix-suggestions . && bun run fmt",
33
33
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
package/src/index.ts CHANGED
@@ -410,6 +410,30 @@ export const INTENT_FIELD = "i";
410
410
  /** Plaintext envelope prefix: `[4B uint32 BE peerId][sealed payload]`. */
411
411
  export const ENVELOPE_HEADER_LENGTH = 4;
412
412
 
413
+ /**
414
+ * Prefix `sealed` with its envelope header. Host→relay: peerId 0 broadcasts to
415
+ * all guests, peerId N targets guest N. Guest→relay: always 0; the relay
416
+ * rewrites it to the sender's id.
417
+ */
418
+ export function packEnvelope(peerId: number, sealed: Uint8Array): Uint8Array<ArrayBuffer> {
419
+ const out = new Uint8Array(ENVELOPE_HEADER_LENGTH + sealed.byteLength);
420
+ new DataView(out.buffer).setUint32(0, peerId, false);
421
+ out.set(sealed, ENVELOPE_HEADER_LENGTH);
422
+ return out;
423
+ }
424
+
425
+ /** Split an envelope into its peerId and sealed payload; `null` when shorter than the header. */
426
+ export function unpackEnvelope(data: Uint8Array): { peerId: number; payload: Uint8Array } | null {
427
+ if (data.byteLength < ENVELOPE_HEADER_LENGTH) return null;
428
+ const peerId = new DataView(data.buffer, data.byteOffset, ENVELOPE_HEADER_LENGTH).getUint32(0, false);
429
+ return { peerId, payload: data.subarray(ENVELOPE_HEADER_LENGTH) };
430
+ }
431
+
432
+ /** Rewrite the peerId in place without copying the payload. */
433
+ export function rewriteEnvelopePeer(data: Uint8Array, peerId: number): void {
434
+ new DataView(data.buffer, data.byteOffset, ENVELOPE_HEADER_LENGTH).setUint32(0, peerId, false);
435
+ }
436
+
413
437
  export const ROOM_ID_BYTES = 16;
414
438
 
415
439
  /** AES-256-GCM room key; the seal key for every collab frame. */
@@ -447,5 +471,7 @@ export type RelayControlToHost = { t: "peer-joined" | "peer-left"; peer: number
447
471
  export type RelayControlToGuest = { t: "room-closed" };
448
472
  export type RelayControlMessage = RelayControlToHost | RelayControlToGuest;
449
473
 
474
+ export * from "./shell-trace";
475
+ export * from "./shell-viz";
450
476
  export * from "./stream";
451
477
  export * from "./tsp";
@@ -0,0 +1,448 @@
1
+ /**
2
+ * `ShellTrace`: the live model of one embedded-shell run, produced by pi-shell's
3
+ * `RunTrace` (`crates/pi-shell/src/bash/trace.rs`), polled over napi, carried in
4
+ * the bash tool's `details.trace`, and forwarded verbatim to Tern as the `shell`
5
+ * node's props. Every offset is a BYTE offset; UIs convert bytes to characters
6
+ * when the script is not ASCII. Fields at their default are omitted.
7
+ */
8
+
9
+ /** NodeKind of the engine's AST, snake_case. `assign` and `word` are not statements. */
10
+ export type ShellTraceNodeKind =
11
+ | "and_or"
12
+ | "pipeline"
13
+ | "simple"
14
+ | "brace"
15
+ | "subshell"
16
+ | "if"
17
+ | "while"
18
+ | "until"
19
+ | "for"
20
+ | "arith_for"
21
+ | "select"
22
+ | "case"
23
+ | "arith"
24
+ | "cond"
25
+ | "func_def"
26
+ | "coproc"
27
+ | "assign"
28
+ | "word";
29
+
30
+ /** How the engine resolved a simple command's name. */
31
+ export type ShellTraceResolved =
32
+ | "function"
33
+ | "intrinsic"
34
+ | "tool"
35
+ | "native"
36
+ | "device"
37
+ | "self_shell"
38
+ | "external"
39
+ | "not_found"
40
+ | "none";
41
+
42
+ /** Derived at snapshot time (contract §3); always present. */
43
+ export type ShellTraceState = "pending" | "running" | "ok" | "fail" | "skipped";
44
+
45
+ export interface ShellTraceRedir {
46
+ fd: number;
47
+ op: string;
48
+ target: string;
49
+ }
50
+
51
+ export interface ShellTraceDiag {
52
+ code: string;
53
+ message: string;
54
+ }
55
+
56
+ /** One AST node of a unit; the unit's `nodes` index is the AstNodeId. */
57
+ export interface ShellTraceNode {
58
+ k: ShellTraceNodeKind;
59
+ /** Byte span relative to the unit source. */
60
+ lo: number;
61
+ hi: number;
62
+ /** Parent node id; absent for the root. */
63
+ p?: number;
64
+ state: ShellTraceState;
65
+ /** CmdStart count. */
66
+ runs?: number;
67
+ /** CmdStart − CmdEnd. */
68
+ live?: number;
69
+ /** Status of the last CmdEnd. */
70
+ st?: number;
71
+ /** CmdEnds with a non-zero status. */
72
+ fails?: number;
73
+ /** Sum of `dur_ns` over the ended runs. */
74
+ ns?: number;
75
+ /** `t_ns` of the last CmdStart, relative to RunStart. */
76
+ at?: number;
77
+ /** Last CmdExpanded argv (each arg ≤ 256 bytes, ≤ 64 args). */
78
+ argv?: string[];
79
+ /** The full argv length when `argv` was cut. */
80
+ argc?: number;
81
+ /** Last CmdExpanded assigns. */
82
+ env?: [string, string][];
83
+ res?: ShellTraceResolved;
84
+ redirs?: ShellTraceRedir[];
85
+ /** Pipestatus of the last CmdEnd. */
86
+ pipe?: number[];
87
+ /** Last IterStart n. */
88
+ iter?: number;
89
+ /** LoopEnd iterations. */
90
+ iters?: number;
91
+ /** Last Branch decision. */
92
+ taken?: boolean;
93
+ /** A Background event named this node. */
94
+ bg?: boolean;
95
+ /** Last Spawn pid. */
96
+ pid?: number;
97
+ /** Actual last command completion timestamp and trace revision. */
98
+ end_ns?: number;
99
+ end_seq?: number;
100
+ }
101
+
102
+ /** One compiled unit (a top-level command, or an eval/function body). */
103
+ export interface ShellTraceUnit {
104
+ /** Byte offset of the unit inside `script`; absent when the unit is not part of the script. */
105
+ at?: number;
106
+ /** 1-based script line. */
107
+ line: number;
108
+ /** Exact UTF-8 source byte length, including heredocs; absent in older traces. */
109
+ bytes?: number;
110
+ /** The unit's source; present only for unlocated units. */
111
+ source?: string;
112
+ /** Opaque canonical native syntax archive. Only pi-shell may interpret it. */
113
+ syntax?: unknown;
114
+ /** Compiler-known wrapper bytes removed from this unit's displayed source. */
115
+ projection?: number;
116
+ /** Script a callout (`timeout`, `git`, `xargs`, …) ran for its operands; its calling command stands for it. */
117
+ callout?: boolean;
118
+ nodes: ShellTraceNode[];
119
+ }
120
+
121
+ /** Exact output/execution identity; repeated visits to one node have distinct seq values. */
122
+ export interface ShellTraceCommandTag {
123
+ /** Actual engine field: descendant vprocs without a root run queue use 0.
124
+ * Do not reject those tags; capture generation and explicit fork events
125
+ * establish run ownership, while vpid/seq distinguish the producer. */
126
+ run: number;
127
+ vpid: number;
128
+ seq: number;
129
+ }
130
+
131
+ /** Proven byte fragment of a decoded scalar spanning different producers.
132
+ * The containing Output has no single primary producer; these at-most-four
133
+ * bytes retain the exact contributing executions without guessing ownership. */
134
+ export interface ShellTraceOutputContributor {
135
+ t_ns: number;
136
+ bytes: number[];
137
+ command?: ShellTraceCommandTag;
138
+ vpid?: number;
139
+ u?: number;
140
+ n?: number;
141
+ }
142
+
143
+ /** Bounded replay journal entry. Missing u/n explicitly means unknown attribution. */
144
+ export interface ShellTraceHistoryEvent {
145
+ /** Trace revision, including coalesced output growth. */
146
+ seq: number;
147
+ /** Measured monotonic engine/host nanoseconds since RunStart, like node.at. */
148
+ t_ns: number;
149
+ /** Original engine event sequence; absent for Output callbacks. */
150
+ event_seq?: number;
151
+ /** Zero-based actual minimizer segment submission; lifecycle entries remain real engine events. */
152
+ segment?: number;
153
+ /** The last real root RunEnd finalized the entire logical segmented call. */
154
+ logicalEnd?: boolean;
155
+ /** Actual host outcome after output/close draining, without replacing engine status. */
156
+ logicalStatus?: number;
157
+ /** Actual emitting vproc, present on semantic entries even without a command tag. */
158
+ vpid?: number;
159
+ kind:
160
+ | "CmdStart"
161
+ | "CmdExpanded"
162
+ | "CmdEnd"
163
+ | "Pipeline"
164
+ | "Branch"
165
+ | "IterStart"
166
+ | "LoopEnd"
167
+ | "Background"
168
+ | "FuncEnter"
169
+ | "FuncExit"
170
+ | "Assign"
171
+ | "RunStart"
172
+ | "RunEnd"
173
+ | "Output"
174
+ | "Diag"
175
+ | "Exit"
176
+ | "ErrExit"
177
+ | "Spawn";
178
+ u?: number;
179
+ n?: number;
180
+ command?: ShellTraceCommandTag;
181
+ status?: number;
182
+ dur_ns?: number;
183
+ pipestatus?: number[];
184
+ argv?: string[];
185
+ assigns?: [string, string][];
186
+ resolved?: ShellTraceResolved;
187
+ redirs?: ShellTraceRedir[];
188
+ name?: string;
189
+ value?: string;
190
+ full_len?: number;
191
+ scope?: string;
192
+ iteration?: number;
193
+ iterations?: number;
194
+ taken?: boolean;
195
+ construct?: string;
196
+ text?: string;
197
+ contributors?: ShellTraceOutputContributor[];
198
+ /** Final visible capture descriptor; merged stderr uses stdout's descriptor. */
199
+ fd?: number;
200
+ pid?: number;
201
+ }
202
+
203
+ export interface ShellTrace {
204
+ v: 1;
205
+ /** Semantic/output revision, including assignments/coalesced bytes; monotonic poll key. */
206
+ seq: number;
207
+ /** Root RunEnd status; absent while running. */
208
+ status?: number;
209
+ /** Actual root vproc, even if history trimming removed its RunStart. */
210
+ rootVpid?: number;
211
+ /** Run clock (ns since RunStart) at the last folded event. */
212
+ t_ns: number;
213
+ /** The command text given at construction. */
214
+ script: string;
215
+ /** Compiled units, in announce order. */
216
+ units: ShellTraceUnit[];
217
+ /** Newest ≤20,000 semantic/output records, ≤2 MiB visible output and ≤8 MiB retained payload.
218
+ * Adjacent output from the same execution may coalesce; its seq changes
219
+ * when text grows. Consumers must replace the full journal on revision. */
220
+ history?: ShellTraceHistoryEvent[];
221
+ /** True when any replay record or output byte was evicted. */
222
+ historyTruncated?: boolean;
223
+ /** The last ≤ 8 diagnostics. */
224
+ diags?: ShellTraceDiag[];
225
+ }
226
+
227
+ function record(value: unknown): value is Record<string, unknown> {
228
+ return typeof value === "object" && value !== null && !Array.isArray(value);
229
+ }
230
+
231
+ function finite(value: unknown): value is number {
232
+ return typeof value === "number" && Number.isFinite(value);
233
+ }
234
+
235
+ function strings(value: unknown): value is string[] {
236
+ return Array.isArray(value) && value.every((item: unknown) => typeof item === "string");
237
+ }
238
+
239
+ function numbers(value: unknown): value is number[] {
240
+ return Array.isArray(value) && value.every(finite);
241
+ }
242
+
243
+ function pairs(value: unknown): value is [string, string][] {
244
+ return (
245
+ Array.isArray(value) &&
246
+ value.every(
247
+ (item: unknown) =>
248
+ Array.isArray(item) && item.length === 2 && typeof item[0] === "string" && typeof item[1] === "string",
249
+ )
250
+ );
251
+ }
252
+
253
+ function optional(value: Record<string, unknown>, key: string, check: (item: unknown) => boolean): boolean {
254
+ return value[key] === undefined || check(value[key]);
255
+ }
256
+
257
+ function string(value: unknown): value is string {
258
+ return typeof value === "string";
259
+ }
260
+
261
+ function boolean(value: unknown): value is boolean {
262
+ return typeof value === "boolean";
263
+ }
264
+
265
+ function resolved(value: unknown): value is ShellTraceResolved {
266
+ return (
267
+ value === "function" ||
268
+ value === "intrinsic" ||
269
+ value === "tool" ||
270
+ value === "native" ||
271
+ value === "device" ||
272
+ value === "self_shell" ||
273
+ value === "external" ||
274
+ value === "not_found" ||
275
+ value === "none"
276
+ );
277
+ }
278
+
279
+ function redirection(value: unknown): value is ShellTraceRedir {
280
+ return record(value) && finite(value.fd) && string(value.op) && string(value.target);
281
+ }
282
+
283
+ function redirections(value: unknown): value is ShellTraceRedir[] {
284
+ return Array.isArray(value) && value.every(redirection);
285
+ }
286
+
287
+ function traceNode(value: unknown): value is ShellTraceNode {
288
+ if (!record(value) || !finite(value.lo) || !finite(value.hi)) return false;
289
+ if (
290
+ !string(value.k) ||
291
+ ![
292
+ "and_or",
293
+ "pipeline",
294
+ "simple",
295
+ "brace",
296
+ "subshell",
297
+ "if",
298
+ "while",
299
+ "until",
300
+ "for",
301
+ "arith_for",
302
+ "select",
303
+ "case",
304
+ "arith",
305
+ "cond",
306
+ "func_def",
307
+ "coproc",
308
+ "assign",
309
+ "word",
310
+ ].includes(value.k)
311
+ )
312
+ return false;
313
+ if (
314
+ value.state !== "pending" &&
315
+ value.state !== "running" &&
316
+ value.state !== "ok" &&
317
+ value.state !== "fail" &&
318
+ value.state !== "skipped"
319
+ )
320
+ return false;
321
+ for (const key of ["p", "runs", "live", "st", "fails", "ns", "at", "argc", "iter", "iters", "pid"]) {
322
+ if (!optional(value, key, finite)) return false;
323
+ }
324
+ return (
325
+ optional(value, "argv", strings) &&
326
+ optional(value, "env", pairs) &&
327
+ optional(value, "res", resolved) &&
328
+ optional(value, "redirs", redirections) &&
329
+ optional(value, "pipe", numbers) &&
330
+ optional(value, "taken", boolean) &&
331
+ optional(value, "bg", boolean)
332
+ );
333
+ }
334
+
335
+ function traceUnit(value: unknown): value is ShellTraceUnit {
336
+ return (
337
+ record(value) &&
338
+ finite(value.line) &&
339
+ optional(value, "at", finite) &&
340
+ optional(value, "bytes", finite) &&
341
+ optional(value, "source", string) &&
342
+ optional(value, "callout", boolean) &&
343
+ Array.isArray(value.nodes) &&
344
+ value.nodes.every(traceNode)
345
+ );
346
+ }
347
+
348
+ function commandTag(value: unknown): value is ShellTraceCommandTag {
349
+ return record(value) && finite(value.run) && finite(value.vpid) && finite(value.seq);
350
+ }
351
+
352
+ function outputContributor(value: unknown): value is ShellTraceOutputContributor {
353
+ return (
354
+ record(value) &&
355
+ finite(value.t_ns) &&
356
+ Array.isArray(value.bytes) &&
357
+ value.bytes.every((byte: unknown) => finite(byte) && Number.isInteger(byte) && byte >= 0 && byte <= 255) &&
358
+ optional(value, "u", finite) &&
359
+ optional(value, "n", finite) &&
360
+ optional(value, "vpid", finite) &&
361
+ optional(value, "command", commandTag)
362
+ );
363
+ }
364
+
365
+ function historyEntry(value: unknown): value is ShellTraceHistoryEvent {
366
+ if (!record(value) || !finite(value.seq) || !finite(value.t_ns)) return false;
367
+ if (
368
+ !string(value.kind) ||
369
+ ![
370
+ "CmdStart",
371
+ "CmdExpanded",
372
+ "CmdEnd",
373
+ "Pipeline",
374
+ "Branch",
375
+ "IterStart",
376
+ "LoopEnd",
377
+ "Background",
378
+ "FuncEnter",
379
+ "FuncExit",
380
+ "Assign",
381
+ "RunStart",
382
+ "RunEnd",
383
+ "Output",
384
+ "Diag",
385
+ "Exit",
386
+ "ErrExit",
387
+ "Spawn",
388
+ ].includes(value.kind)
389
+ )
390
+ return false;
391
+ for (const key of [
392
+ "event_seq",
393
+ "segment",
394
+ "logicalStatus",
395
+ "vpid",
396
+ "u",
397
+ "n",
398
+ "status",
399
+ "dur_ns",
400
+ "full_len",
401
+ "iteration",
402
+ "iterations",
403
+ "fd",
404
+ "pid",
405
+ ]) {
406
+ if (!optional(value, key, finite)) return false;
407
+ }
408
+ for (const key of ["name", "value", "scope", "construct", "text"]) {
409
+ if (!optional(value, key, string)) return false;
410
+ }
411
+ return (
412
+ optional(value, "argv", strings) &&
413
+ optional(value, "assigns", pairs) &&
414
+ optional(value, "redirs", redirections) &&
415
+ optional(value, "resolved", resolved) &&
416
+ optional(value, "pipestatus", numbers) &&
417
+ optional(value, "taken", boolean) &&
418
+ optional(value, "logicalEnd", boolean) &&
419
+ optional(value, "command", commandTag) &&
420
+ optional(value, "contributors", items => Array.isArray(items) && items.every(outputContributor)) &&
421
+ (value.kind !== "Output" || (string(value.text) && finite(value.fd))) &&
422
+ (value.kind !== "Assign" ||
423
+ (string(value.name) && string(value.value) && finite(value.full_len) && string(value.scope)))
424
+ );
425
+ }
426
+
427
+ /** Validate an untrusted persisted/native trace, including nested nodes and replay entries.
428
+ * Callers should memoize by input reference; validating a journal is linear in its retained size.
429
+ * This guard never caches mutable input objects as trusted. */
430
+ export function isShellTrace(value: unknown): value is ShellTrace {
431
+ if (!record(value) || value.v !== 1 || !finite(value.seq) || !finite(value.t_ns) || !string(value.script))
432
+ return false;
433
+ return (
434
+ optional(value, "status", finite) &&
435
+ optional(value, "rootVpid", finite) &&
436
+ Array.isArray(value.units) &&
437
+ value.units.every(traceUnit) &&
438
+ optional(
439
+ value,
440
+ "diags",
441
+ items =>
442
+ Array.isArray(items) &&
443
+ items.every((item: unknown) => record(item) && string(item.code) && string(item.message)),
444
+ ) &&
445
+ optional(value, "history", items => Array.isArray(items) && items.every(historyEntry)) &&
446
+ optional(value, "historyTruncated", boolean)
447
+ );
448
+ }
@@ -0,0 +1,109 @@
1
+ import type { ShellTraceState } from "./shell-trace";
2
+
3
+ /** Supported shell presentation modes. */
4
+ export type ShellVizView = "raw" | "pseudo";
5
+ /** Independently toggleable, non-executing presentation transformations. */
6
+ export type ShellVizLayer = "color" | "inline" | "idioms" | "effects" | "unwrap";
7
+ /** Every layer is explicit so persisted preferences have deterministic meaning. */
8
+ export interface ShellVizLayers {
9
+ color: boolean;
10
+ inline: boolean;
11
+ idioms: boolean;
12
+ effects: boolean;
13
+ unwrap: boolean;
14
+ }
15
+ /** Per-tool-call preferences. Node identifiers are opaque native `unit:source:node` keys. */
16
+ export interface ShellVizPreferences {
17
+ view: ShellVizView;
18
+ layers: ShellVizLayers;
19
+ split: boolean;
20
+ /** Number of retained history records applied; absent means live. */
21
+ cursor?: number;
22
+ playing: boolean;
23
+ speed: number;
24
+ selectedNode?: string;
25
+ expandedNodes: string[];
26
+ }
27
+ /** Semantic text. All ranges are UTF-16 [lo, hi) in the original outermost source. */
28
+ export interface ShellVizSegment {
29
+ text: string;
30
+ /** True only on an executing leaf span; dim other spans while presentation.running. */
31
+ active?: boolean;
32
+ /** Actual governing leaf outcome, including failures inside otherwise successful pipelines/chains. */
33
+ state?: ShellTraceState;
34
+ /** Actual last leaf CmdEnd clock/sequence; identity is executionNode (or node) + executionSeq. */
35
+ executedAtNs?: number;
36
+ executionSeq?: number;
37
+ /** Space-separated semantic tokens; structural chip/badge tokens survive Color off. */
38
+ style?: string;
39
+ /** Source-token identity used only for selection and source links. */
40
+ node?: string;
41
+ /** Actual command owning state/completion when this token has a different source identity. */
42
+ executionNode?: string;
43
+ range?: [number, number];
44
+ tip?: string;
45
+ /** Complete original text revealed by inspection or expansion. */
46
+ expanded?: string;
47
+ /** Embedded language, with semantic tokenization already performed. */
48
+ language?: string;
49
+ }
50
+ /** Heuristic effect annotation, never a guarantee of safety or completeness. */
51
+ export interface ShellVizEffect {
52
+ kind: "network" | "write" | "destructive" | "secret" | "privilege" | "background";
53
+ glyph: string;
54
+ label: string;
55
+ why: string;
56
+ }
57
+ /** One physical Raw/Pseudo presentation row; structural line breaks are separate rows, not terminal LF text. */
58
+ export interface ShellVizRow {
59
+ id: string;
60
+ depth: number;
61
+ node?: string;
62
+ range?: [number, number];
63
+ /** This row contains an active executable leaf, not merely an enclosing scope. */
64
+ active?: boolean;
65
+ role?: "stmt" | "head" | "mid" | "tail" | "raw" | "document";
66
+ state?: ShellTraceState;
67
+ segments: ShellVizSegment[];
68
+ effects: ShellVizEffect[];
69
+ /** Actual last leaf completion, never copied onto enclosing scope/scaffold rows. */
70
+ executedAtNs?: number;
71
+ executionSeq?: number;
72
+ /** Human-readable runtime metadata, not a machine-readable protocol. */
73
+ meta?: string;
74
+ detail?: string;
75
+ /** Latest real variable values whose last setter is represented by this row; never a detached watch strip. */
76
+ annotations?: ShellVizWatch[];
77
+ }
78
+ /** Latest observed setter of one scoped variable, never a statically guessed runtime value. */
79
+ export interface ShellVizWatch {
80
+ name: string;
81
+ value: string;
82
+ fullLength?: number;
83
+ scope?: string;
84
+ /** Proven scope/process identity; unknown binding identity is not replaced with a guessed function frame. */
85
+ scopeId?: string;
86
+ /** Owning executable/header AST node, promoted from assignment/word event ids. Absent means unlocated. */
87
+ node?: string;
88
+ /** Actual setter span in original outer-script UTF-16 coordinates; absent for unlocated dynamic source. */
89
+ range?: [number, number];
90
+ /** Retained journal seq of the actual last setter, so replay moves or removes the annotation. */
91
+ setterSeq?: number;
92
+ }
93
+ /** Browser-independent presentation shared by ANSI and native GUI renderers. */
94
+ export interface ShellVizPresentation {
95
+ preferences: ShellVizPreferences;
96
+ rows: ShellVizRow[];
97
+ /** Raw counterpart for Pseudo split; empty when that pane is not requested. */
98
+ original: ShellVizRow[];
99
+ /** Actual captured stdout/stderr through the replay cursor, in event order. */
100
+ output: string;
101
+ /** At least one actual executable leaf is running; dim all inactive content. */
102
+ running?: boolean;
103
+ /** Native run has not ended, even when currently between executable leaves. */
104
+ executing: boolean;
105
+ /** Canonical latest observed values; consumers render row.annotations, not a detached strip. */
106
+ watches: ShellVizWatch[];
107
+ timeline: { length: number; cursor: number; elapsedNs: number; durationNs: number; trimmed?: boolean };
108
+ message?: string;
109
+ }
package/src/tsp.ts CHANGED
@@ -18,6 +18,8 @@
18
18
  * here is optional.
19
19
  */
20
20
 
21
+ import type { ShellVizPresentation } from "./shell-viz";
22
+
21
23
  /** Protocol version this build speaks. */
22
24
  export const TSP_VERSION = 1;
23
25
  /** APC identifier: every TSP message body starts with `tsp;`. */
@@ -81,6 +83,7 @@ export const TSP_KINDS = [
81
83
  "chart",
82
84
  "meter",
83
85
  "effort",
86
+ "shell",
84
87
  ] as const;
85
88
 
86
89
  export type TspKind = (typeof TSP_KINDS)[number];
@@ -223,6 +226,33 @@ export interface TspMarkdownProps {
223
226
  /** Literal runs drawn as these spans wherever they occur in prose (a sent prompt's chip tokens). */
224
227
  marks?: readonly TspSpan[];
225
228
  }
229
+ /** One marked line of a {@link TspCodeProps.marks}. */
230
+ export interface TspCodeMark {
231
+ line: number;
232
+ tone?: TspTone;
233
+ heat?: number;
234
+ /**
235
+ * Scores in [0, 1] washing the row on the `heat` ramp in place of `heat`'s wash: one flat, several a gradient across
236
+ * the row, an evenly spaced stop per score from left to right. `heat` still sizes the line's overview bar.
237
+ */
238
+ wash?: readonly number[];
239
+ }
240
+ /** A run of lines a {@link TspCodeProps} draws as one row until it is opened. */
241
+ export interface TspCodeFold {
242
+ /** First and last folded line, in the gutter's numbering. */
243
+ from: number;
244
+ to: number;
245
+ /** The folded row's text, in the code's face (default `… [lines {from}-{to}]`, `… [line {from}]` for one line). */
246
+ label?: string;
247
+ /** What the fold hides, after a sparkline of its lines' mark `heat`. */
248
+ note?: TspText;
249
+ /** `error` tints the row; any tone colors the note. */
250
+ tone?: TspTone;
251
+ /** Drawn open; a click on the folded row (or on an open fold's head) toggles it in place. */
252
+ open?: boolean;
253
+ /** A card shown while the pointer rests on the folded row, as the common {@link TspCommonProps.hovercard}. */
254
+ hovercard?: TspPropNode;
255
+ }
226
256
  export interface TspCodeProps {
227
257
  text?: string;
228
258
  /** The language of text that is no file's (`bash`, `json`); wins over `path`. */
@@ -239,10 +269,23 @@ export interface TspCodeProps {
239
269
  */
240
270
  nums?: readonly number[];
241
271
  numbers?: boolean;
242
- marks?: readonly { line: number; tone: TspTone }[];
272
+ /**
273
+ * Marked lines (`line` is the drawn line number). `heat` in [0, 1] washes the row in a heat ramp, `wash` runs that
274
+ * ramp across it; `tone` draws a bar, and any tone but `accent` also tints an unwashed row. A mark without `heat`
275
+ * or `wash` has tone `accent` by default; one with only those draws no bar.
276
+ */
277
+ marks?: readonly TspCodeMark[];
243
278
  wrap?: boolean;
244
279
  /** Clamp to the first `lines` drawn lines under a "N more lines" button; gives way inside an open disclosure. */
245
280
  preview?: TspPreview;
281
+ /** Runs of lines drawn folded: sorted, not overlapping; marks inside apply once a fold is open. */
282
+ folds?: readonly TspCodeFold[];
283
+ /**
284
+ * An overview strip above the lines: one bar per line (its height the mark's `heat`, its color the mark's tone),
285
+ * a band under it over the lines outside folds, and a bracket over the lines in view. A click or drag on it
286
+ * scrolls the code there.
287
+ */
288
+ overview?: boolean;
246
289
  }
247
290
  export interface TspDiffHunk {
248
291
  oldStart: number;
@@ -896,6 +939,26 @@ export interface TspChartProps {
896
939
  size?: "sm" | "md" | "lg";
897
940
  }
898
941
 
942
+ /** Engine-produced shell presentation; hosts render rows and forward controls, never syntax or history. */
943
+ export interface TspShellProps {
944
+ /** Canonical semantic presentation, shared with the ANSI inspector. */
945
+ presentation: ShellVizPresentation;
946
+ /** Show replay controls only in the explicitly opened inspector. */
947
+ inspector?: boolean;
948
+ /** Changed only by explicit seek/reset/live-mode transitions, clearing native highlight trails. */
949
+ replayEpoch?: number;
950
+ /** This mounted call received a real execution-start event, not a settled historical load. */
951
+ liveOrigin?: boolean;
952
+ /** Working directory shown ahead of the script when it differs from the project's. */
953
+ cwd?: string;
954
+ lang?: "bash" | "zsh";
955
+ /**
956
+ * Clamp the command to its first `lines` drawn lines under a "N more lines" button; gives way inside an open
957
+ * disclosure, and never applies in the inspector. The Raw/Pseudo switch and menu stay on the first line.
958
+ */
959
+ preview?: { lines: number };
960
+ }
961
+
899
962
  /** Kind-specific props, indexed by kind. */
900
963
  export interface TspPropsByKind {
901
964
  col: TspColProps;
@@ -943,6 +1006,7 @@ export interface TspPropsByKind {
943
1006
  chart: TspChartProps;
944
1007
  meter: TspMeterProps;
945
1008
  effort: TspEffortProps;
1009
+ shell: TspShellProps;
946
1010
  }
947
1011
 
948
1012
  /**