gentle-shell-bridge 0.3.0 → 0.3.2

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.
Files changed (76) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +91 -722
  3. package/docs/contract.md +92 -35
  4. package/package.json +13 -1
  5. package/src/api.ts +1 -1
  6. package/.codegraph/.gitignore +0 -5
  7. package/PRD.md +0 -144
  8. package/examples/bash-card.ts +0 -1694
  9. package/gentle-shell-bridge-0.3.0.tgz +0 -0
  10. package/odd/tasks/palette-sections.md +0 -30
  11. package/odd/tasks/plugin-keymap.md +0 -32
  12. package/odd/tasks/plugin-surface-contract.md +0 -55
  13. package/test/api.test.ts +0 -395
  14. package/test/apply-on-move.test.ts +0 -477
  15. package/test/arm.test.ts +0 -220
  16. package/test/bridge-ready.test.ts +0 -309
  17. package/test/bus.test.ts +0 -165
  18. package/test/button-rows.test.ts +0 -409
  19. package/test/capture.test.ts +0 -356
  20. package/test/card-style-rows.test.ts +0 -510
  21. package/test/card-style-transform.test.ts +0 -379
  22. package/test/card-style-wiring.test.ts +0 -258
  23. package/test/card-styles-e2e.test.ts +0 -491
  24. package/test/card-styles.test.ts +0 -324
  25. package/test/card-transcript-path.test.ts +0 -178
  26. package/test/config.test.ts +0 -93
  27. package/test/e2e.test.ts +0 -443
  28. package/test/example-bash-identity.test.ts +0 -372
  29. package/test/example-bash-matrix.test.ts +0 -856
  30. package/test/example-designs.test.ts +0 -734
  31. package/test/example-neon-transform.test.ts +0 -283
  32. package/test/example-plain-style.test.ts +0 -1268
  33. package/test/extension.test.ts +0 -160
  34. package/test/fake-events.ts +0 -58
  35. package/test/fake-host.ts +0 -211
  36. package/test/fake-sidebar.ts +0 -516
  37. package/test/fake-transcript.ts +0 -486
  38. package/test/frame-telemetry.test.ts +0 -417
  39. package/test/hook.test.ts +0 -196
  40. package/test/manifest.test.ts +0 -105
  41. package/test/new-category.test.ts +0 -979
  42. package/test/notices.test.ts +0 -328
  43. package/test/plugin-integration-lines.test.ts +0 -979
  44. package/test/plugin-surface.test.ts +0 -152
  45. package/test/presence.test.ts +0 -459
  46. package/test/rail-surfaces.test.ts +0 -457
  47. package/test/rail-width.test.ts +0 -423
  48. package/test/regression.test.ts +0 -483
  49. package/test/repaint-api.test.ts +0 -200
  50. package/test/reserved-categories.test.ts +0 -361
  51. package/test/row-labels.test.ts +0 -284
  52. package/test/row-names.test.ts +0 -473
  53. package/test/row-origin.test.ts +0 -395
  54. package/test/section-interaction.test.ts +0 -364
  55. package/test/section-panel.test.ts +0 -403
  56. package/test/sections-rows.test.ts +0 -291
  57. package/test/sidebar-backend.test.ts +0 -471
  58. package/test/sidebar-e2e.test.ts +0 -331
  59. package/test/sidebar-hover.test.ts +0 -276
  60. package/test/sidebar-independent.test.ts +0 -349
  61. package/test/sidebar-render.test.ts +0 -281
  62. package/test/sidebar-sections.test.ts +0 -245
  63. package/test/status-card-styles.test.ts +0 -201
  64. package/test/status-section-frame.test.ts +0 -181
  65. package/test/status-sections.test.ts +0 -243
  66. package/test/status.test.ts +0 -209
  67. package/test/store.test.ts +0 -192
  68. package/test/surface-repaint-cost.test.ts +0 -520
  69. package/test/tool-design-chain.test.ts +0 -483
  70. package/test/tool-design-cost.test.ts +0 -473
  71. package/test/tool-design-rows.test.ts +0 -359
  72. package/test/tool-designs.test.ts +0 -745
  73. package/test/transform-pipeline.test.ts +0 -465
  74. package/test/transforms-bus.test.ts +0 -233
  75. package/test/verifier-width-regressions.test.ts +0 -164
  76. package/tsconfig.json +0 -18
@@ -1,486 +0,0 @@
1
- /**
2
- * A fake conversation, and a fake `tui` that reaches it the way Pi does (S8, S4).
3
- *
4
- * Two things are modelled here, and nothing else:
5
- *
6
- * 1. **The component whose `render(width)` produces the conversation rows.** Its output is real
7
- * gentle-shell chrome, not an invention: every line below was produced by running
8
- * `gentle-shell/lib/shell-card.ts` (read-only) — `floatRows` / `cardTop` / `cardLine` /
9
- * `cardBottom` — with a theme whose `fg` maps the host's roles to SGR codes
10
- * (`border`→90, `accent`→95, `dim`→2, `muted`→37, `text`→39) and whose `bg` opens a
11
- * 256-colour background and closes with `\x1b[49m`, the shape `surfaceOpener` requires
12
- * (`shell-card.ts:288-303`). Width 40 throughout. The grammar these lines encode is documented
13
- * at the top of `src/card-styles.ts`.
14
- * 2. **The tree Pi mounts, and the property path from `tui` down to that component.** It is copied
15
- * from Pi's own construction, not guessed:
16
- * - `interactive-mode.js:403-407` — the document container's children are
17
- * `[header, loadedResources, chatContainer]`;
18
- * - `chat-viewport.js:4-11,22-25` — the transcript is a `primary` `ScrollView` wrapping that
19
- * document, and the layout root is a `VStack` of `[transcript, dock]`;
20
- * - `interactive-mode.js:623-629` — mounting adds the seven containers with `addChild` (so
21
- * `tui.children[0]` is the document, in both renderer modes) and then calls
22
- * `setLayoutRoot(root)` (fullscreen only);
23
- * - `pi-tui/dist/layout-node.d.ts:2,36-38` and `components/scroll-view.d.ts:57` — both the
24
- * stack and the scroll view expose their parts through `LAYOUT_NODE()`.
25
- *
26
- * The fake keeps those shapes faithfully: `layoutRoot` is absent in regular mode and before
27
- * mounting, `children[0]` is always the document when mounted, and the scroll view's
28
- * `[LAYOUT_NODE]().component` is its child.
29
- */
30
-
31
- /** The layout protocol symbol, exactly as pi-tui defines it (`pi-tui/dist/layout-node.js:1`). */
32
- export const LAYOUT_NODE: symbol = Symbol.for("@earendil-works/pi-tui/layout-node");
33
-
34
- /** Strips SGR sequences, so a test can assert on what the user sees. */
35
- export function stripAnsi(line: string): string {
36
- return line.replace(/\u001b\[[0-9;]*m/g, "");
37
- }
38
-
39
- /**
40
- * One `float` card, width 40: title `read`, hint `ctrl+o`, one body row `config.ts`.
41
- * Rows: padding, header, padding, body, closing rule.
42
- */
43
- export const READ_CARD: readonly string[] = [
44
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
45
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[95m✿ read\u001b[39m\u001b[90m \u001b[39m \u001b[2mctrl+o\u001b[39m \u001b[90m \u001b[39m\u001b[49m ",
46
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
47
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m \u001b[39mconfig.ts\u001b[39m \u001b[90m \u001b[39m\u001b[49m ",
48
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
49
- ];
50
-
51
- // ---------------------------------------------------------------------------------------------
52
- // The bash card MATRIX (S11/T17): 2 host card styles x 2 card contents, plus the shapes a
53
- // result can take. Every string below was PRODUCED, not written: a scratch script imported the
54
- // INSTALLED host's own `lib/shell-card.ts` (`floatRows` / `cardTopRows` / `cardLine` / `cardBottom`
55
- // / `cardRunningLine`, from
56
- // `~/.pi/agent/git/github.com/Gentleman-Programming/gentle-shell/lib/shell-card.ts`) and pi's own
57
- // `@earendil-works/pi-tui` (`Text`, `Container`, `Spacer`) plus
58
- // `dist/modes/interactive/components/visual-truncate.js` (`VisualLinePreview`), with the arguments
59
- // the host's real call sites pass:
60
- //
61
- // minimal `extensions/quiet-tools.ts:910-914` answers bash with `createQuietToolRenderer("bash")`:
62
- // `ToolCardTop.render` (`:778-785`) draws the call (`floatRows(cardTopRows(...))`, plus
63
- // `cardRunningLine` + `cardBottom` while the result is missing) and `ToolCardBody.render`
64
- // (`:670-700`) draws the result (`body`, `bottom`, `afterHeading: true`). The heading is
65
- // `formatToolCall`'s title (`:519-527`): the bare command, because the card glyph is `$`
66
- // (`TOOL_GLYPH`, `:44-52`) — `$ npm test` plus the muted ` (timeout 30s)` and the dim
67
- // `ctrl+o to expand` hint (`:781`). Collapsed minimal bash results are the empty
68
- // `Text` (`:800`) so the result is only the closing rule.
69
- // default the same resolver calls `next()`, so pi's native renderer draws (no gentle-shell card):
70
- // `core/tools/renderers/bash.js:14-20` is `theme.fg("toolTitle", theme.bold("$ …"))` plus
71
- // `theme.fg("muted", " (timeout Ns)")` in a `Text("", 0, 0)`; the card is
72
- // `ToolExecutionComponent`'s `Box(1, 1, bg)` (`modes/interactive/components/
73
- // tool-execution.js:60`), whose `render` (`pi-tui/components/box.js:96-118`) pads every
74
- // child row to `width - 2` and adds one blank row above and below. The result is a
75
- // `Container` of `Spacer(1)` + `VisualLinePreview` (collapsed) or a `Text("\n" + output)`
76
- // (expanded) — so its FIRST row is blank, and an expanded one starts with a blank row too.
77
- // `cardStyle()` is irrelevant here: nothing on this path reads it.
78
- //
79
- // The theme is the fixture's own fake, applied through the host's `CardTheme` interface; its role
80
- // map is the one the host's roles get in this file (`border` 90, `success`/`accent`/`toolTitle` 95,
81
- // `dim` 2, `muted` 37, `text`/`toolOutput` 39) and `bg` opens 256-colour 236 and closes with
82
- // `\x1b[49m`, the shape `surfaceOpener` requires (`shell-card.ts:288-303`). Colour codes are not
83
- // what the design reads — the STRUCTURE (chrome, mark, fill, hint, row order) is, and that is what
84
- // the host code above decides. Widths: 60 for a `float`/`neon` card component, 58 for the native
85
- // cell (the `Box` spends one column of padding on each side), so every row below is exactly that
86
- // wide and a design that changes a row without keeping its width is caught by the tests.
87
- //
88
- // Reachability, with evidence (see `test/example-bash-matrix.test.ts`):
89
- // - `minimal` x `float` and `minimal` x `neon` are both reachable: `cardStyle()` picks the chrome
90
- // inside `floatRows` (`shell-card.ts:314-346`), so the same renderer draws either.
91
- // - `default` x `float` and `default` x `neon` are reachable and draw the SAME rows: with content
92
- // default the host's resolver calls `next()` and nothing on pi's path reads `cardStyle()`. The two
93
- // cells therefore share one fixture; that is the finding, not a shortcut.
94
- // - A style ADDED through `registerCardStyle` is reachable only with `minimal`. The bridge repaints
95
- // float frames only (`src/card-style-transform.ts:189-196` requires a ` ▎ ` row, `:320-352`
96
- // matches the card), and the native cell has no float grammar at all, so a registered style can
97
- // repaint the minimal card and can never repaint the default one.
98
-
99
- /**
100
- * `minimal` x `float`, the CALL component of a settled card: `floatRows` -> padding row + heading.
101
- * `quiet-tools.ts:778-785` produces exactly these two rows once the result marked the shared state
102
- * (`cardAwaitingResult` false, `shell-card.ts:167-181`), which is the state every later frame draws.
103
- */
104
- export const BASH_MINIMAL_FLOAT_CALL: readonly string[] = [
105
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
106
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[95m$ \u001b[95m\u001b[1mnpm test\u001b[22m\u001b[39m\u001b[37m (timeout 30s)\u001b[39m\u001b[39m\u001b[95m \u001b[39m \u001b[2mctrl+o to expand\u001b[39m \u001b[95m \u001b[39m\u001b[49m ",
107
- ];
108
-
109
- /**
110
- * `minimal` x `float`, the CALL component while the command still runs: the same heading, then a
111
- * padding row, the muted `running…` row and the closing rule (`ToolCardTop.render`, `:782-785`).
112
- */
113
- export const BASH_MINIMAL_FLOAT_CALL_RUNNING: readonly string[] = [
114
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
115
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[95m$ \u001b[95m\u001b[1mnpm test\u001b[22m\u001b[39m\u001b[37m (timeout 30s)\u001b[39m\u001b[39m\u001b[95m \u001b[39m \u001b[2mctrl+o to expand\u001b[39m \u001b[95m \u001b[39m\u001b[49m ",
116
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
117
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m \u001b[37mrunning…\u001b[39m \u001b[95m \u001b[39m\u001b[49m ",
118
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
119
- ];
120
-
121
- /**
122
- * `minimal` x `float`, the RESULT component of a collapsed card with no output: the empty `Text`
123
- * (`quiet-tools.ts:800`) leaves `ToolCardBody` nothing to draw but the closing rule.
124
- */
125
- export const BASH_MINIMAL_FLOAT_RESULT_EMPTY: readonly string[] = [
126
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
127
- ];
128
-
129
- /**
130
- * `minimal` x `float`, the RESULT component with two output rows: a separator padding row, the body
131
- * rows in the muted role, then the closing rule (`ToolCardBody.render` + `floatRows`).
132
- */
133
- export const BASH_MINIMAL_FLOAT_RESULT: readonly string[] = [
134
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
135
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m \u001b[37mPASS 42 tests\u001b[39m \u001b[95m \u001b[39m\u001b[49m ",
136
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m \u001b[37mduration 1.2s\u001b[39m \u001b[95m \u001b[39m\u001b[49m ",
137
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
138
- ];
139
-
140
- /**
141
- * `minimal` x `float`, the RESULT component of a command that echoed a prompt: its first body row
142
- * begins with `$ `. From rows alone this row sits exactly where a heading sits, because the call and
143
- * the result share the float chrome. The design tells them apart by `ctx.part` (S12), so this result
144
- * passes through byte for byte (`test/example-bash-matrix.test.ts` pins it).
145
- */
146
- export const BASH_MINIMAL_FLOAT_RESULT_PROMPT: readonly string[] = [
147
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
148
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m \u001b[37m$ echo hi\u001b[39m \u001b[95m \u001b[39m\u001b[49m ",
149
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m \u001b[37mhi\u001b[39m \u001b[95m \u001b[39m\u001b[49m ",
150
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
151
- ];
152
-
153
- /**
154
- * `minimal` x `float`, the CALL component of a settled card whose command is too long for the
155
- * heading row. Derived from the INSTALLED host, not written by hand: `bashSummary(command, width)`
156
- * (`quiet-tools.ts:492-501`) clips the command and appends the host's ellipsis, `clipPlain`
157
- * (`:505-510`) does the cut, and `formatToolCall` (`:519-527`) puts the muted ` (timeout 30s)` after
158
- * it. The width it is handed is the one `renderCall` computes (`:781`): the card's inner width minus
159
- * `CARD_TOP_CHROME` (8, `:489`) and the 18 columns of the `ctrl+o to expand` hint — 32 at width 60 —
160
- * less the timeout's own 14 columns, so the command is clipped to 18 columns. `cardTopRows`
161
- * (`shell-card.ts:246-271`) then wraps that title and `cardTop` (`:141-164`) leaves only its minimum
162
- * fill before the hint. That minimum is why the design's fill rule refuses this row.
163
- */
164
- export const BASH_MINIMAL_FLOAT_CALL_LONG: readonly string[] = [
165
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[49m ",
166
- " \u001b[48;5;236m\u001b[95m▎\u001b[39m\u001b[95m \u001b[39m\u001b[95m$ \u001b[95m\u001b[1mnpm run typecheck…\u001b[22m\u001b[39m\u001b[37m (timeout 30s)\u001b[39m\u001b[39m\u001b[95m \u001b[39m \u001b[2mctrl+o to expand\u001b[39m \u001b[95m \u001b[39m\u001b[49m ",
167
- ];
168
-
169
- /**
170
- * `minimal` x `neon`, the same collapsed long command: the outline spends one column fewer on its
171
- * leading chrome, so `bashSummary` clips two more columns of the command before the ellipsis. Produced
172
- * by the same host path (the only difference is `floatRows`' chrome, `shell-card.ts:105-108`).
173
- */
174
- export const BASH_MINIMAL_NEON_CALL_LONG: readonly string[] = [
175
- "\u001b[95m╭\u001b[39m\u001b[95m─ \u001b[39m\u001b[95m$ \u001b[95m\u001b[1mnpm run typecheck -…\u001b[22m\u001b[39m\u001b[37m (timeout 30s)\u001b[39m\u001b[39m\u001b[95m ─\u001b[39m \u001b[2mctrl+o to expand\u001b[39m \u001b[95m╮\u001b[39m",
176
- ];
177
-
178
- /**
179
- * `minimal` x `neon`, the CALL component of a settled card: one row, the outlined top rule
180
- * (`floatRows` falls back to `OUTLINE_CHROME` when the style is not float, `shell-card.ts:105-108`).
181
- */
182
- export const BASH_MINIMAL_NEON_CALL: readonly string[] = [
183
- "\u001b[95m╭\u001b[39m\u001b[95m─ \u001b[39m\u001b[95m$ \u001b[95m\u001b[1mnpm test\u001b[22m\u001b[39m\u001b[37m (timeout 30s)\u001b[39m\u001b[39m\u001b[95m ─────────────\u001b[39m \u001b[2mctrl+o to expand\u001b[39m \u001b[95m╮\u001b[39m",
184
- ];
185
-
186
- /** `minimal` x `neon`, the CALL component while running: top rule, `running…` row, closing rule. */
187
- export const BASH_MINIMAL_NEON_CALL_RUNNING: readonly string[] = [
188
- "\u001b[95m╭\u001b[39m\u001b[95m─ \u001b[39m\u001b[95m$ \u001b[95m\u001b[1mnpm test\u001b[22m\u001b[39m\u001b[37m (timeout 30s)\u001b[39m\u001b[39m\u001b[95m ─────────────\u001b[39m \u001b[2mctrl+o to expand\u001b[39m \u001b[95m╮\u001b[39m",
189
- "\u001b[95m│\u001b[39m \u001b[37mrunning…\u001b[39m \u001b[95m│\u001b[39m",
190
- "\u001b[95m╰\u001b[39m\u001b[95m──────────────────────────────────────────────────────────╯\u001b[39m",
191
- ];
192
-
193
- /** `minimal` x `neon`, the RESULT component with no output: only the closing rule. */
194
- export const BASH_MINIMAL_NEON_RESULT_EMPTY: readonly string[] = [
195
- "\u001b[95m╰\u001b[39m\u001b[95m──────────────────────────────────────────────────────────╯\u001b[39m",
196
- ];
197
-
198
- /** `minimal` x `neon`, the RESULT component with two output rows and the closing rule. */
199
- export const BASH_MINIMAL_NEON_RESULT: readonly string[] = [
200
- "\u001b[95m│\u001b[39m \u001b[37mPASS 42 tests\u001b[39m \u001b[95m│\u001b[39m",
201
- "\u001b[95m│\u001b[39m \u001b[37mduration 1.2s\u001b[39m \u001b[95m│\u001b[39m",
202
- "\u001b[95m╰\u001b[39m\u001b[95m──────────────────────────────────────────────────────────╯\u001b[39m",
203
- ];
204
-
205
- /** `minimal` x `neon`, the RESULT component of a command that echoed a prompt (`$ echo hi`). */
206
- export const BASH_MINIMAL_NEON_RESULT_PROMPT: readonly string[] = [
207
- "\u001b[95m│\u001b[39m \u001b[37m$ echo hi\u001b[39m \u001b[95m│\u001b[39m",
208
- "\u001b[95m│\u001b[39m \u001b[37mhi\u001b[39m \u001b[95m│\u001b[39m",
209
- "\u001b[95m╰\u001b[39m\u001b[95m──────────────────────────────────────────────────────────╯\u001b[39m",
210
- ];
211
-
212
- /**
213
- * `default` x `float` AND `default` x `neon` (one fixture: see the reachability note above), the
214
- * CALL component: pi's native `Text` row, padded to the `Box`'s 58 content columns.
215
- */
216
- export const BASH_DEFAULT_CALL: readonly string[] = [
217
- "\u001b[95m\u001b[1m$ npm test\u001b[22m\u001b[39m\u001b[37m (timeout 30s)\u001b[39m ",
218
- ];
219
-
220
- /** The same call with no `timeout` argument, so no muted suffix follows the command. */
221
- export const BASH_DEFAULT_CALL_NO_TIMEOUT: readonly string[] = [
222
- "\u001b[95m\u001b[1m$ npm test\u001b[22m\u001b[39m ",
223
- ];
224
-
225
- /**
226
- * The same call with a command long enough to leave no padding after the mark: pi's `Text` wraps it,
227
- * and the first row is exactly 58 columns wide. A design cannot grow this row without breaking the
228
- * frame, so it must leave it alone and say so (`test/example-bash-matrix.test.ts`).
229
- */
230
- export const BASH_DEFAULT_CALL_TIGHT: readonly string[] = [
231
- "\u001b[95m\u001b[1m$ yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\u001b[22m\u001b[39m\u001b[37m (timeout ",
232
- "\u001b[37m30s)\u001b[39m ",
233
- ];
234
-
235
- /** `default`, the RESULT component collapsed: a blank `Spacer` row, then the preview rows. */
236
- export const BASH_DEFAULT_RESULT: readonly string[] = [
237
- "",
238
- "\u001b[39mPASS 42 tests\u001b[39m ",
239
- "\u001b[39mduration 1.2s\u001b[39m ",
240
- ];
241
-
242
- /** `default`, the RESULT component expanded: `Text("\n" + output)` — its first row is blank too. */
243
- export const BASH_DEFAULT_RESULT_EXPANDED: readonly string[] = [
244
- " ",
245
- "\u001b[39mPASS 42 tests\u001b[39m ",
246
- "\u001b[39mduration 1.2s\u001b[39m ",
247
- ];
248
-
249
- /** `default`, the RESULT component of a command whose first output row is `$ echo hi`. */
250
- export const BASH_DEFAULT_RESULT_PROMPT: readonly string[] = [
251
- "",
252
- "\u001b[39m$ echo hi\u001b[39m ",
253
- "\u001b[39mhi\u001b[39m ",
254
- ];
255
-
256
- /**
257
- * A `read` tool call beside it, drawn by the same host code (glyph `≡`, `TOOL_GLYPH.read`). The
258
- * heading is `≡ read src/api.ts`; nothing about it is bash, so a bash design must leave it alone.
259
- */
260
- export const HOST_READ_CARD: readonly string[] = [
261
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
262
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[95m≡ read src/api.ts\u001b[39m\u001b[90m \u001b[39m \u001b[2mctrl+o to expand\u001b[39m \u001b[90m \u001b[39m\u001b[49m ",
263
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
264
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m \u001b[39mexport function bridgeApi() {\u001b[39m \u001b[90m \u001b[39m\u001b[49m ",
265
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
266
- ];
267
-
268
- /**
269
- * One `float` card, width 40, with NO hint and NO body: padding, header, padding. Its opener and
270
- * closer rows are byte-identical to the padded card's, which is why detection must key on
271
- * position rather than on a row's shape alone.
272
- */
273
- export const BASH_CARD: readonly string[] = [
274
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
275
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[95m✿ bash\u001b[39m\u001b[90m \u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
276
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
277
- ];
278
-
279
- /**
280
- * One `float` card whose body row was truncated by `cardLine` (`shell-card.ts:172-184`): the
281
- * `…` arrives with a full reset, and the background is re-armed after it
282
- * (`paintSurfaceRow`, `:300-303`) — the escape sequence real output carries.
283
- */
284
- export const TRUNCATED_CARD: readonly string[] = [
285
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
286
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[95m✿ bash\u001b[39m\u001b[90m \u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
287
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
288
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m \u001b[39mnpm run typecheck exits zero and \u001b[0m\u001b[48;5;236m…\u001b[0m\u001b[48;5;236m \u001b[90m \u001b[39m\u001b[49m ",
289
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
290
- ];
291
-
292
- /**
293
- * The same card in `neon`: no margin, no background, `╭─ … ╮` / `│ … │` / `╰ … ╯`. Detection must
294
- * never mistake one style for the other, so both are fixtures.
295
- */
296
- export const NEON_CARD: readonly string[] = [
297
- "\u001b[90m╭\u001b[39m\u001b[90m─ \u001b[39m\u001b[95m✿ read\u001b[39m\u001b[90m ─────────────────────\u001b[39m \u001b[2mctrl+o\u001b[39m \u001b[90m╮\u001b[39m",
298
- "\u001b[90m│\u001b[39m \u001b[39mconfig.ts\u001b[39m \u001b[90m│\u001b[39m",
299
- "\u001b[90m╰\u001b[39m\u001b[90m──────────────────────────────────────╯\u001b[39m",
300
- ];
301
-
302
- /**
303
- * A `float` card with a SUBTITLE and no hint, width 40: the header carries two text runs (title in
304
- * the title role, `· config.ts` in the muted role) and no dim hint. It pins the honest limit of
305
- * extraction: a heading whose last run is a subtitle must not be read as a hint.
306
- */
307
- export const SUBTITLE_CARD: readonly string[] = [
308
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
309
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[95m✿ read\u001b[39m \u001b[37m·\u001b[39m \u001b[37mconfig.ts\u001b[39m\u001b[90m \u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
310
- " \u001b[48;5;236m\u001b[90m▎\u001b[39m\u001b[90m \u001b[39m\u001b[49m ",
311
- ];
312
-
313
- /** A line the host did not draw as a card, plus a blank one: a repaint must leave both alone. */
314
- export const PROSE_LINE = "I will read the config first.";
315
- export const BLANK_LINE = "";
316
-
317
- /** What the conversation component renders: prose, a card, more prose, a second card. */
318
- export const CONVERSATION: readonly string[] = [
319
- PROSE_LINE,
320
- BLANK_LINE,
321
- ...READ_CARD,
322
- BLANK_LINE,
323
- ...TRUNCATED_CARD,
324
- BLANK_LINE,
325
- ...BASH_CARD,
326
- ];
327
-
328
- /** A component, in Pi's sense: `render(width)` returns one string per row. */
329
- export interface FakeComponent {
330
- render(width: number): string[];
331
- }
332
-
333
- /** The card component: records every width it was asked to render at. */
334
- export interface FakeConversation extends FakeComponent {
335
- readonly widths: number[];
336
- }
337
-
338
- export function createFakeConversation(lines: readonly string[] = CONVERSATION): FakeConversation {
339
- const widths: number[] = [];
340
- return {
341
- widths,
342
- render(width: number): string[] {
343
- widths.push(width);
344
- return [...lines];
345
- },
346
- };
347
- }
348
-
349
- /** A component that paints one fixed row, for the header and the loaded-resources slots. */
350
- function fixedRow(text: string): FakeComponent {
351
- return { render: () => [text] };
352
- }
353
-
354
- /** What `LAYOUT_NODE()` returns for a stack / scroll view, narrowed to what a walk reads. */
355
- export interface FakeLayoutNode {
356
- type: string;
357
- entries?: ReadonlyArray<{ component?: unknown }>;
358
- component?: unknown;
359
- }
360
-
361
- /** Anything exposing the layout protocol, as `ScrollView` and `VStack` both do. */
362
- export function layoutNodeOf(value: unknown): FakeLayoutNode | undefined {
363
- if (value === null || typeof value !== "object") return undefined;
364
- const read = (value as Record<symbol, unknown>)[LAYOUT_NODE];
365
- if (typeof read !== "function") return undefined;
366
- const node: unknown = read.call(value);
367
- if (node === null || typeof node !== "object") return undefined;
368
- return node as FakeLayoutNode;
369
- }
370
-
371
- /** The transcript `ScrollView`, shaped like `pi-tui`'s: a scrollbar, a child, the layout protocol. */
372
- export interface FakeScrollView {
373
- readonly child: unknown;
374
- scrollbar: "auto" | "hidden" | "always";
375
- setScrollbar(value: "auto" | "hidden" | "always"): void;
376
- getContentWidth(width: number): number;
377
- render(width: number): string[];
378
- [LAYOUT_NODE](): FakeLayoutNode;
379
- }
380
-
381
- export interface FakeDocument extends FakeComponent {
382
- children: FakeComponent[];
383
- }
384
-
385
- export interface FakeTui {
386
- mode: "fullscreen" | "regular";
387
- terminal: Record<string, unknown>;
388
- children: FakeComponent[];
389
- /** Absent in regular mode and before mounting, exactly as on Pi's own renderer. */
390
- layoutRoot?: { [LAYOUT_NODE](): FakeLayoutNode };
391
- render(width: number): string[];
392
- document: FakeDocument;
393
- transcript?: FakeScrollView;
394
- chat: FakeConversation;
395
- }
396
-
397
- export interface FakeTranscriptTuiOptions {
398
- /** `regular` models `TuiMainScreen`: children only, no layout root. */
399
- mode?: "fullscreen" | "regular";
400
- /** `false` models the window before `mountInteractiveTui` runs: nothing mounted at all. */
401
- mounted?: boolean;
402
- lines?: readonly string[];
403
- }
404
-
405
- /**
406
- * The fake `tui`. `layoutRoot` (fullscreen, mounted) and `children[0]` (mounted, either mode) both
407
- * lead to `chat`, so a walk can be written against the documented path and proven here — and the
408
- * absent shapes are just as reachable.
409
- */
410
- export function createFakeTranscriptTui(options: FakeTranscriptTuiOptions = {}): FakeTui {
411
- const mode = options.mode ?? "fullscreen";
412
- const mounted = options.mounted ?? true;
413
- const chat = createFakeConversation(options.lines ?? CONVERSATION);
414
- const header = fixedRow("header");
415
- const loadedResources = fixedRow("resources");
416
-
417
- // interactive-mode.js:403-407 — the document container's children, in that order.
418
- const document: FakeDocument = {
419
- children: [header, loadedResources, chat],
420
- render(width: number): string[] {
421
- return this.children.flatMap((child) => child.render(width));
422
- },
423
- };
424
-
425
- // chat-viewport.js:4-11 — a primary ScrollView over the document; its render adds the scrollbar
426
- // column when `getContentWidth` steals one (pi-tui/components/scroll-view.js:186-190).
427
- const transcript: FakeScrollView = {
428
- child: document,
429
- scrollbar: "auto",
430
- setScrollbar(value) {
431
- this.scrollbar = value;
432
- },
433
- getContentWidth(width: number): number {
434
- return this.scrollbar === "hidden" ? width : Math.max(1, width - 1);
435
- },
436
- render(width: number): string[] {
437
- const contentWidth = this.getContentWidth(width);
438
- const lines = (this.child as FakeComponent).render(contentWidth);
439
- return contentWidth === width ? lines : lines.map((line) => `${line} `);
440
- },
441
- [LAYOUT_NODE](): FakeLayoutNode {
442
- return { type: "scroll", component: this.child };
443
- },
444
- };
445
-
446
- const dock = {
447
- [LAYOUT_NODE](): FakeLayoutNode {
448
- return { type: "vstack", entries: [{ component: fixedRow("editor") }] };
449
- },
450
- };
451
-
452
- const layoutRoot = {
453
- [LAYOUT_NODE](): FakeLayoutNode {
454
- return {
455
- type: "vstack",
456
- entries: [{ component: transcript }, { component: dock }],
457
- };
458
- },
459
- };
460
-
461
- const tui: FakeTui = {
462
- mode,
463
- terminal: {},
464
- // interactive-mode.js:622-625 — the seven containers, document first, in both modes.
465
- children: mounted
466
- ? [document, fixedRow("pending"), fixedRow("status"), fixedRow("above"), fixedRow("editor"), fixedRow("below"), fixedRow("footer")]
467
- : [],
468
- render(width: number): string[] {
469
- // TuiAltScreen renders the layout root and ignores `children` (tui-alt-screen.js:145-151).
470
- if (this.layoutRoot !== undefined) {
471
- return (this.layoutRoot[LAYOUT_NODE]().entries ?? []).flatMap((entry) =>
472
- (entry.component as FakeComponent | undefined)?.render(width) ?? [],
473
- );
474
- }
475
- return this.children.flatMap((child) => child.render(width));
476
- },
477
- document,
478
- chat,
479
- };
480
- // Only the fullscreen renderer has a layout root, and only once mounted (interactive-mode.js:626).
481
- if (mounted && mode === "fullscreen") {
482
- tui.layoutRoot = layoutRoot;
483
- tui.transcript = transcript;
484
- }
485
- return tui;
486
- }