gentle-shell-bridge 0.3.0 → 0.3.1

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 (75) hide show
  1. package/LICENSE +202 -0
  2. package/docs/contract.md +1 -1
  3. package/package.json +13 -1
  4. package/src/api.ts +1 -1
  5. package/.codegraph/.gitignore +0 -5
  6. package/PRD.md +0 -144
  7. package/examples/bash-card.ts +0 -1694
  8. package/gentle-shell-bridge-0.3.0.tgz +0 -0
  9. package/odd/tasks/palette-sections.md +0 -30
  10. package/odd/tasks/plugin-keymap.md +0 -32
  11. package/odd/tasks/plugin-surface-contract.md +0 -55
  12. package/test/api.test.ts +0 -395
  13. package/test/apply-on-move.test.ts +0 -477
  14. package/test/arm.test.ts +0 -220
  15. package/test/bridge-ready.test.ts +0 -309
  16. package/test/bus.test.ts +0 -165
  17. package/test/button-rows.test.ts +0 -409
  18. package/test/capture.test.ts +0 -356
  19. package/test/card-style-rows.test.ts +0 -510
  20. package/test/card-style-transform.test.ts +0 -379
  21. package/test/card-style-wiring.test.ts +0 -258
  22. package/test/card-styles-e2e.test.ts +0 -491
  23. package/test/card-styles.test.ts +0 -324
  24. package/test/card-transcript-path.test.ts +0 -178
  25. package/test/config.test.ts +0 -93
  26. package/test/e2e.test.ts +0 -443
  27. package/test/example-bash-identity.test.ts +0 -372
  28. package/test/example-bash-matrix.test.ts +0 -856
  29. package/test/example-designs.test.ts +0 -734
  30. package/test/example-neon-transform.test.ts +0 -283
  31. package/test/example-plain-style.test.ts +0 -1268
  32. package/test/extension.test.ts +0 -160
  33. package/test/fake-events.ts +0 -58
  34. package/test/fake-host.ts +0 -211
  35. package/test/fake-sidebar.ts +0 -516
  36. package/test/fake-transcript.ts +0 -486
  37. package/test/frame-telemetry.test.ts +0 -417
  38. package/test/hook.test.ts +0 -196
  39. package/test/manifest.test.ts +0 -105
  40. package/test/new-category.test.ts +0 -979
  41. package/test/notices.test.ts +0 -328
  42. package/test/plugin-integration-lines.test.ts +0 -979
  43. package/test/plugin-surface.test.ts +0 -152
  44. package/test/presence.test.ts +0 -459
  45. package/test/rail-surfaces.test.ts +0 -457
  46. package/test/rail-width.test.ts +0 -423
  47. package/test/regression.test.ts +0 -483
  48. package/test/repaint-api.test.ts +0 -200
  49. package/test/reserved-categories.test.ts +0 -361
  50. package/test/row-labels.test.ts +0 -284
  51. package/test/row-names.test.ts +0 -473
  52. package/test/row-origin.test.ts +0 -395
  53. package/test/section-interaction.test.ts +0 -364
  54. package/test/section-panel.test.ts +0 -403
  55. package/test/sections-rows.test.ts +0 -291
  56. package/test/sidebar-backend.test.ts +0 -471
  57. package/test/sidebar-e2e.test.ts +0 -331
  58. package/test/sidebar-hover.test.ts +0 -276
  59. package/test/sidebar-independent.test.ts +0 -349
  60. package/test/sidebar-render.test.ts +0 -281
  61. package/test/sidebar-sections.test.ts +0 -245
  62. package/test/status-card-styles.test.ts +0 -201
  63. package/test/status-section-frame.test.ts +0 -181
  64. package/test/status-sections.test.ts +0 -243
  65. package/test/status.test.ts +0 -209
  66. package/test/store.test.ts +0 -192
  67. package/test/surface-repaint-cost.test.ts +0 -520
  68. package/test/tool-design-chain.test.ts +0 -483
  69. package/test/tool-design-cost.test.ts +0 -473
  70. package/test/tool-design-rows.test.ts +0 -359
  71. package/test/tool-designs.test.ts +0 -745
  72. package/test/transform-pipeline.test.ts +0 -465
  73. package/test/transforms-bus.test.ts +0 -233
  74. package/test/verifier-width-regressions.test.ts +0 -164
  75. 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
- }