@sigrun/tui 0.1.1 → 0.2.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.
- package/CHANGELOG.md +11 -0
- package/package.json +2 -2
- package/dist/actions.d.ts +0 -119
- package/dist/actions.js +0 -12
- package/dist/api.d.ts +0 -152
- package/dist/api.js +0 -189
- package/dist/blocks/braille.d.ts +0 -41
- package/dist/blocks/braille.js +0 -131
- package/dist/blocks/chart.d.ts +0 -20
- package/dist/blocks/chart.js +0 -397
- package/dist/blocks/collection/calendar.d.ts +0 -28
- package/dist/blocks/collection/calendar.js +0 -232
- package/dist/blocks/collection/card.d.ts +0 -36
- package/dist/blocks/collection/card.js +0 -128
- package/dist/blocks/collection/collection.d.ts +0 -9
- package/dist/blocks/collection/collection.js +0 -26
- package/dist/blocks/collection/grid.d.ts +0 -18
- package/dist/blocks/collection/grid.js +0 -72
- package/dist/blocks/collection/lanes.d.ts +0 -32
- package/dist/blocks/collection/lanes.js +0 -320
- package/dist/blocks/collection/list.d.ts +0 -30
- package/dist/blocks/collection/list.js +0 -273
- package/dist/blocks/collection/move.d.ts +0 -67
- package/dist/blocks/collection/move.js +0 -96
- package/dist/blocks/collection/shared.d.ts +0 -87
- package/dist/blocks/collection/shared.js +0 -177
- package/dist/blocks/collection/table.d.ts +0 -26
- package/dist/blocks/collection/table.js +0 -221
- package/dist/blocks/content.d.ts +0 -48
- package/dist/blocks/content.js +0 -253
- package/dist/blocks/context.d.ts +0 -96
- package/dist/blocks/context.js +0 -65
- package/dist/blocks/diff.d.ts +0 -21
- package/dist/blocks/diff.js +0 -25
- package/dist/blocks/ext-transport.d.ts +0 -70
- package/dist/blocks/ext-transport.js +0 -165
- package/dist/blocks/form/choice.d.ts +0 -54
- package/dist/blocks/form/choice.js +0 -175
- package/dist/blocks/form/fields.d.ts +0 -56
- package/dist/blocks/form/fields.js +0 -136
- package/dist/blocks/form/file.d.ts +0 -61
- package/dist/blocks/form/file.js +0 -121
- package/dist/blocks/form/form.d.ts +0 -53
- package/dist/blocks/form/form.js +0 -703
- package/dist/blocks/form/list.d.ts +0 -37
- package/dist/blocks/form/list.js +0 -70
- package/dist/blocks/form/values.d.ts +0 -20
- package/dist/blocks/form/values.js +0 -104
- package/dist/blocks/icons.d.ts +0 -14
- package/dist/blocks/icons.js +0 -74
- package/dist/blocks/image.d.ts +0 -39
- package/dist/blocks/image.js +0 -82
- package/dist/blocks/layout.d.ts +0 -31
- package/dist/blocks/layout.js +0 -155
- package/dist/blocks/log.d.ts +0 -24
- package/dist/blocks/log.js +0 -111
- package/dist/blocks/meter.d.ts +0 -16
- package/dist/blocks/meter.js +0 -50
- package/dist/blocks/page.d.ts +0 -54
- package/dist/blocks/page.js +0 -355
- package/dist/blocks/render.d.ts +0 -31
- package/dist/blocks/render.js +0 -145
- package/dist/blocks/surface-view.d.ts +0 -33
- package/dist/blocks/surface-view.js +0 -86
- package/dist/blocks/surface.d.ts +0 -75
- package/dist/blocks/surface.js +0 -468
- package/dist/blocks/tones.d.ts +0 -23
- package/dist/blocks/tones.js +0 -63
- package/dist/blocks/treeview.d.ts +0 -22
- package/dist/blocks/treeview.js +0 -121
- package/dist/blocks/width.d.ts +0 -32
- package/dist/blocks/width.js +0 -70
- 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 -172
- package/dist/keymaps/standard.d.ts +0 -17
- package/dist/keymaps/standard.js +0 -175
- 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 -291
- package/dist/launch.d.ts +0 -149
- package/dist/launch.js +0 -446
- package/dist/sse.d.ts +0 -109
- package/dist/sse.js +0 -401
- package/dist/theme.d.ts +0 -51
- package/dist/theme.js +0 -119
- package/dist/ui/ambient.d.ts +0 -81
- package/dist/ui/ambient.js +0 -301
- package/dist/ui/app.d.ts +0 -245
- package/dist/ui/app.js +0 -1325
- 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 -330
- package/dist/ui/picker.d.ts +0 -26
- package/dist/ui/picker.js +0 -130
- package/dist/ui/sidebar.d.ts +0 -99
- package/dist/ui/sidebar.js +0 -269
- package/dist/ui/status-bar.d.ts +0 -77
- package/dist/ui/status-bar.js +0 -146
package/dist/sse.js
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The TUI's §5 conversation stream watcher (Task 4; delta frames in core
|
|
3
|
-
* hardening W3 Task 3, spec §7.3 ruling 11): a Node-side SSE client over
|
|
4
|
-
* `GET /v1/conversations/:id/stream` carrying `snapshot` and `delta` frames.
|
|
5
|
-
*
|
|
6
|
-
* This watcher is the TUI's delta folder: `snapshot` REPLACES the held view
|
|
7
|
-
* wholesale, `delta` folds into it via the shared `applyDelta`, and every
|
|
8
|
-
* accepted frame surfaces to the consumer as a FULL immutable
|
|
9
|
-
* `ConversationViewWire` revision — the consumer replaces, never
|
|
10
|
-
* accumulates. The server sends a fresh `snapshot` on EVERY connect, so a
|
|
11
|
-
* reconnect's snapshot SUBSUMES any frame missed while disconnected — no
|
|
12
|
-
* replay, no stale merge (Review Focus 1).
|
|
13
|
-
*
|
|
14
|
-
* Node 22 global fetch + manual frame parsing: `:` lines are comments,
|
|
15
|
-
* `data:` lines accumulate until a blank line dispatches the frame;
|
|
16
|
-
* multi-line `data:` joins with `\n`.
|
|
17
|
-
*
|
|
18
|
-
* Transport failures close the reader and reconnect with exponential
|
|
19
|
-
* backoff (base 250ms doubling, capped at 4s, ±30% jitter). Reconnects
|
|
20
|
-
* give up after 10 consecutive failures with ONE final `onError`; the
|
|
21
|
-
* counter resets on every successful open and at watch start. Transport
|
|
22
|
-
* failures (read error, mid-stream close) emit at most one `onError` per
|
|
23
|
-
* connection (a read failure and the ensuing close are the same failure);
|
|
24
|
-
* a non-OK status on (re)connect reports per attempt — it never opens a
|
|
25
|
-
* connection. A malformed frame reports
|
|
26
|
-
* `onError` and the stream STAYS OPEN — one bad frame must never kill the
|
|
27
|
-
* transport.
|
|
28
|
-
*/
|
|
29
|
-
import { applyDelta } from "@sigrun/shared/view-delta";
|
|
30
|
-
/** Parse a frame; `undefined` when the payload is not an object. */
|
|
31
|
-
function parseFrame(data) {
|
|
32
|
-
let parsed;
|
|
33
|
-
try {
|
|
34
|
-
parsed = JSON.parse(data);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
return parsed;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* An incremental SSE frame parser: feed it chunks; it emits complete
|
|
46
|
-
* frames (`{event, data}`). `:` lines are comments; `data:` accumulates
|
|
47
|
-
* until a blank line; the field-name space after `:` is stripped once.
|
|
48
|
-
*/
|
|
49
|
-
function createFrameParser(emit) {
|
|
50
|
-
let buffer = "";
|
|
51
|
-
let event = "";
|
|
52
|
-
let data = [];
|
|
53
|
-
function dispatchLine(line) {
|
|
54
|
-
if (line === "") {
|
|
55
|
-
if (data.length > 0) {
|
|
56
|
-
emit({ event, data: data.join("\n") });
|
|
57
|
-
}
|
|
58
|
-
event = "";
|
|
59
|
-
data = [];
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (line.startsWith(":")) {
|
|
63
|
-
return; // a comment (the heartbeat `: ping`)
|
|
64
|
-
}
|
|
65
|
-
const colon = line.indexOf(":");
|
|
66
|
-
const field = colon === -1 ? line : line.slice(0, colon);
|
|
67
|
-
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
68
|
-
if (value.startsWith(" ")) {
|
|
69
|
-
value = value.slice(1);
|
|
70
|
-
}
|
|
71
|
-
if (field === "event") {
|
|
72
|
-
event = value;
|
|
73
|
-
}
|
|
74
|
-
else if (field === "data") {
|
|
75
|
-
data.push(value);
|
|
76
|
-
}
|
|
77
|
-
// `id:` (and unknown fields) are accepted and ignored — every frame
|
|
78
|
-
// is a full revision, so the revision cursor is never needed.
|
|
79
|
-
}
|
|
80
|
-
return (chunk) => {
|
|
81
|
-
buffer += chunk;
|
|
82
|
-
for (;;) {
|
|
83
|
-
const newline = buffer.indexOf("\n");
|
|
84
|
-
if (newline === -1)
|
|
85
|
-
return;
|
|
86
|
-
let line = buffer.slice(0, newline);
|
|
87
|
-
buffer = buffer.slice(newline + 1);
|
|
88
|
-
if (line.endsWith("\r")) {
|
|
89
|
-
line = line.slice(0, -1);
|
|
90
|
-
}
|
|
91
|
-
dispatchLine(line);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Watch one conversation's stream. Returns `unwatch()`. Reconnects on
|
|
97
|
-
* stream drop; a reconnect's fresh snapshot replaces state wholesale.
|
|
98
|
-
*/
|
|
99
|
-
export function watchConversation(conversationId, handlers, options) {
|
|
100
|
-
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
101
|
-
const baseMs = options.reconnectBaseMs ?? 250;
|
|
102
|
-
const maxMs = options.reconnectMaxMs ?? 4_000;
|
|
103
|
-
/** Give up after this many consecutive failed reconnects. */
|
|
104
|
-
const maxAttempts = 10;
|
|
105
|
-
const user = options.user?.trim();
|
|
106
|
-
const token = options.token?.trim();
|
|
107
|
-
const headers = {
|
|
108
|
-
accept: "text/event-stream",
|
|
109
|
-
...(user === undefined || user === "" ? {} : { "x-sigrun-user": user }),
|
|
110
|
-
...(token === undefined || token === ""
|
|
111
|
-
? {}
|
|
112
|
-
: { authorization: `Bearer ${token}` }),
|
|
113
|
-
};
|
|
114
|
-
const url = `${options.baseUrl}/v1/conversations/${conversationId}/stream`;
|
|
115
|
-
let closed = false;
|
|
116
|
-
let attempts = 0;
|
|
117
|
-
let timer;
|
|
118
|
-
let abort;
|
|
119
|
-
/** Per-attempt onError dedupe (fix wave A10); cleared on open. */
|
|
120
|
-
let reportedTransportError = false;
|
|
121
|
-
/**
|
|
122
|
-
* The held view: the last snapshot, folded with every accepted delta
|
|
123
|
-
* since. `undefined` before the first snapshot — a delta arriving then
|
|
124
|
-
* is malformed (there is nothing to fold into).
|
|
125
|
-
*/
|
|
126
|
-
let currentView;
|
|
127
|
-
function scheduleReconnect() {
|
|
128
|
-
attempts += 1;
|
|
129
|
-
const base = Math.min(baseMs * 2 ** (attempts - 1), maxMs);
|
|
130
|
-
// ±30% jitter: correlated drops must not reconnect in lockstep.
|
|
131
|
-
const delay = base * (1 + (Math.random() * 0.6 - 0.3));
|
|
132
|
-
timer = setTimeout(() => {
|
|
133
|
-
timer = undefined;
|
|
134
|
-
void connect();
|
|
135
|
-
}, delay);
|
|
136
|
-
}
|
|
137
|
-
async function connect() {
|
|
138
|
-
if (closed)
|
|
139
|
-
return;
|
|
140
|
-
if (attempts >= maxAttempts) {
|
|
141
|
-
// Give up (fix wave A3): the 10th consecutive failure was just
|
|
142
|
-
// reported by its caller; emit ONE final notice and stop — no
|
|
143
|
-
// 11th fetch fires.
|
|
144
|
-
handlers.onError?.(`giving up after ${maxAttempts} failed reconnects`);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
const controller = new AbortController();
|
|
148
|
-
abort = controller;
|
|
149
|
-
let response;
|
|
150
|
-
try {
|
|
151
|
-
response = await fetchImpl(url, {
|
|
152
|
-
method: "GET",
|
|
153
|
-
headers,
|
|
154
|
-
signal: controller.signal,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
if (closed)
|
|
159
|
-
return;
|
|
160
|
-
handlers.onError?.(`conversation stream connect failed: ${error.message}`);
|
|
161
|
-
scheduleReconnect();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (!response.ok || response.body === null) {
|
|
165
|
-
if (closed)
|
|
166
|
-
return;
|
|
167
|
-
handlers.onError?.(`conversation stream responded with status ${response.status}`);
|
|
168
|
-
scheduleReconnect();
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
attempts = 0;
|
|
172
|
-
// The open clears the per-attempt dedupe (fix wave A10): the next
|
|
173
|
-
// failed connection gets its own single onError.
|
|
174
|
-
reportedTransportError = false;
|
|
175
|
-
handlers.onOpen?.();
|
|
176
|
-
const parser = createFrameParser((frame) => {
|
|
177
|
-
if (closed)
|
|
178
|
-
return;
|
|
179
|
-
const parsed = parseFrame(frame.data);
|
|
180
|
-
if (parsed === undefined) {
|
|
181
|
-
handlers.onError?.(`malformed ${frame.event === "" ? "message" : frame.event} frame (not a view object)`);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (frame.event === "snapshot") {
|
|
185
|
-
// A fresh snapshot SUBSUMES anything missed — the fold re-bases
|
|
186
|
-
// on it wholesale; nothing here merges.
|
|
187
|
-
currentView = parsed;
|
|
188
|
-
handlers.onSnapshot?.(currentView);
|
|
189
|
-
}
|
|
190
|
-
else if (frame.event === "delta") {
|
|
191
|
-
if (currentView === undefined) {
|
|
192
|
-
handlers.onError?.("malformed delta frame (arrived before any snapshot)");
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
// Fold, then surface the FULL revision: the consumer's contract
|
|
196
|
-
// stays "replace, never accumulate".
|
|
197
|
-
currentView = applyDelta(currentView, parsed);
|
|
198
|
-
handlers.onView?.(currentView);
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
handlers.onError?.(`unknown stream event "${frame.event}"`);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
const decoder = new TextDecoder();
|
|
205
|
-
try {
|
|
206
|
-
const reader = response.body.getReader();
|
|
207
|
-
for (;;) {
|
|
208
|
-
const { done, value } = await reader.read();
|
|
209
|
-
if (done)
|
|
210
|
-
break;
|
|
211
|
-
parser(decoder.decode(value, { stream: true }));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
if (closed)
|
|
216
|
-
return;
|
|
217
|
-
// Dedupe (fix wave A10): a read failure and the close that
|
|
218
|
-
// follows it are ONE connection failure — one onError.
|
|
219
|
-
if (!reportedTransportError) {
|
|
220
|
-
reportedTransportError = true;
|
|
221
|
-
handlers.onError?.(`conversation stream read failed: ${error.message}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (!closed) {
|
|
225
|
-
if (!reportedTransportError) {
|
|
226
|
-
reportedTransportError = true;
|
|
227
|
-
handlers.onError?.("conversation stream closed by the server");
|
|
228
|
-
}
|
|
229
|
-
scheduleReconnect();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
void connect();
|
|
233
|
-
return () => {
|
|
234
|
-
if (closed)
|
|
235
|
-
return;
|
|
236
|
-
closed = true;
|
|
237
|
-
if (timer !== undefined) {
|
|
238
|
-
clearTimeout(timer);
|
|
239
|
-
timer = undefined;
|
|
240
|
-
}
|
|
241
|
-
abort?.abort();
|
|
242
|
-
abort = undefined;
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
/** Loose parse: the launch wiring only branches on `type`. */
|
|
246
|
-
function parseEvent(data) {
|
|
247
|
-
let parsed;
|
|
248
|
-
try {
|
|
249
|
-
parsed = JSON.parse(data);
|
|
250
|
-
}
|
|
251
|
-
catch {
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
if (typeof parsed !== "object" ||
|
|
255
|
-
parsed === null ||
|
|
256
|
-
Array.isArray(parsed) ||
|
|
257
|
-
typeof parsed.type !== "string") {
|
|
258
|
-
return undefined;
|
|
259
|
-
}
|
|
260
|
-
return parsed;
|
|
261
|
-
}
|
|
262
|
-
/** Subscribe to the global events stream. Returns the unsubscribe. */
|
|
263
|
-
export function watchEvents(handlers, options) {
|
|
264
|
-
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
265
|
-
const baseMs = options.reconnectBaseMs ?? 250;
|
|
266
|
-
const maxMs = options.reconnectMaxMs ?? 4_000;
|
|
267
|
-
const maxAttempts = 10;
|
|
268
|
-
const user = options.user?.trim();
|
|
269
|
-
const token = options.token?.trim();
|
|
270
|
-
const headers = {
|
|
271
|
-
accept: "text/event-stream",
|
|
272
|
-
...(user === undefined || user === "" ? {} : { "x-sigrun-user": user }),
|
|
273
|
-
...(token === undefined || token === ""
|
|
274
|
-
? {}
|
|
275
|
-
: { authorization: `Bearer ${token}` }),
|
|
276
|
-
};
|
|
277
|
-
const url = `${options.baseUrl}/v1/events`;
|
|
278
|
-
let closed = false;
|
|
279
|
-
let attempts = 0;
|
|
280
|
-
let timer;
|
|
281
|
-
let abort;
|
|
282
|
-
let reportedTransportError = false;
|
|
283
|
-
function scheduleReconnect() {
|
|
284
|
-
attempts += 1;
|
|
285
|
-
const base = Math.min(baseMs * 2 ** (attempts - 1), maxMs);
|
|
286
|
-
const delay = base * (1 + (Math.random() * 0.6 - 0.3));
|
|
287
|
-
timer = setTimeout(() => {
|
|
288
|
-
timer = undefined;
|
|
289
|
-
void connect();
|
|
290
|
-
}, delay);
|
|
291
|
-
}
|
|
292
|
-
async function connect() {
|
|
293
|
-
if (closed)
|
|
294
|
-
return;
|
|
295
|
-
if (attempts >= maxAttempts) {
|
|
296
|
-
handlers.onError?.(`events stream: giving up after ${maxAttempts} failed reconnects`);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
const controller = new AbortController();
|
|
300
|
-
abort = controller;
|
|
301
|
-
let response;
|
|
302
|
-
try {
|
|
303
|
-
response = await fetchImpl(url, {
|
|
304
|
-
method: "GET",
|
|
305
|
-
headers,
|
|
306
|
-
signal: controller.signal,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
if (closed)
|
|
311
|
-
return;
|
|
312
|
-
handlers.onError?.(`events stream connect failed: ${error.message}`);
|
|
313
|
-
scheduleReconnect();
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
if (!response.ok || response.body === null) {
|
|
317
|
-
if (closed)
|
|
318
|
-
return;
|
|
319
|
-
handlers.onError?.(`events stream responded with status ${response.status}`);
|
|
320
|
-
scheduleReconnect();
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
attempts = 0;
|
|
324
|
-
reportedTransportError = false;
|
|
325
|
-
handlers.onOpen?.();
|
|
326
|
-
const parser = createFrameParser((frame) => {
|
|
327
|
-
if (closed)
|
|
328
|
-
return;
|
|
329
|
-
// The hello frame (U3 Task 4) is connection-local: it names
|
|
330
|
-
// THIS stream for the watcher registry — surface fetches send
|
|
331
|
-
// the id as `X-Sigrun-Stream`; a NEW id (reconnect) re-fires
|
|
332
|
-
// so the channel re-registers. It never reaches onEvent (the
|
|
333
|
-
// launch wiring's switch would ignore it anyway — event types
|
|
334
|
-
// are an open set). Parsed loosely: the hello payload carries
|
|
335
|
-
// `streamId`, not a `type` member.
|
|
336
|
-
if (frame.event === "hello") {
|
|
337
|
-
let parsed;
|
|
338
|
-
try {
|
|
339
|
-
parsed = JSON.parse(frame.data);
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
handlers.onError?.("malformed hello frame (not JSON)");
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
// SAFETY: the JSON.parse output is unknown; the read below
|
|
346
|
-
// narrows `streamId` by typeof before use.
|
|
347
|
-
const streamId = parsed.streamId;
|
|
348
|
-
if (typeof streamId === "string") {
|
|
349
|
-
handlers.onHello?.(streamId);
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
handlers.onError?.("malformed hello frame (no streamId)");
|
|
353
|
-
}
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
const event = parseEvent(frame.data);
|
|
357
|
-
if (event === undefined) {
|
|
358
|
-
handlers.onError?.(`malformed ${frame.event === "" ? "message" : frame.event} frame (not an event object)`);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
handlers.onEvent(event);
|
|
362
|
-
});
|
|
363
|
-
const decoder = new TextDecoder();
|
|
364
|
-
try {
|
|
365
|
-
const reader = response.body.getReader();
|
|
366
|
-
for (;;) {
|
|
367
|
-
const { done, value } = await reader.read();
|
|
368
|
-
if (done)
|
|
369
|
-
break;
|
|
370
|
-
parser(decoder.decode(value, { stream: true }));
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
catch (error) {
|
|
374
|
-
if (closed)
|
|
375
|
-
return;
|
|
376
|
-
if (!reportedTransportError) {
|
|
377
|
-
reportedTransportError = true;
|
|
378
|
-
handlers.onError?.(`events stream read failed: ${error.message}`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (!closed) {
|
|
382
|
-
if (!reportedTransportError) {
|
|
383
|
-
reportedTransportError = true;
|
|
384
|
-
handlers.onError?.("events stream closed by the server");
|
|
385
|
-
}
|
|
386
|
-
scheduleReconnect();
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
void connect();
|
|
390
|
-
return () => {
|
|
391
|
-
if (closed)
|
|
392
|
-
return;
|
|
393
|
-
closed = true;
|
|
394
|
-
if (timer !== undefined) {
|
|
395
|
-
clearTimeout(timer);
|
|
396
|
-
timer = undefined;
|
|
397
|
-
}
|
|
398
|
-
abort?.abort();
|
|
399
|
-
abort = undefined;
|
|
400
|
-
};
|
|
401
|
-
}
|
package/dist/theme.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The TUI theme (Task C1, UI refresh): wraps the shared themes module's
|
|
3
|
-
* palettes in pi-tui's color pipeline (parseColor → foregroundAnsi /
|
|
4
|
-
* backgroundAnsi) so components style text through token names instead
|
|
5
|
-
* of raw SGR sequences. The palette DATA lives in one place
|
|
6
|
-
* (@sigrun/shared/themes, shared with the webui — zero duplication);
|
|
7
|
-
* this module owns only the terminal mapping.
|
|
8
|
-
*
|
|
9
|
-
* Degradation: `colorMode` threads through foregroundAnsi/backgroundAnsi,
|
|
10
|
-
* so a 256color terminal gets `38;5;N` (pi-tui's nearest-256 mapping)
|
|
11
|
-
* instead of truecolor `38;2;r;g;b`. The launcher resolves the mode at
|
|
12
|
-
* boot via `getTerminalColorMode()`; tests pass it explicitly.
|
|
13
|
-
*
|
|
14
|
-
* The `code` token is CSS `rgba(...)` (the shared data's overlay chip):
|
|
15
|
-
* pi-tui's parseColor takes hex/`rgb()` only, so it is alpha-composited
|
|
16
|
-
* over the mode's `bg` token here — the same visual the webui gets from
|
|
17
|
-
* layering the overlay on the background.
|
|
18
|
-
*/
|
|
19
|
-
import { type TerminalColorMode } from "@earendil-works/pi-tui";
|
|
20
|
-
import { type AccentName, type ModeName, type PaletteName, type PaletteTokens } from "@sigrun/shared/themes";
|
|
21
|
-
/** A palette token, the accent colour, or a state colour (warn/bad). */
|
|
22
|
-
export type TuiToken = keyof PaletteTokens | "accent" | "warn" | "bad";
|
|
23
|
-
/**
|
|
24
|
-
* Terminal styling surface: token-named fg/bg plus the plain attribute
|
|
25
|
-
* helpers. `fg`/`bg` return string wrappers so call sites compose
|
|
26
|
-
* (`theme.fg("muted")(text)`) without holding SGR sequences.
|
|
27
|
-
*/
|
|
28
|
-
export interface TuiTheme {
|
|
29
|
-
fg(token: TuiToken): (s: string) => string;
|
|
30
|
-
bg(token: TuiToken): (s: string) => string;
|
|
31
|
-
bold(s: string): string;
|
|
32
|
-
dim(s: string): string;
|
|
33
|
-
italic(s: string): string;
|
|
34
|
-
}
|
|
35
|
-
export interface TuiThemeOptions {
|
|
36
|
-
readonly palette: PaletteName;
|
|
37
|
-
readonly accent: AccentName;
|
|
38
|
-
readonly mode: ModeName;
|
|
39
|
-
readonly colorMode: TerminalColorMode;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Build the theme. Precomputes each token's fg/bg SGR (pi-tui's cached
|
|
43
|
-
* sequence idiom — `styleTextWithAnsi` without the dependency) and wraps
|
|
44
|
-
* it in reset-terminated spans.
|
|
45
|
-
*/
|
|
46
|
-
export declare function createTuiTheme(options: TuiThemeOptions): TuiTheme;
|
|
47
|
-
/**
|
|
48
|
-
* The no-colour fallback (tests, non-TTY sinks): every method is the
|
|
49
|
-
* identity — the structure renders identically without SGR sequences.
|
|
50
|
-
*/
|
|
51
|
-
export declare function plainTuiTheme(): TuiTheme;
|
package/dist/theme.js
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The TUI theme (Task C1, UI refresh): wraps the shared themes module's
|
|
3
|
-
* palettes in pi-tui's color pipeline (parseColor → foregroundAnsi /
|
|
4
|
-
* backgroundAnsi) so components style text through token names instead
|
|
5
|
-
* of raw SGR sequences. The palette DATA lives in one place
|
|
6
|
-
* (@sigrun/shared/themes, shared with the webui — zero duplication);
|
|
7
|
-
* this module owns only the terminal mapping.
|
|
8
|
-
*
|
|
9
|
-
* Degradation: `colorMode` threads through foregroundAnsi/backgroundAnsi,
|
|
10
|
-
* so a 256color terminal gets `38;5;N` (pi-tui's nearest-256 mapping)
|
|
11
|
-
* instead of truecolor `38;2;r;g;b`. The launcher resolves the mode at
|
|
12
|
-
* boot via `getTerminalColorMode()`; tests pass it explicitly.
|
|
13
|
-
*
|
|
14
|
-
* The `code` token is CSS `rgba(...)` (the shared data's overlay chip):
|
|
15
|
-
* pi-tui's parseColor takes hex/`rgb()` only, so it is alpha-composited
|
|
16
|
-
* over the mode's `bg` token here — the same visual the webui gets from
|
|
17
|
-
* layering the overlay on the background.
|
|
18
|
-
*/
|
|
19
|
-
import { backgroundAnsi, foregroundAnsi, parseColor, } from "@earendil-works/pi-tui";
|
|
20
|
-
import { ACCENTS, PALETTES, } from "@sigrun/shared/themes";
|
|
21
|
-
const RESET = "\u001b[0m";
|
|
22
|
-
const attr = (code, s) => `\u001b[${code}m${s}${RESET}`;
|
|
23
|
-
/** Alpha-composite a CSS `rgba(r,g,b,a)` over an opaque base. */
|
|
24
|
-
function compositeOver(rgba, base) {
|
|
25
|
-
const m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/.exec(rgba.replace(/\s/g, ""));
|
|
26
|
-
if (!m)
|
|
27
|
-
return base;
|
|
28
|
-
const [, rs, gs, bs, as = "1"] = m;
|
|
29
|
-
const [r, g, b, a] = [Number(rs), Number(gs), Number(bs), Number(as)];
|
|
30
|
-
const [br, bg, bb] = hexToRgb(base);
|
|
31
|
-
const mix = (fg, bgv) => Math.round(fg * a + bgv * (1 - a));
|
|
32
|
-
return rgbToHex(mix(r, br), mix(g, bg), mix(b, bb));
|
|
33
|
-
}
|
|
34
|
-
function hexToRgb(hex) {
|
|
35
|
-
const value = hex.replace("#", "");
|
|
36
|
-
return [
|
|
37
|
-
parseInt(value.slice(0, 2), 16),
|
|
38
|
-
parseInt(value.slice(2, 4), 16),
|
|
39
|
-
parseInt(value.slice(4, 6), 16),
|
|
40
|
-
];
|
|
41
|
-
}
|
|
42
|
-
function rgbToHex(r, g, b) {
|
|
43
|
-
const two = (n) => n.toString(16).padStart(2, "0");
|
|
44
|
-
return `#${two(r)}${two(g)}${two(b)}`;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* The state colours (warnings + errors), defined per mode like the
|
|
48
|
-
* webui's `--warn`/`--danger` (webui/src/app.css — they are NOT palette
|
|
49
|
-
* members in either channel). The TUI's tone table (blocks/tones.ts)
|
|
50
|
-
* paints `warn`/`bad` tones with them.
|
|
51
|
-
*/
|
|
52
|
-
const STATE_COLORS = {
|
|
53
|
-
warn: { dark: "#d9a35a", light: "#8a6116" },
|
|
54
|
-
bad: { dark: "#e0707a", light: "#b3363f" },
|
|
55
|
-
};
|
|
56
|
-
/** Resolve a token to a parseable colour for the palette+mode. */
|
|
57
|
-
function resolveToken(tokens, accentHex, mode, token) {
|
|
58
|
-
if (token === "accent")
|
|
59
|
-
return accentHex;
|
|
60
|
-
if (token === "warn" || token === "bad")
|
|
61
|
-
return STATE_COLORS[token][mode];
|
|
62
|
-
const value = tokens[token];
|
|
63
|
-
// `code` is the rgba overlay chip; composite over the mode's bg.
|
|
64
|
-
if (value.startsWith("rgba"))
|
|
65
|
-
return compositeOver(value, tokens.bg);
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Build the theme. Precomputes each token's fg/bg SGR (pi-tui's cached
|
|
70
|
-
* sequence idiom — `styleTextWithAnsi` without the dependency) and wraps
|
|
71
|
-
* it in reset-terminated spans.
|
|
72
|
-
*/
|
|
73
|
-
export function createTuiTheme(options) {
|
|
74
|
-
const tokens = PALETTES[options.palette][options.mode];
|
|
75
|
-
const accentHex = ACCENTS[options.accent][options.mode];
|
|
76
|
-
const mode = options.colorMode;
|
|
77
|
-
const fgSequences = new Map();
|
|
78
|
-
const bgSequences = new Map();
|
|
79
|
-
for (const token of [
|
|
80
|
-
"bg",
|
|
81
|
-
"surface",
|
|
82
|
-
"surface2",
|
|
83
|
-
"border",
|
|
84
|
-
"text",
|
|
85
|
-
"muted",
|
|
86
|
-
"faint",
|
|
87
|
-
"ok",
|
|
88
|
-
"code",
|
|
89
|
-
"accent",
|
|
90
|
-
"warn",
|
|
91
|
-
"bad",
|
|
92
|
-
]) {
|
|
93
|
-
const color = parseColor(resolveToken(tokens, accentHex, options.mode, token));
|
|
94
|
-
fgSequences.set(token, foregroundAnsi(color, mode));
|
|
95
|
-
bgSequences.set(token, backgroundAnsi(color, mode));
|
|
96
|
-
}
|
|
97
|
-
const span = (seq, s) => `${seq}${s}${RESET}`;
|
|
98
|
-
return {
|
|
99
|
-
fg: (token) => (s) => span(fgSequences.get(token) ?? "", s),
|
|
100
|
-
bg: (token) => (s) => span(bgSequences.get(token) ?? "", s),
|
|
101
|
-
bold: (s) => attr("1", s),
|
|
102
|
-
dim: (s) => attr("2", s),
|
|
103
|
-
italic: (s) => attr("3", s),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* The no-colour fallback (tests, non-TTY sinks): every method is the
|
|
108
|
-
* identity — the structure renders identically without SGR sequences.
|
|
109
|
-
*/
|
|
110
|
-
export function plainTuiTheme() {
|
|
111
|
-
const identity = (s) => s;
|
|
112
|
-
return {
|
|
113
|
-
fg: () => identity,
|
|
114
|
-
bg: () => identity,
|
|
115
|
-
bold: identity,
|
|
116
|
-
dim: identity,
|
|
117
|
-
italic: identity,
|
|
118
|
-
};
|
|
119
|
-
}
|
package/dist/ui/ambient.d.ts
DELETED
|
@@ -1,81 +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
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
45
|
-
import type { AmbientWire, SigrunClient } from "../api.js";
|
|
46
|
-
export declare class AmbientChrome implements Component {
|
|
47
|
-
#private;
|
|
48
|
-
/** Rendered-state changes (the app requests a repaint). */
|
|
49
|
-
onChanged?: () => void;
|
|
50
|
-
constructor(client: SigrunClient);
|
|
51
|
-
/** The latest served surfaces (before the idle filter). */
|
|
52
|
-
get records(): readonly AmbientWire[];
|
|
53
|
-
/**
|
|
54
|
-
* Retarget the chrome: reset state and fetch immediately (the brief's
|
|
55
|
-
* "fetch on connect"). `undefined` clears the chrome. (W3 Task 4: no
|
|
56
|
-
* timer cadence — `ambient.changed` host events own the refreshes.)
|
|
57
|
-
*/
|
|
58
|
-
setConversation(id: number | undefined): void;
|
|
59
|
-
/**
|
|
60
|
-
* Refetch when an `ambient.changed` host event names the LIVE
|
|
61
|
-
* conversation (the app's events wiring calls this; other
|
|
62
|
-
* conversations' events are ignored).
|
|
63
|
-
*/
|
|
64
|
-
kick(conversationId: number): void;
|
|
65
|
-
/**
|
|
66
|
-
* The conversation's run state (the app feeds it from stream frames).
|
|
67
|
-
* `expires: "idle"` surfaces render only while busy.
|
|
68
|
-
*/
|
|
69
|
-
setBusy(busy: boolean): void;
|
|
70
|
-
/** The statusline: the status records' summaries joined " · ". */
|
|
71
|
-
statusline(): string;
|
|
72
|
-
/** The footer zone: one summary line per footer record. */
|
|
73
|
-
footerLines(): readonly string[];
|
|
74
|
-
/** The panel zone: the full per-record rendering, blocks joined. */
|
|
75
|
-
panelLines(): readonly string[];
|
|
76
|
-
/** Detach (the app calls this on dispose): stale fetches never land. */
|
|
77
|
-
dispose(): void;
|
|
78
|
-
/** The chrome: panel blocks, then footer lines, then the statusline. */
|
|
79
|
-
render(_width: number): string[];
|
|
80
|
-
invalidate(): void;
|
|
81
|
-
}
|