@sigrun/tui 0.4.1 → 0.5.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/dist/actions.d.ts +119 -0
  3. package/dist/actions.js +12 -0
  4. package/dist/api.d.ts +209 -0
  5. package/dist/api.js +282 -0
  6. package/dist/blocks/braille.d.ts +41 -0
  7. package/dist/blocks/braille.js +131 -0
  8. package/dist/blocks/chart.d.ts +20 -0
  9. package/dist/blocks/chart.js +397 -0
  10. package/dist/blocks/chat-transport.d.ts +44 -0
  11. package/dist/blocks/chat-transport.js +116 -0
  12. package/dist/blocks/collection/calendar.d.ts +28 -0
  13. package/dist/blocks/collection/calendar.js +232 -0
  14. package/dist/blocks/collection/card.d.ts +36 -0
  15. package/dist/blocks/collection/card.js +128 -0
  16. package/dist/blocks/collection/collection.d.ts +9 -0
  17. package/dist/blocks/collection/collection.js +26 -0
  18. package/dist/blocks/collection/grid.d.ts +18 -0
  19. package/dist/blocks/collection/grid.js +72 -0
  20. package/dist/blocks/collection/lanes.d.ts +32 -0
  21. package/dist/blocks/collection/lanes.js +320 -0
  22. package/dist/blocks/collection/list.d.ts +30 -0
  23. package/dist/blocks/collection/list.js +273 -0
  24. package/dist/blocks/collection/move.d.ts +67 -0
  25. package/dist/blocks/collection/move.js +96 -0
  26. package/dist/blocks/collection/shared.d.ts +87 -0
  27. package/dist/blocks/collection/shared.js +177 -0
  28. package/dist/blocks/collection/table.d.ts +26 -0
  29. package/dist/blocks/collection/table.js +221 -0
  30. package/dist/blocks/content.d.ts +48 -0
  31. package/dist/blocks/content.js +253 -0
  32. package/dist/blocks/context.d.ts +117 -0
  33. package/dist/blocks/context.js +65 -0
  34. package/dist/blocks/diff.d.ts +21 -0
  35. package/dist/blocks/diff.js +25 -0
  36. package/dist/blocks/ext-transport.d.ts +70 -0
  37. package/dist/blocks/ext-transport.js +165 -0
  38. package/dist/blocks/form/choice.d.ts +54 -0
  39. package/dist/blocks/form/choice.js +175 -0
  40. package/dist/blocks/form/fields.d.ts +56 -0
  41. package/dist/blocks/form/fields.js +136 -0
  42. package/dist/blocks/form/file.d.ts +61 -0
  43. package/dist/blocks/form/file.js +121 -0
  44. package/dist/blocks/form/form.d.ts +66 -0
  45. package/dist/blocks/form/form.js +764 -0
  46. package/dist/blocks/form/list.d.ts +37 -0
  47. package/dist/blocks/form/list.js +70 -0
  48. package/dist/blocks/form/values.d.ts +20 -0
  49. package/dist/blocks/form/values.js +106 -0
  50. package/dist/blocks/icons.d.ts +14 -0
  51. package/dist/blocks/icons.js +74 -0
  52. package/dist/blocks/image.d.ts +39 -0
  53. package/dist/blocks/image.js +82 -0
  54. package/dist/blocks/interaction-transport.d.ts +42 -0
  55. package/dist/blocks/interaction-transport.js +79 -0
  56. package/dist/blocks/layout.d.ts +31 -0
  57. package/dist/blocks/layout.js +155 -0
  58. package/dist/blocks/log.d.ts +24 -0
  59. package/dist/blocks/log.js +111 -0
  60. package/dist/blocks/meter.d.ts +16 -0
  61. package/dist/blocks/meter.js +50 -0
  62. package/dist/blocks/page.d.ts +54 -0
  63. package/dist/blocks/page.js +360 -0
  64. package/dist/blocks/render.d.ts +31 -0
  65. package/dist/blocks/render.js +163 -0
  66. package/dist/blocks/surface-view.d.ts +41 -0
  67. package/dist/blocks/surface-view.js +137 -0
  68. package/dist/blocks/surface.d.ts +80 -0
  69. package/dist/blocks/surface.js +506 -0
  70. package/dist/blocks/tones.d.ts +23 -0
  71. package/dist/blocks/tones.js +63 -0
  72. package/dist/blocks/treeview.d.ts +22 -0
  73. package/dist/blocks/treeview.js +121 -0
  74. package/dist/blocks/width.d.ts +32 -0
  75. package/dist/blocks/width.js +70 -0
  76. package/dist/clipboard.d.ts +20 -0
  77. package/dist/clipboard.js +19 -0
  78. package/dist/editor-launch.d.ts +34 -0
  79. package/dist/editor-launch.js +70 -0
  80. package/dist/keymaps/emacs.d.ts +18 -0
  81. package/dist/keymaps/emacs.js +172 -0
  82. package/dist/keymaps/standard.d.ts +17 -0
  83. package/dist/keymaps/standard.js +175 -0
  84. package/dist/keymaps/types.d.ts +43 -0
  85. package/dist/keymaps/types.js +28 -0
  86. package/dist/keymaps/vim.d.ts +48 -0
  87. package/dist/keymaps/vim.js +291 -0
  88. package/dist/launch.d.ts +159 -0
  89. package/dist/launch.js +478 -0
  90. package/dist/sse.d.ts +112 -0
  91. package/dist/sse.js +418 -0
  92. package/dist/theme.d.ts +51 -0
  93. package/dist/theme.js +119 -0
  94. package/dist/ui/ambient.d.ts +78 -0
  95. package/dist/ui/ambient.js +264 -0
  96. package/dist/ui/app.d.ts +319 -0
  97. package/dist/ui/app.js +1539 -0
  98. package/dist/ui/composer.d.ts +66 -0
  99. package/dist/ui/composer.js +133 -0
  100. package/dist/ui/conversation-pane.d.ts +143 -0
  101. package/dist/ui/conversation-pane.js +615 -0
  102. package/dist/ui/help.d.ts +15 -0
  103. package/dist/ui/help.js +80 -0
  104. package/dist/ui/interactions.d.ts +91 -0
  105. package/dist/ui/interactions.js +477 -0
  106. package/dist/ui/picker.d.ts +26 -0
  107. package/dist/ui/picker.js +130 -0
  108. package/dist/ui/settings.d.ts +68 -0
  109. package/dist/ui/settings.js +254 -0
  110. package/dist/ui/sidebar.d.ts +99 -0
  111. package/dist/ui/sidebar.js +269 -0
  112. package/dist/ui/status-bar.d.ts +77 -0
  113. package/dist/ui/status-bar.js +146 -0
  114. package/package.json +4 -3
@@ -0,0 +1,319 @@
1
+ /**
2
+ * The TUI shell (Task 6): sidebar left, conversation pane right, composer
3
+ * footer — pi's idiom over the Task 4 client layers.
4
+ *
5
+ * File naming (the controller's pick-one ruling): plain `.ts` — pi-tui is
6
+ * not React (components are `render(width): string[]` classes), so the
7
+ * plan's `app.tsx` alternative does not apply.
8
+ *
9
+ * Layout: {@link TuiApp} is a `VStack`: the body row (`HStack`: sidebar
10
+ * at a fixed basis, pane growing), then the interaction seam's prompt
11
+ * strip (Task 8 — the pending stack, answerable via ctrl+o + digits), then
12
+ * the ambient chrome (panel/footer/statusline zones by the record's
13
+ * `place`). Both strips collapse to zero height when empty. The composer
14
+ * is a pi-tui `Editor` the launcher mounts after the app on the TUI root —
15
+ * pi's idiom is `TuiMainScreen` (the terminal owns scrollback; chat content
16
+ * scrolls, the editor anchors the bottom as the last child). Viewport
17
+ * `VStack`/`ScrollView` layouts are alt-screen-only (pi-tui 1.3.0), so the
18
+ * footer composition lives in the launcher, not the app component.
19
+ *
20
+ * Wiring: the sidebar refreshes from `listConversations()`; selecting a
21
+ * conversation swaps the pane's `watchConversation` subscription (the old
22
+ * watch is closed before the new one opens); stream frames REPLACE the
23
+ * pane (the SSE contract — a snapshot subsumes missed frames); the
24
+ * composer submits fire-and-forget via `submit(id, text)` — the stream is
25
+ * the source of truth for the reply (spec §10's composer rule). The fork
26
+ * action (`forkActive`, driven by the `/fork` slash command; task C3's
27
+ * action layer reintroduces a keybinding) calls
28
+ * `forkConversation(id)` at the latest entry (the v1 default, spec §8),
29
+ * refreshes the sidebar, and switches to the fork.
30
+ *
31
+ * Composer behaviors (Task 7): the composer tracks the wire's `steer.busy`
32
+ * per frame; submitting while busy sends `whenBusy: "steer"`. Esc during a
33
+ * live run aborts (the binding rides the launcher's pre-editor global
34
+ * listener through {@link handleKeybinding} — pi-tui 1.3.0's `Editor` has
35
+ * no escape hook of its own). A `/`-prefixed line routes through the
36
+ * slash-command catalog (`getCommands()`, fetched once and cached by
37
+ * {@link SlashCatalog}); unknown commands render a clear error. The POST's
38
+ * immediate failure (4xx) and the abort outcomes render as pane
39
+ * error/notice lines — the pane keeps rendering the reply off the stream.
40
+ *
41
+ * Sidebar focus (fix wave F2): the sidebar is the switcher (spec §2), so it
42
+ * must be keyboard-reachable — ctrl+b toggles key routing between the
43
+ * composer and the conversation list; a selection (or a second ctrl+b)
44
+ * returns keys to the composer. Guarded actions (fix wave F1): every
45
+ * fire-and-forget mutation path (`+ new`, conversation select, ctrl+f
46
+ * fork) catches its rejection into a pane error line — a flaky remote must
47
+ * never crash the process on an unhandled rejection.
48
+ */
49
+ import { Editor, type Component, type TUI } from "@earendil-works/pi-tui";
50
+ import type { SigrunClient } from "../api.js";
51
+ import type { JsonObject } from "@sigrun/shared/blocks";
52
+ import { type Unwatch, type WatchHandlers, type WatchOptions } from "../sse.js";
53
+ import { type TuiTheme } from "../theme.js";
54
+ import type { Action, ActionHost, Focus } from "../actions.js";
55
+ import { AmbientChrome } from "./ambient.js";
56
+ import { ConversationPane } from "./conversation-pane.js";
57
+ import { InteractionPrompter } from "./interactions.js";
58
+ import { SettingsView } from "./settings.js";
59
+ import { Sidebar } from "./sidebar.js";
60
+ import type { StatusBar } from "./status-bar.js";
61
+ /** The watch injection seam (tests stub it; the launcher uses the real one). */
62
+ export type WatchFn = (conversationId: number, handlers: WatchHandlers, options: WatchOptions) => Unwatch;
63
+ /**
64
+ * How long the restart-recovery poll waits for /readyz before giving up
65
+ * with a permanent error (U4 Task 2, spec §13.3). DUPLICATE of the
66
+ * webui's `RESTART_READYZ_DEADLINE_MS` (webui/src/App.tsx) by design:
67
+ * the tui package consumes only `@sigrun/shared` wire types (NodeNext,
68
+ * no codegen), and a shared-module move for one number is not worth the
69
+ * coupling — keep the two in sync by hand.
70
+ */
71
+ export declare const RESTART_READYZ_DEADLINE_MS = 60000;
72
+ export interface TuiAppOptions {
73
+ /** The pi-tui instance (the Editor needs it for height-aware scrolling). */
74
+ readonly tui: TUI;
75
+ /** The stream watcher; defaults to Task 4's `watchConversation`. */
76
+ readonly watch?: WatchFn;
77
+ /** The watcher's transport options (baseUrl + user); defaults to none. */
78
+ readonly watchOptions?: WatchOptions;
79
+ /** The sidebar's fixed column basis (default 24). */
80
+ readonly sidebarWidth?: number;
81
+ /** The token theme (C1/C2); defaults to the plain identity fallback. */
82
+ readonly theme?: TuiTheme;
83
+ /** The tui.json `editor` field (C1; used when it names a vim binary). */
84
+ readonly editor?: string;
85
+ /** TEST SEAM: the vim resolution for edit-in-vim (C4). */
86
+ readonly resolveVim?: () => string | undefined;
87
+ /**
88
+ * The restart-recovery poll cadence (U4 Task 2, spec §13.3): the
89
+ * `/readyz` probe interval after `restart()` — default 500 ms; the
90
+ * tests shorten it.
91
+ */
92
+ readonly restartPollMs?: number;
93
+ /**
94
+ * TEST SEAM (U4 Task 2 round-2): the restart-recovery deadline —
95
+ * how long the poll waits for /readyz before surfacing the stalled
96
+ * restart as a permanent error notice. Default
97
+ * {@link RESTART_READYZ_DEADLINE_MS}; the deadline test shortens it.
98
+ */
99
+ readonly restartDeadlineMs?: number;
100
+ }
101
+ /** The main column's swap slot (U4 Task 4 round-2): renders the
102
+ * conversation pane, or the open settings view while one is up.
103
+ * The launcher mounts this slot (in its ScrollView) so the swap is
104
+ * visible — a settings view rendered only in `TuiApp.render`'s
105
+ * fallback path never paints in the launcher's composition. */
106
+ export declare class MainSlot implements Component {
107
+ #private;
108
+ constructor(child: Component);
109
+ /** Swap the rendered child (the conversation pane ↔ settings view). */
110
+ set(child: Component): void;
111
+ /** The current child (tests assert the swap). */
112
+ get current(): Component;
113
+ render(width: number): string[];
114
+ handleInput(data: string): void;
115
+ invalidate(): void;
116
+ }
117
+ export declare class TuiApp implements Component, ActionHost {
118
+ #private;
119
+ /** The sidebar (the app mounts it; tests drive it). */
120
+ readonly sidebar: Sidebar;
121
+ /** The conversation pane (the stream replaces its contents). */
122
+ readonly pane: ConversationPane;
123
+ /** The composer (the launcher mounts it after the app + focuses it). */
124
+ readonly editor: Editor;
125
+ /** The interaction seam's prompt strip (Task 8; zero height when empty). */
126
+ readonly interactions: InteractionPrompter;
127
+ /** The ambient seam's chrome zones (Task 8; zero height when empty). */
128
+ readonly ambient: AmbientChrome;
129
+ /** The main column's slot: renders the conversation pane normally,
130
+ * the open settings view while one is up (U4 Task 4 round-2 — the
131
+ * settings view renders IN the body slot; the launcher's root
132
+ * scrolls this slot, so a `#settingsView`-only render would paint
133
+ * nothing and swallow keys). Exported for the launcher's mount. */
134
+ readonly mainContent: MainSlot;
135
+ /** The quit sink: the launcher's exit resolution. */
136
+ onQuit: (() => void) | undefined;
137
+ constructor(client: SigrunClient, options: TuiAppOptions);
138
+ /** The active conversation id (undefined before the first refresh). */
139
+ get activeConversationId(): number | undefined;
140
+ /** The launcher's hello hook (U3 Task 5): the stream id feeds
141
+ * future openExtView transports; an OPEN view's handle gets the
142
+ * new id at once (its next fetch re-registers the watcher entry —
143
+ * spec §7.3's reconnect rule; the webui twin remounts instead). */
144
+ setEventsStreamId(streamId: string): void;
145
+ /** The open extension view's identity (undefined = the conversation
146
+ * pane is the main content). */
147
+ get extView(): {
148
+ readonly slug: string;
149
+ readonly id: string;
150
+ } | undefined;
151
+ /**
152
+ * Open an extension VIEW in the main pane (U3 Task 5, spec §7.1):
153
+ * the sidebar's view selection swaps the pane's content to the
154
+ * surface's transport. The block renderer is U2's; the app holds
155
+ * the transport + identity here so U2's mount is one prop pass.
156
+ */
157
+ openExtView(slug: string, id: string, params?: JsonObject): Promise<void>;
158
+ /**
159
+ * Open the settings view (U4 Task 4, spec §13.4): `/settings` mounts
160
+ * the SettingsView (the sections list + the search picker over the
161
+ * index); Esc closes it back to the conversation.
162
+ */
163
+ openSettings(): Promise<void>;
164
+ /** The open settings view (tests drive it). */
165
+ get settingsView(): SettingsView | undefined;
166
+ /**
167
+ * Close the open settings view (Esc). Returns true when one was open.
168
+ */
169
+ closeSettings(): boolean;
170
+ /**
171
+ * Close the open extension view (the Esc/back idiom): the pane
172
+ * returns to the conversation AND the surface's fetches stop — no
173
+ * cadence, no keepalive for a surface nobody is looking at (spec
174
+ * §7.3's stop-when-hidden rule; U2's cadence timer consults this).
175
+ * Returns true when a view was open (the key was consumed).
176
+ */
177
+ closeExtView(): boolean;
178
+ /** The app-level surface.changed dispatch (the launcher's
179
+ * `watchEvents` onEvent routes events here). */
180
+ dispatchSurfaceChanged(event: {
181
+ slug: string;
182
+ id: string;
183
+ k: string;
184
+ rev: number;
185
+ treeRev: number;
186
+ }): void;
187
+ /** The chat member of `surface.changed` (U5 Task 4): fan the
188
+ * conversation-scoped frame into the chat hub. */
189
+ dispatchChatSurfaceChanged(event: {
190
+ conversationId: number;
191
+ entryId: string;
192
+ rev: number;
193
+ }): void;
194
+ /** The focused region (the action layer's routing state). */
195
+ get focus(): Focus;
196
+ /** A modal overlay (picker/search) owns the keys. */
197
+ get overlayOpen(): boolean;
198
+ /** The last frame's `steer.busy` (the keymap's abort gate). */
199
+ get busy(): boolean;
200
+ /**
201
+ * The launcher's sink: the status bar it mounted (flashes, help, and
202
+ * the search overlay render through it). `set` also snapshots the
203
+ * launcher's steady state so a flash can restore it.
204
+ */
205
+ setStatusBar(statusBar: StatusBar): void;
206
+ /**
207
+ * Refresh the sidebar listing. Boot selects the first conversation;
208
+ * later refreshes keep the current selection.
209
+ */
210
+ refresh(): Promise<void>;
211
+ /**
212
+ * Swap the pane to `id`: close the old watch, open the new one. The
213
+ * composer is disabled for the swap's duration (review finding): a
214
+ * submission must never split from the pane it was typed against, so
215
+ * Enter cannot fire `onSubmit` mid-swap (pi-tui's `disableSubmit`
216
+ * gates the submit path).
217
+ */
218
+ selectConversation(id: number): Promise<void>;
219
+ /**
220
+ * Stream-error sink: the launcher wires this to the stream-health
221
+ * notices (fix wave F5 — "stream: disconnected" on the first failure,
222
+ * "stream: reconnected" on the next open). The generation guard above
223
+ * fences STALE watches' errors so they can't reach whatever is mounted
224
+ * here.
225
+ */
226
+ onWatchError?: (message: string) => void;
227
+ /**
228
+ * Stream-open sink (fix wave F5): fires on every (re)connect of the
229
+ * CURRENT watch — the recovery half of the launcher's stream-health
230
+ * notices. Generation-guarded like {@link onWatchError}.
231
+ */
232
+ onWatchOpen?: () => void;
233
+ /** Create a conversation (`+ new`), refresh the sidebar, select it. */
234
+ createAndSelect(): Promise<void>;
235
+ /**
236
+ * Fork the active conversation at the latest entry (the v1 default,
237
+ * spec §8), refresh the sidebar, and switch to the fork. No-op with no
238
+ * active conversation.
239
+ */
240
+ forkActive(): Promise<void>;
241
+ /**
242
+ * The launcher's global keybinding seam: ctrl+o enters the interaction
243
+ * seam's answer mode; Esc during a live run aborts it (the composer
244
+ * idiom — the launcher routes input through here before the editor sees
245
+ * it, so Esc while busy never reaches the editor). Returns true when the
246
+ * key was consumed. Task C1b removed ctrl+b (sidebar focus) and ctrl+f
247
+ * (fork): task C3's action layer reintroduces navigation — until then
248
+ * the mouse drives the sidebar and `/fork` carries the fork.
249
+ */
250
+ handleKeybinding(data: string): boolean;
251
+ /** Close the stream subscription + seam pollers (the launcher calls this on exit). */
252
+ dispose(): void;
253
+ /**
254
+ * The admin restart surface (U4 Task 2, spec §13.3): POST
255
+ * `/v1/admin/restart`, show the "Restarting…" notice, and poll
256
+ * `/readyz` — when it answers, the supervisor brought the server
257
+ * back, so refresh the sidebar (the durable listing) and clear the
258
+ * notice. A rejected POST (403/409 — no restart began) renders the
259
+ * error and returns without touching state. Resolves once the
260
+ * recovery poll ends (recovered or disposed).
261
+ */
262
+ restart(): Promise<void>;
263
+ /**
264
+ * The keymap's context: focus, busy, autocomplete, overlay. The
265
+ * composer's autocomplete state is internal to pi-tui's Editor (no
266
+ * public getter), so the flag is a projection — the composer is the
267
+ * default focus and the editor cancels autocomplete before any
268
+ * focus-leaving key the keymap would act on.
269
+ */
270
+ keyContext(): {
271
+ focus: Focus;
272
+ busy: boolean;
273
+ autocompleteOpen: boolean;
274
+ overlayOpen: boolean;
275
+ };
276
+ /** Run one action (the keymap's / pickers' / mouse's single seam). */
277
+ dispatch(action: Action): Promise<void>;
278
+ /**
279
+ * The launcher's keymap seam (C5 fix): the active keymap's
280
+ * `hints(ctx)` for the current {@link keyContext}. Wired once at
281
+ * launch; {@link refreshHints} re-polls it on every focus change
282
+ * (and the launcher re-polls after each key for vim mode changes).
283
+ */
284
+ hintsProvider: (() => readonly string[]) | undefined;
285
+ /**
286
+ * Re-poll the keymap's hints into the status bar's steady state.
287
+ * A transient flash or the search overlay owns the bar's right
288
+ * cluster until it restores the snapshot, so only the snapshot is
289
+ * updated while one is up (the restore paints the fresh hints).
290
+ */
291
+ refreshHints(): void;
292
+ /**
293
+ * The vim command line's command table (Task C4). A `/`-prefixed line
294
+ * (or any name outside the table) routes through the existing slash
295
+ * catalog — the table holds only the vim-native commands.
296
+ */
297
+ runCommand(line: string): Promise<void>;
298
+ /** The launcher's vim-keymap seam: re-enter INSERT after edit-in-vim. */
299
+ enterInsertMode: (() => void) | undefined;
300
+ /**
301
+ * The launcher's help-overlay seam (C5): opens the key reference for
302
+ * the active keymap; the callback fires when the overlay closes.
303
+ */
304
+ helpOverlay: ((onClose: () => void) => void) | undefined;
305
+ /**
306
+ * The slash catalog's command names (C3's picker source, cached) —
307
+ * the launcher feeds them to the vim `:` Tab completion.
308
+ */
309
+ slashCommandNames(): Promise<string[]>;
310
+ /**
311
+ * The vim keymap's status-bar seam (Task C4): the app polls the keymap
312
+ * after each key. `commandLine` set → the bar shows the `:` line;
313
+ * otherwise the steady state is restored with the keymap's mode chip
314
+ * in the `mode` segment.
315
+ */
316
+ renderCommandLine(commandLine: string | undefined, modeLabel: string | undefined): void;
317
+ render(width: number): string[];
318
+ invalidate(): void;
319
+ }