@sigrun/tui 0.1.0 → 0.2.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 +12 -0
- package/package.json +3 -3
- package/dist/actions.d.ts +0 -78
- package/dist/actions.js +0 -12
- package/dist/api.d.ts +0 -147
- package/dist/api.js +0 -185
- package/dist/clipboard.d.ts +0 -20
- package/dist/clipboard.js +0 -19
- package/dist/editor-launch.d.ts +0 -34
- package/dist/editor-launch.js +0 -70
- package/dist/keymaps/emacs.d.ts +0 -18
- package/dist/keymaps/emacs.js +0 -133
- package/dist/keymaps/standard.d.ts +0 -17
- package/dist/keymaps/standard.js +0 -131
- package/dist/keymaps/types.d.ts +0 -43
- package/dist/keymaps/types.js +0 -28
- package/dist/keymaps/vim.d.ts +0 -48
- package/dist/keymaps/vim.js +0 -254
- package/dist/launch.d.ts +0 -149
- package/dist/launch.js +0 -433
- package/dist/sse.d.ts +0 -101
- package/dist/sse.js +0 -374
- package/dist/theme.d.ts +0 -51
- package/dist/theme.js +0 -105
- package/dist/ui/ambient.d.ts +0 -81
- package/dist/ui/ambient.js +0 -301
- package/dist/ui/app.d.ts +0 -209
- package/dist/ui/app.js +0 -1241
- package/dist/ui/composer.d.ts +0 -66
- package/dist/ui/composer.js +0 -125
- package/dist/ui/conversation-pane.d.ts +0 -123
- package/dist/ui/conversation-pane.js +0 -463
- package/dist/ui/help.d.ts +0 -15
- package/dist/ui/help.js +0 -80
- package/dist/ui/interactions.d.ts +0 -90
- package/dist/ui/interactions.js +0 -335
- package/dist/ui/picker.d.ts +0 -26
- package/dist/ui/picker.js +0 -130
- package/dist/ui/sidebar.d.ts +0 -76
- package/dist/ui/sidebar.js +0 -205
- package/dist/ui/status-bar.d.ts +0 -77
- package/dist/ui/status-bar.js +0 -146
package/dist/ui/ambient.js
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The ambient seam's TUI renderer (Task 8 — the parity rule): ambient
|
|
3
|
-
* surfaces placed in the TUI's chrome per the seam's `place` field. Spec
|
|
4
|
-
* §8 leaves the place→chrome mapping to the builder; the TUI's mapping:
|
|
5
|
-
*
|
|
6
|
-
* - `status` → the statusline: one row, directly above the composer (the
|
|
7
|
-
* last row of the app body). Multiple status records join with " · ".
|
|
8
|
-
* - `footer` → the footer zone: one summary line per record, above the
|
|
9
|
-
* statusline.
|
|
10
|
-
* - `panel` → the panel zone: the full rendering (line, rows, gauge,
|
|
11
|
-
* ledger), above the footer.
|
|
12
|
-
*
|
|
13
|
-
* Genericity (the seam's whole point): the chrome renders the closed
|
|
14
|
-
* primitives and knows NOTHING about what a surface means — no "todo",
|
|
15
|
-
* no "goal". `details` renders as one collapsed JSON line, never
|
|
16
|
-
* interpreted. The rendering mirrors the webui's AmbientPanel: rows are
|
|
17
|
-
* severity-sorted (0 severe .. 2 quiet, ties by id) with quiet rows
|
|
18
|
-
* folded into a "+N quiet" summary; metrics format via the record's
|
|
19
|
-
* `format` hint; gauges render label/phase/elapsed plus a text percent
|
|
20
|
-
* bar; ledgers render newest-first. Ambient surfaces are read-only (the
|
|
21
|
-
* seam routes actions through interaction records).
|
|
22
|
-
*
|
|
23
|
-
* Fetching (W3 Task 4 — the timer cadence is RETIRED): on conversation
|
|
24
|
-
* connect, and on `ambient.changed` host events for the live
|
|
25
|
-
* conversation (the app wires `watchEvents` to {@link kick}; an event
|
|
26
|
-
* for another conversation is ignored). A record's declared `refreshMs`
|
|
27
|
-
* no longer drives a poll — the extension that declares it owns its
|
|
28
|
-
* writes, and each write emits the event. Fetch failures are QUIET
|
|
29
|
-
* (best-effort chrome; stream transport health is reported separately by
|
|
30
|
-
* the watcher's error/open transition notices — the launcher's
|
|
31
|
-
* stream-health wiring).
|
|
32
|
-
*
|
|
33
|
-
* Expiry: `expires: "idle"` surfaces render only while the conversation
|
|
34
|
-
* has a live run ({@link setBusy}, driven by the stream's `steer.busy`);
|
|
35
|
-
* they disappear when the conversation goes idle and reappear on the next
|
|
36
|
-
* busy frame while the record is still served. `expires: "never"` records
|
|
37
|
-
* stay until the server stops serving them (replacement-state reads).
|
|
38
|
-
*
|
|
39
|
-
* Segments: a line's `segments` render as their texts joined with spaces
|
|
40
|
-
* in AUTHORED order — the segment `place` zones (left/middle/right) are
|
|
41
|
-
* ignored in the statusline join (the TUI chrome has no zone layout; the
|
|
42
|
-
* webui owns zoned placement).
|
|
43
|
-
*/
|
|
44
|
-
/** The gauge bar width (text cells). */
|
|
45
|
-
const GAUGE_CELLS = 10;
|
|
46
|
-
export class AmbientChrome {
|
|
47
|
-
#client;
|
|
48
|
-
#conversationId;
|
|
49
|
-
/** Monotonic per-conversation generation: stale fetches never land. */
|
|
50
|
-
#gen = 0;
|
|
51
|
-
/** The latest served surfaces, keyed by slot key (fetch order). */
|
|
52
|
-
#records = [];
|
|
53
|
-
/** Whether the conversation has a live run (stream `steer.busy`). */
|
|
54
|
-
#busy = false;
|
|
55
|
-
/** Rendered-state changes (the app requests a repaint). */
|
|
56
|
-
onChanged;
|
|
57
|
-
constructor(client) {
|
|
58
|
-
this.#client = client;
|
|
59
|
-
}
|
|
60
|
-
/** The latest served surfaces (before the idle filter). */
|
|
61
|
-
get records() {
|
|
62
|
-
return this.#records;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Retarget the chrome: reset state and fetch immediately (the brief's
|
|
66
|
-
* "fetch on connect"). `undefined` clears the chrome. (W3 Task 4: no
|
|
67
|
-
* timer cadence — `ambient.changed` host events own the refreshes.)
|
|
68
|
-
*/
|
|
69
|
-
setConversation(id) {
|
|
70
|
-
this.#gen += 1;
|
|
71
|
-
this.#conversationId = id;
|
|
72
|
-
this.#records = [];
|
|
73
|
-
if (id === undefined) {
|
|
74
|
-
this.#changed();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
void this.#refresh();
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Refetch when an `ambient.changed` host event names the LIVE
|
|
81
|
-
* conversation (the app's events wiring calls this; other
|
|
82
|
-
* conversations' events are ignored).
|
|
83
|
-
*/
|
|
84
|
-
kick(conversationId) {
|
|
85
|
-
if (conversationId !== this.#conversationId)
|
|
86
|
-
return;
|
|
87
|
-
void this.#refresh();
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* The conversation's run state (the app feeds it from stream frames).
|
|
91
|
-
* `expires: "idle"` surfaces render only while busy.
|
|
92
|
-
*/
|
|
93
|
-
setBusy(busy) {
|
|
94
|
-
if (busy === this.#busy)
|
|
95
|
-
return;
|
|
96
|
-
this.#busy = busy;
|
|
97
|
-
this.#changed();
|
|
98
|
-
}
|
|
99
|
-
/** The statusline: the status records' summaries joined " · ". */
|
|
100
|
-
statusline() {
|
|
101
|
-
return this.#visible("status").map(summaryOf).join(" · ");
|
|
102
|
-
}
|
|
103
|
-
/** The footer zone: one summary line per footer record. */
|
|
104
|
-
footerLines() {
|
|
105
|
-
return this.#visible("footer").map(summaryOf);
|
|
106
|
-
}
|
|
107
|
-
/** The panel zone: the full per-record rendering, blocks joined. */
|
|
108
|
-
panelLines() {
|
|
109
|
-
const lines = [];
|
|
110
|
-
for (const record of this.#visible("panel")) {
|
|
111
|
-
if (lines.length > 0)
|
|
112
|
-
lines.push("");
|
|
113
|
-
lines.push(...panelBlock(record));
|
|
114
|
-
}
|
|
115
|
-
return lines;
|
|
116
|
-
}
|
|
117
|
-
/** Visible records in one place zone (fetch order preserved). */
|
|
118
|
-
#visible(place) {
|
|
119
|
-
return this.#records.filter((record) => record.place === place &&
|
|
120
|
-
(record.expires !== "idle" || this.#busy) &&
|
|
121
|
-
hasContent(record));
|
|
122
|
-
}
|
|
123
|
-
/** Generation-guarded fetch (quiet failures keep the last surfaces). */
|
|
124
|
-
async #refresh() {
|
|
125
|
-
const id = this.#conversationId;
|
|
126
|
-
const gen = this.#gen;
|
|
127
|
-
if (id === undefined)
|
|
128
|
-
return;
|
|
129
|
-
let list;
|
|
130
|
-
try {
|
|
131
|
-
list = await this.#client.getAmbient(id);
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
return; // quiet: keep the last surfaces
|
|
135
|
-
}
|
|
136
|
-
if (gen !== this.#gen)
|
|
137
|
-
return; // stale — a switch happened
|
|
138
|
-
this.#records = list.ambient;
|
|
139
|
-
this.#changed();
|
|
140
|
-
}
|
|
141
|
-
#changed() {
|
|
142
|
-
this.onChanged?.();
|
|
143
|
-
}
|
|
144
|
-
/** Detach (the app calls this on dispose): stale fetches never land. */
|
|
145
|
-
dispose() {
|
|
146
|
-
this.#gen += 1;
|
|
147
|
-
}
|
|
148
|
-
/** The chrome: panel blocks, then footer lines, then the statusline. */
|
|
149
|
-
render(_width) {
|
|
150
|
-
const lines = [...this.panelLines(), ...this.footerLines()];
|
|
151
|
-
const status = this.statusline();
|
|
152
|
-
if (status !== "")
|
|
153
|
-
lines.push(status);
|
|
154
|
-
return lines;
|
|
155
|
-
}
|
|
156
|
-
invalidate() {
|
|
157
|
-
// Stateless render: lines assemble from the current records.
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// ─── Rendering helpers (the webui's AmbientPanel, mirrored in text) ─────
|
|
161
|
-
/** A surface with no body primitives and no details renders nothing. */
|
|
162
|
-
function hasContent(record) {
|
|
163
|
-
const body = record.body;
|
|
164
|
-
if (body === undefined)
|
|
165
|
-
return record.details !== undefined;
|
|
166
|
-
return (body.line !== undefined ||
|
|
167
|
-
(body.rows !== undefined && body.rows.length > 0) ||
|
|
168
|
-
body.gauge !== undefined ||
|
|
169
|
-
(body.ledger !== undefined && body.ledger.length > 0) ||
|
|
170
|
-
record.details !== undefined);
|
|
171
|
-
}
|
|
172
|
-
/** The one-line summary (status/footer zones). */
|
|
173
|
-
function summaryOf(record) {
|
|
174
|
-
const body = record.body;
|
|
175
|
-
if (body?.line !== undefined)
|
|
176
|
-
return lineText(body.line);
|
|
177
|
-
if (body?.rows !== undefined && body.rows.length > 0) {
|
|
178
|
-
const visible = body.rows.filter((row) => row.quiet !== true);
|
|
179
|
-
if (visible.length === 0)
|
|
180
|
-
return record.key;
|
|
181
|
-
return `${record.key}: ${visible.map((row) => row.label).join(", ")}`;
|
|
182
|
-
}
|
|
183
|
-
if (body?.gauge !== undefined) {
|
|
184
|
-
const percent = body.gauge.percent === undefined ? "" : ` ${body.gauge.percent}%`;
|
|
185
|
-
return `${body.gauge.label}${percent}`;
|
|
186
|
-
}
|
|
187
|
-
if (body?.ledger !== undefined && body.ledger.length > 0) {
|
|
188
|
-
return body.ledger[body.ledger.length - 1]?.summary ?? record.key;
|
|
189
|
-
}
|
|
190
|
-
return `[${record.key}]`;
|
|
191
|
-
}
|
|
192
|
-
function lineText(line) {
|
|
193
|
-
if (line.segments !== undefined && line.segments.length > 0) {
|
|
194
|
-
return line.segments.map((segment) => segment.text).join(" ");
|
|
195
|
-
}
|
|
196
|
-
return line.text ?? "";
|
|
197
|
-
}
|
|
198
|
-
/** The full panel-zone rendering of one record. */
|
|
199
|
-
function panelBlock(record) {
|
|
200
|
-
const body = record.body;
|
|
201
|
-
const lines = [];
|
|
202
|
-
lines.push(record.owner === "" ? `[${record.key}]` : `[${record.key}] ${record.owner}`);
|
|
203
|
-
if (body?.line !== undefined)
|
|
204
|
-
lines.push(lineText(body.line));
|
|
205
|
-
if (body?.rows !== undefined && body.rows.length > 0) {
|
|
206
|
-
lines.push(...rowLines(body.rows));
|
|
207
|
-
}
|
|
208
|
-
if (body?.gauge !== undefined) {
|
|
209
|
-
const gauge = body.gauge;
|
|
210
|
-
let header = gauge.label;
|
|
211
|
-
if (gauge.phase !== undefined)
|
|
212
|
-
header += ` · ${gauge.phase}`;
|
|
213
|
-
if (gauge.elapsedMs !== undefined) {
|
|
214
|
-
header += ` · ${(gauge.elapsedMs / 1000).toFixed(1)}s`;
|
|
215
|
-
}
|
|
216
|
-
lines.push(header);
|
|
217
|
-
if (gauge.percent !== undefined) {
|
|
218
|
-
const filled = Math.max(0, Math.min(GAUGE_CELLS, Math.round(gauge.percent / GAUGE_CELLS)));
|
|
219
|
-
lines.push(`[${"#".repeat(filled)}${"-".repeat(GAUGE_CELLS - filled)}] ${gauge.percent}%`);
|
|
220
|
-
}
|
|
221
|
-
if (gauge.checks !== undefined && gauge.checks.length > 0) {
|
|
222
|
-
lines.push(...rowLines(gauge.checks));
|
|
223
|
-
}
|
|
224
|
-
if (gauge.recentOutput !== undefined && gauge.recentOutput.length > 0) {
|
|
225
|
-
lines.push(...gauge.recentOutput.slice(-3));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (body?.ledger !== undefined && body.ledger.length > 0) {
|
|
229
|
-
for (const entry of [...body.ledger].reverse()) {
|
|
230
|
-
const detail = entry.detail === undefined || entry.detail.length === 0
|
|
231
|
-
? ""
|
|
232
|
-
: ` — ${entry.detail.join(" · ")}`;
|
|
233
|
-
lines.push(`${entry.kind} ${entry.summary}${detail}`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (record.details !== undefined) {
|
|
237
|
-
lines.push(`[details] ${JSON.stringify(record.details)}`);
|
|
238
|
-
}
|
|
239
|
-
return lines;
|
|
240
|
-
}
|
|
241
|
-
/** Rows: severity-sorted (ties by id), quiet folded into "+N quiet". */
|
|
242
|
-
function rowLines(rows) {
|
|
243
|
-
const sorted = [...rows].sort((a, b) => {
|
|
244
|
-
const severityA = a.severity ?? 2;
|
|
245
|
-
const severityB = b.severity ?? 2;
|
|
246
|
-
if (severityA !== severityB)
|
|
247
|
-
return severityA - severityB;
|
|
248
|
-
return a.id < b.id ? -1 : 1;
|
|
249
|
-
});
|
|
250
|
-
const visible = sorted.filter((row) => row.quiet !== true);
|
|
251
|
-
const quietCount = sorted.length - visible.length;
|
|
252
|
-
const lines = visible.map(rowLine);
|
|
253
|
-
if (quietCount > 0)
|
|
254
|
-
lines.push(`+${quietCount} quiet`);
|
|
255
|
-
return lines;
|
|
256
|
-
}
|
|
257
|
-
function rowLine(row) {
|
|
258
|
-
const indent = " ".repeat(row.depth ?? 0);
|
|
259
|
-
const glyph = row.status ?? "·";
|
|
260
|
-
let line = `${indent}${glyph} ${row.label}`;
|
|
261
|
-
if (row.spinnerLabel !== undefined)
|
|
262
|
-
line += ` ${row.spinnerLabel}`;
|
|
263
|
-
if (row.detail !== undefined)
|
|
264
|
-
line += ` — ${row.detail}`;
|
|
265
|
-
if (row.deps !== undefined && row.deps.length > 0) {
|
|
266
|
-
line += ` ⛓ ${row.deps.join(",")}`;
|
|
267
|
-
}
|
|
268
|
-
const metrics = metricsText(row.metrics);
|
|
269
|
-
if (metrics !== "")
|
|
270
|
-
line += ` ${metrics}`;
|
|
271
|
-
return line;
|
|
272
|
-
}
|
|
273
|
-
function metricsText(metrics) {
|
|
274
|
-
if (metrics === undefined)
|
|
275
|
-
return "";
|
|
276
|
-
const parts = [];
|
|
277
|
-
if (metrics.tokens !== undefined) {
|
|
278
|
-
parts.push(`tok ${formatMetric(metrics.tokens, metrics.format)}`);
|
|
279
|
-
}
|
|
280
|
-
if (metrics.cost !== undefined) {
|
|
281
|
-
parts.push(`$${metrics.cost.toFixed(4)}`);
|
|
282
|
-
}
|
|
283
|
-
if (metrics.total !== undefined) {
|
|
284
|
-
parts.push(`${metrics.completed ?? metrics.current ?? 0}/${metrics.total}`);
|
|
285
|
-
}
|
|
286
|
-
else if (metrics.current !== undefined) {
|
|
287
|
-
parts.push(`cur ${metrics.current}`);
|
|
288
|
-
}
|
|
289
|
-
return parts.join(" · ");
|
|
290
|
-
}
|
|
291
|
-
/** The webui's metric formatting (compact / usd / percent hints). */
|
|
292
|
-
function formatMetric(value, format) {
|
|
293
|
-
if (format === "usd")
|
|
294
|
-
return `$${value.toFixed(4)}`;
|
|
295
|
-
if (format === "percent")
|
|
296
|
-
return `${Math.round(value * 100)}%`;
|
|
297
|
-
if (format === "compact" && Math.abs(value) >= 1000) {
|
|
298
|
-
return `${(value / 1000).toFixed(1)}k`;
|
|
299
|
-
}
|
|
300
|
-
return String(value);
|
|
301
|
-
}
|
package/dist/ui/app.d.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
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 Unwatch, type WatchHandlers, type WatchOptions } from "../sse.js";
|
|
52
|
-
import type { TuiTheme } from "../theme.js";
|
|
53
|
-
import type { Action, ActionHost, Focus } from "../actions.js";
|
|
54
|
-
import { AmbientChrome } from "./ambient.js";
|
|
55
|
-
import { ConversationPane } from "./conversation-pane.js";
|
|
56
|
-
import { InteractionPrompter } from "./interactions.js";
|
|
57
|
-
import { Sidebar } from "./sidebar.js";
|
|
58
|
-
import type { StatusBar } from "./status-bar.js";
|
|
59
|
-
/** The watch injection seam (tests stub it; the launcher uses the real one). */
|
|
60
|
-
export type WatchFn = (conversationId: number, handlers: WatchHandlers, options: WatchOptions) => Unwatch;
|
|
61
|
-
export interface TuiAppOptions {
|
|
62
|
-
/** The pi-tui instance (the Editor needs it for height-aware scrolling). */
|
|
63
|
-
readonly tui: TUI;
|
|
64
|
-
/** The stream watcher; defaults to Task 4's `watchConversation`. */
|
|
65
|
-
readonly watch?: WatchFn;
|
|
66
|
-
/** The watcher's transport options (baseUrl + user); defaults to none. */
|
|
67
|
-
readonly watchOptions?: WatchOptions;
|
|
68
|
-
/** The sidebar's fixed column basis (default 24). */
|
|
69
|
-
readonly sidebarWidth?: number;
|
|
70
|
-
/** The token theme (C1/C2); defaults to the plain identity fallback. */
|
|
71
|
-
readonly theme?: TuiTheme;
|
|
72
|
-
/** The tui.json `editor` field (C1; used when it names a vim binary). */
|
|
73
|
-
readonly editor?: string;
|
|
74
|
-
/** TEST SEAM: the vim resolution for edit-in-vim (C4). */
|
|
75
|
-
readonly resolveVim?: () => string | undefined;
|
|
76
|
-
}
|
|
77
|
-
export declare class TuiApp implements Component, ActionHost {
|
|
78
|
-
#private;
|
|
79
|
-
/** The sidebar (the app mounts it; tests drive it). */
|
|
80
|
-
readonly sidebar: Sidebar;
|
|
81
|
-
/** The conversation pane (the stream replaces its contents). */
|
|
82
|
-
readonly pane: ConversationPane;
|
|
83
|
-
/** The composer (the launcher mounts it after the app + focuses it). */
|
|
84
|
-
readonly editor: Editor;
|
|
85
|
-
/** The interaction seam's prompt strip (Task 8; zero height when empty). */
|
|
86
|
-
readonly interactions: InteractionPrompter;
|
|
87
|
-
/** The ambient seam's chrome zones (Task 8; zero height when empty). */
|
|
88
|
-
readonly ambient: AmbientChrome;
|
|
89
|
-
/** The quit sink: the launcher's exit resolution. */
|
|
90
|
-
onQuit: (() => void) | undefined;
|
|
91
|
-
constructor(client: SigrunClient, options: TuiAppOptions);
|
|
92
|
-
/** The active conversation id (undefined before the first refresh). */
|
|
93
|
-
get activeConversationId(): number | undefined;
|
|
94
|
-
/** The focused region (the action layer's routing state). */
|
|
95
|
-
get focus(): Focus;
|
|
96
|
-
/** A modal overlay (picker/search) owns the keys. */
|
|
97
|
-
get overlayOpen(): boolean;
|
|
98
|
-
/** The last frame's `steer.busy` (the keymap's abort gate). */
|
|
99
|
-
get busy(): boolean;
|
|
100
|
-
/**
|
|
101
|
-
* The launcher's sink: the status bar it mounted (flashes, help, and
|
|
102
|
-
* the search overlay render through it). `set` also snapshots the
|
|
103
|
-
* launcher's steady state so a flash can restore it.
|
|
104
|
-
*/
|
|
105
|
-
setStatusBar(statusBar: StatusBar): void;
|
|
106
|
-
/**
|
|
107
|
-
* Refresh the sidebar listing. Boot selects the first conversation;
|
|
108
|
-
* later refreshes keep the current selection.
|
|
109
|
-
*/
|
|
110
|
-
refresh(): Promise<void>;
|
|
111
|
-
/**
|
|
112
|
-
* Swap the pane to `id`: close the old watch, open the new one. The
|
|
113
|
-
* composer is disabled for the swap's duration (review finding): a
|
|
114
|
-
* submission must never split from the pane it was typed against, so
|
|
115
|
-
* Enter cannot fire `onSubmit` mid-swap (pi-tui's `disableSubmit`
|
|
116
|
-
* gates the submit path).
|
|
117
|
-
*/
|
|
118
|
-
selectConversation(id: number): Promise<void>;
|
|
119
|
-
/**
|
|
120
|
-
* Stream-error sink: the launcher wires this to the stream-health
|
|
121
|
-
* notices (fix wave F5 — "stream: disconnected" on the first failure,
|
|
122
|
-
* "stream: reconnected" on the next open). The generation guard above
|
|
123
|
-
* fences STALE watches' errors so they can't reach whatever is mounted
|
|
124
|
-
* here.
|
|
125
|
-
*/
|
|
126
|
-
onWatchError?: (message: string) => void;
|
|
127
|
-
/**
|
|
128
|
-
* Stream-open sink (fix wave F5): fires on every (re)connect of the
|
|
129
|
-
* CURRENT watch — the recovery half of the launcher's stream-health
|
|
130
|
-
* notices. Generation-guarded like {@link onWatchError}.
|
|
131
|
-
*/
|
|
132
|
-
onWatchOpen?: () => void;
|
|
133
|
-
/** Create a conversation (`+ new`), refresh the sidebar, select it. */
|
|
134
|
-
createAndSelect(): Promise<void>;
|
|
135
|
-
/**
|
|
136
|
-
* Fork the active conversation at the latest entry (the v1 default,
|
|
137
|
-
* spec §8), refresh the sidebar, and switch to the fork. No-op with no
|
|
138
|
-
* active conversation.
|
|
139
|
-
*/
|
|
140
|
-
forkActive(): Promise<void>;
|
|
141
|
-
/**
|
|
142
|
-
* The launcher's global keybinding seam: ctrl+o enters the interaction
|
|
143
|
-
* seam's answer mode; Esc during a live run aborts it (the composer
|
|
144
|
-
* idiom — the launcher routes input through here before the editor sees
|
|
145
|
-
* it, so Esc while busy never reaches the editor). Returns true when the
|
|
146
|
-
* key was consumed. Task C1b removed ctrl+b (sidebar focus) and ctrl+f
|
|
147
|
-
* (fork): task C3's action layer reintroduces navigation — until then
|
|
148
|
-
* the mouse drives the sidebar and `/fork` carries the fork.
|
|
149
|
-
*/
|
|
150
|
-
handleKeybinding(data: string): boolean;
|
|
151
|
-
/** Close the stream subscription + seam pollers (the launcher calls this on exit). */
|
|
152
|
-
dispose(): void;
|
|
153
|
-
/**
|
|
154
|
-
* The keymap's context: focus, busy, autocomplete, overlay. The
|
|
155
|
-
* composer's autocomplete state is internal to pi-tui's Editor (no
|
|
156
|
-
* public getter), so the flag is a projection — the composer is the
|
|
157
|
-
* default focus and the editor cancels autocomplete before any
|
|
158
|
-
* focus-leaving key the keymap would act on.
|
|
159
|
-
*/
|
|
160
|
-
keyContext(): {
|
|
161
|
-
focus: Focus;
|
|
162
|
-
busy: boolean;
|
|
163
|
-
autocompleteOpen: boolean;
|
|
164
|
-
overlayOpen: boolean;
|
|
165
|
-
};
|
|
166
|
-
/** Run one action (the keymap's / pickers' / mouse's single seam). */
|
|
167
|
-
dispatch(action: Action): Promise<void>;
|
|
168
|
-
/**
|
|
169
|
-
* The launcher's keymap seam (C5 fix): the active keymap's
|
|
170
|
-
* `hints(ctx)` for the current {@link keyContext}. Wired once at
|
|
171
|
-
* launch; {@link refreshHints} re-polls it on every focus change
|
|
172
|
-
* (and the launcher re-polls after each key for vim mode changes).
|
|
173
|
-
*/
|
|
174
|
-
hintsProvider: (() => readonly string[]) | undefined;
|
|
175
|
-
/**
|
|
176
|
-
* Re-poll the keymap's hints into the status bar's steady state.
|
|
177
|
-
* A transient flash or the search overlay owns the bar's right
|
|
178
|
-
* cluster until it restores the snapshot, so only the snapshot is
|
|
179
|
-
* updated while one is up (the restore paints the fresh hints).
|
|
180
|
-
*/
|
|
181
|
-
refreshHints(): void;
|
|
182
|
-
/**
|
|
183
|
-
* The vim command line's command table (Task C4). A `/`-prefixed line
|
|
184
|
-
* (or any name outside the table) routes through the existing slash
|
|
185
|
-
* catalog — the table holds only the vim-native commands.
|
|
186
|
-
*/
|
|
187
|
-
runCommand(line: string): Promise<void>;
|
|
188
|
-
/** The launcher's vim-keymap seam: re-enter INSERT after edit-in-vim. */
|
|
189
|
-
enterInsertMode: (() => void) | undefined;
|
|
190
|
-
/**
|
|
191
|
-
* The launcher's help-overlay seam (C5): opens the key reference for
|
|
192
|
-
* the active keymap; the callback fires when the overlay closes.
|
|
193
|
-
*/
|
|
194
|
-
helpOverlay: ((onClose: () => void) => void) | undefined;
|
|
195
|
-
/**
|
|
196
|
-
* The slash catalog's command names (C3's picker source, cached) —
|
|
197
|
-
* the launcher feeds them to the vim `:` Tab completion.
|
|
198
|
-
*/
|
|
199
|
-
slashCommandNames(): Promise<string[]>;
|
|
200
|
-
/**
|
|
201
|
-
* The vim keymap's status-bar seam (Task C4): the app polls the keymap
|
|
202
|
-
* after each key. `commandLine` set → the bar shows the `:` line;
|
|
203
|
-
* otherwise the steady state is restored with the keymap's mode chip
|
|
204
|
-
* in the `mode` segment.
|
|
205
|
-
*/
|
|
206
|
-
renderCommandLine(commandLine: string | undefined, modeLabel: string | undefined): void;
|
|
207
|
-
render(width: number): string[];
|
|
208
|
-
invalidate(): void;
|
|
209
|
-
}
|