camouflage-tui 2.1.0-beta.1 → 2.2.2-beta.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/README.md +28 -2
- package/package.json +1 -1
- package/src/__fake-argv.js +8 -0
- package/src/binding.d.ts +18 -3
- package/src/binding.js +78 -51
- package/src/binding.test.js +49 -3
- package/src/index.d.ts +9 -0
- package/src/index.js +1 -1
- package/src/types.d.ts +83 -5
- package/src/types.js +2 -1
package/README.md
CHANGED
|
@@ -15,9 +15,9 @@ The `postinstall` script downloads a pre-built native binary for your platform (
|
|
|
15
15
|
```js
|
|
16
16
|
import { mount } from "camouflage-tui";
|
|
17
17
|
|
|
18
|
-
const cam = await mount();
|
|
18
|
+
const cam = await mount({ ui: "inline" });
|
|
19
19
|
|
|
20
|
-
cam.send("SessionStarted", {});
|
|
20
|
+
cam.send("SessionStarted", { title: "my-agent", detail: ["model · ~/project"], accent: "orange" });
|
|
21
21
|
cam.send("UserMessageCreated", { text: "investigate failing test" });
|
|
22
22
|
cam.send("AssistantStreamStarted", { stream_id: "s1" });
|
|
23
23
|
cam.send("AssistantTokenDelta", { stream_id: "s1", token: "Looking " });
|
|
@@ -51,9 +51,35 @@ interface MountOptions {
|
|
|
51
51
|
env?: NodeJS.ProcessEnv; // merged with process.env
|
|
52
52
|
inheritStderr?: boolean; // default true; false → "stderr" event
|
|
53
53
|
renderToTerminal?: boolean; // true → stdout goes to terminal, responses on fd 3
|
|
54
|
+
ui?: "inline" | "fullscreen"; // default "fullscreen"; see below
|
|
54
55
|
}
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
`ui: "inline"` prints finished output into the terminal's normal scrollback
|
|
59
|
+
and redraws only a small live region (the reply being streamed, the
|
|
60
|
+
spinner, the input box). The transcript stays after exit, text selection
|
|
61
|
+
works normally, and an idle renderer uses no CPU. It draws on the terminal
|
|
62
|
+
(`/dev/tty`) even in the default piped mode, so it works with a plain
|
|
63
|
+
`mount({ ui: "inline" })`.
|
|
64
|
+
|
|
65
|
+
### Helpers that wait for the user
|
|
66
|
+
|
|
67
|
+
`selectList`, `confirm`, `permission`, `form` and `wizard` send a prompt and
|
|
68
|
+
resolve with the user's answer. If the renderer exits first they resolve
|
|
69
|
+
as cancelled (`permission` resolves `{ choice: "deny" }`), so a host never
|
|
70
|
+
hangs on a dead renderer.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { permission } from "camouflage-tui";
|
|
74
|
+
|
|
75
|
+
const { choice } = await permission(cam, {
|
|
76
|
+
request_id: "r1",
|
|
77
|
+
tool: "edit",
|
|
78
|
+
action: "edit src/auth/session.ts",
|
|
79
|
+
diff: { path: "src/auth/session.ts", before, after },
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
57
83
|
### `CamouflageHandle`
|
|
58
84
|
|
|
59
85
|
Extends `EventEmitter`. Send events with `send()`; subscribe to outbound events with `on()`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "camouflage-tui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2-beta.1",
|
|
4
4
|
"description": "High-performance terminal renderer for AI agent applications. A React Ink alternative built for streaming, persistence, and replay.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Test harness only: reports the arguments it was started with as one
|
|
3
|
+
// UserInputSubmitted event, so tests can check what mount() passes.
|
|
4
|
+
process.stdout.write(
|
|
5
|
+
JSON.stringify({ event_type: "UserInputSubmitted", payload: { text: process.argv.slice(2).join(" ") } }) + "\n",
|
|
6
|
+
);
|
|
7
|
+
process.stdin.resume();
|
|
8
|
+
process.stdin.on("end", () => process.exit(0));
|
package/src/binding.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type { Event } from "./types.js";
|
|
2
|
+
import type { Event, EventType, PayloadOf, PermissionRequested, TodoItem } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export interface MountOptions {
|
|
5
5
|
/** Executable name or path. Defaults to "camouflage-tui" (PATH lookup). */
|
|
@@ -28,6 +28,11 @@ export interface MountOptions {
|
|
|
28
28
|
* compatible programmatic mode where both directions ride on the
|
|
29
29
|
* pipes the binding manages. */
|
|
30
30
|
renderToTerminal?: boolean;
|
|
31
|
+
/** Renderer UI. `"inline"` prints finished output into the terminal's
|
|
32
|
+
* normal scrollback and redraws only a small live region at the bottom;
|
|
33
|
+
* it draws on the terminal even in the default piped mode. Default
|
|
34
|
+
* `"fullscreen"` (the v2 alternate-screen UI). */
|
|
35
|
+
ui?: "inline" | "fullscreen";
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export interface PermissionResponseEvent {
|
|
@@ -188,9 +193,10 @@ export interface ExitEvent {
|
|
|
188
193
|
|
|
189
194
|
export interface CamouflageHandle extends EventEmitter {
|
|
190
195
|
/** Send one event INTO the renderer. */
|
|
191
|
-
|
|
196
|
+
/** Send one event to the renderer. The payload is typed per event. */
|
|
197
|
+
send<T extends EventType>(event_type: T, payload?: PayloadOf<T>): boolean;
|
|
192
198
|
/** Send a pre-built Event object. */
|
|
193
|
-
sendEvent(ev:
|
|
199
|
+
sendEvent(ev: Event): boolean;
|
|
194
200
|
/** Gracefully close: end stdin, wait for child exit, resolve with code. */
|
|
195
201
|
close(): Promise<number>;
|
|
196
202
|
/** Force-kill the renderer. */
|
|
@@ -226,3 +232,12 @@ export interface CamouflageHandle extends EventEmitter {
|
|
|
226
232
|
* await cam.close();
|
|
227
233
|
*/
|
|
228
234
|
export function mount(opts?: MountOptions): Promise<CamouflageHandle>;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Ask the user for permission and resolve to their answer. Resolves with
|
|
238
|
+
* `{ choice: "deny" }` if the renderer exits first, so a caller never hangs.
|
|
239
|
+
*/
|
|
240
|
+
export function permission(cam: CamouflageHandle, spec: PermissionRequested): Promise<PermissionResponseEvent>;
|
|
241
|
+
|
|
242
|
+
/** Replace the agent's plan checklist. Pass an empty array to clear it. */
|
|
243
|
+
export function tasksSet(cam: CamouflageHandle, todos: TodoItem[]): void;
|
package/src/binding.js
CHANGED
|
@@ -231,7 +231,7 @@ export async function mount(opts = {}) {
|
|
|
231
231
|
defaultArgs = ["--stdin-events", "--responses-fd", "3"];
|
|
232
232
|
stdio = ["pipe", "inherit", "inherit", "pipe"];
|
|
233
233
|
} else {
|
|
234
|
-
defaultArgs = ["--stdin-events", "--emit-responses"];
|
|
234
|
+
defaultArgs = ["--stdin-events", "--emit-responses=true"];
|
|
235
235
|
stdio = ["pipe", "pipe", stderrMode];
|
|
236
236
|
}
|
|
237
237
|
// Brand the header with the host app's name (explicit > auto-detected).
|
|
@@ -245,7 +245,8 @@ export async function mount(opts = {}) {
|
|
|
245
245
|
titleArgs.push("--app-title", title);
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
|
-
const
|
|
248
|
+
const uiArgs = opts.ui && !opts.skipDefaultArgs && !userArgs.includes("--ui") ? ["--ui", opts.ui] : [];
|
|
249
|
+
const args = [...defaultArgs, ...uiArgs, ...titleArgs, ...userArgs];
|
|
249
250
|
|
|
250
251
|
const child = spawn(bin, args, {
|
|
251
252
|
stdio,
|
|
@@ -267,6 +268,13 @@ export async function mount(opts = {}) {
|
|
|
267
268
|
|
|
268
269
|
const handle = new CamouflageHandle(child, child.stdin);
|
|
269
270
|
|
|
271
|
+
// If the renderer exits, writes to its stdin fail with EPIPE. Unhandled,
|
|
272
|
+
// that 'error' event would crash the host process; treat it as closed.
|
|
273
|
+
child.stdin.on("error", (err) => {
|
|
274
|
+
handle._closed = true;
|
|
275
|
+
if (err && err.code !== "EPIPE") handle.emit("invalid", { line: "", error: String(err) });
|
|
276
|
+
});
|
|
277
|
+
|
|
270
278
|
// Stream outbound events (UserInputSubmitted, PermissionResponse) from
|
|
271
279
|
// whichever stream the renderer is writing them to. In renderToTerminal
|
|
272
280
|
// mode that's fd 3 (child.stdio[3]); otherwise it's stdout.
|
|
@@ -352,45 +360,82 @@ export async function mount(opts = {}) {
|
|
|
352
360
|
return handle;
|
|
353
361
|
}
|
|
354
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Resolve with the first `eventName` response whose id matches, or with
|
|
365
|
+
* `onExit` if the renderer exits first, so callers never hang on a dead
|
|
366
|
+
* renderer.
|
|
367
|
+
*/
|
|
368
|
+
function awaitResponse(cam, eventName, matches, onExit) {
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
if (cam._closed) {
|
|
371
|
+
resolve(onExit);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const cleanup = () => {
|
|
375
|
+
cam.off(eventName, listener);
|
|
376
|
+
cam.off("exit", exitListener);
|
|
377
|
+
};
|
|
378
|
+
const listener = (resp) => {
|
|
379
|
+
if (!matches(resp)) return;
|
|
380
|
+
cleanup();
|
|
381
|
+
resolve(resp);
|
|
382
|
+
};
|
|
383
|
+
const exitListener = () => {
|
|
384
|
+
cleanup();
|
|
385
|
+
resolve(onExit);
|
|
386
|
+
};
|
|
387
|
+
cam.on(eventName, listener);
|
|
388
|
+
cam.on("exit", exitListener);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
355
392
|
/**
|
|
356
393
|
* Convenience helper: emit a ShowSelectList and resolve to the user's
|
|
357
|
-
* SelectListResponse for that id.
|
|
358
|
-
*
|
|
394
|
+
* SelectListResponse for that id. Resolves `{ cancelled: true }` if the
|
|
395
|
+
* renderer exits first.
|
|
359
396
|
*
|
|
360
397
|
* @param {CamouflageHandle} cam
|
|
361
398
|
* @param {{id: string, prompt: string, options: object[], default?: string, allow_filter?: boolean, allow_cancel?: boolean}} spec
|
|
362
399
|
* @returns {Promise<{id: string, value?: string, cancelled: boolean}>}
|
|
363
400
|
*/
|
|
364
401
|
export function selectList(cam, spec) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
cam.off("selectListResponse", listener);
|
|
369
|
-
resolve(resp);
|
|
370
|
-
};
|
|
371
|
-
cam.on("selectListResponse", listener);
|
|
372
|
-
cam.send("ShowSelectList", spec);
|
|
373
|
-
});
|
|
402
|
+
const done = awaitResponse(cam, "selectListResponse", (r) => r.id === spec.id, { id: spec.id, cancelled: true });
|
|
403
|
+
cam.send("ShowSelectList", spec);
|
|
404
|
+
return done;
|
|
374
405
|
}
|
|
375
406
|
|
|
376
407
|
/**
|
|
377
408
|
* Convenience helper: emit a ShowConfirm and resolve to the user's
|
|
378
|
-
* ConfirmResponse for that id.
|
|
409
|
+
* ConfirmResponse for that id. Resolves `{ cancelled: true }` if the
|
|
410
|
+
* renderer exits first.
|
|
379
411
|
*
|
|
380
412
|
* @param {CamouflageHandle} cam
|
|
381
413
|
* @param {{id: string, prompt: string, yes_label?: string, no_label?: string, default?: "yes"|"no", allow_cancel?: boolean}} spec
|
|
382
414
|
* @returns {Promise<{id: string, value?: boolean, cancelled: boolean}>}
|
|
383
415
|
*/
|
|
384
416
|
export function confirm(cam, spec) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
417
|
+
const done = awaitResponse(cam, "confirmResponse", (r) => r.id === spec.id, { id: spec.id, cancelled: true });
|
|
418
|
+
cam.send("ShowConfirm", spec);
|
|
419
|
+
return done;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Ask the user for permission and resolve to their answer. Resolves
|
|
424
|
+
* `{ choice: "deny" }` if the renderer exits first.
|
|
425
|
+
*
|
|
426
|
+
* @param {CamouflageHandle} cam
|
|
427
|
+
* @param {{request_id: string, tool: string, action: string, detail?: string, diff?: object}} spec
|
|
428
|
+
* @returns {Promise<{request_id: string, choice: "allow_once"|"allow_session"|"deny", feedback?: string}>}
|
|
429
|
+
*/
|
|
430
|
+
export function permission(cam, spec) {
|
|
431
|
+
const done = awaitResponse(
|
|
432
|
+
cam,
|
|
433
|
+
"permissionResponse",
|
|
434
|
+
(r) => r.request_id === spec.request_id,
|
|
435
|
+
{ request_id: spec.request_id, choice: "deny", feedback: "" },
|
|
436
|
+
);
|
|
437
|
+
cam.send("PermissionRequested", spec);
|
|
438
|
+
return done;
|
|
394
439
|
}
|
|
395
440
|
|
|
396
441
|
/**
|
|
@@ -422,15 +467,9 @@ export function keyValueView(cam, spec) {
|
|
|
422
467
|
* @returns {Promise<{id: string, values?: Record<string, string>, cancelled: boolean}>}
|
|
423
468
|
*/
|
|
424
469
|
export function form(cam, spec) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
cam.off("formResponse", listener);
|
|
429
|
-
resolve(resp);
|
|
430
|
-
};
|
|
431
|
-
cam.on("formResponse", listener);
|
|
432
|
-
cam.send("ShowForm", spec);
|
|
433
|
-
});
|
|
470
|
+
const done = awaitResponse(cam, "formResponse", (r) => r.id === spec.id, { id: spec.id, cancelled: true });
|
|
471
|
+
cam.send("ShowForm", spec);
|
|
472
|
+
return done;
|
|
434
473
|
}
|
|
435
474
|
|
|
436
475
|
/**
|
|
@@ -444,25 +483,13 @@ export function form(cam, spec) {
|
|
|
444
483
|
* @returns {Promise<{id: string, results?: Record<string, any>, cancelled?: boolean, at_step?: number}>}
|
|
445
484
|
*/
|
|
446
485
|
export function wizard(cam, spec) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (resp.id !== spec.id) return;
|
|
455
|
-
cleanup();
|
|
456
|
-
resolve({ id: resp.id, cancelled: true, at_step: resp.at_step });
|
|
457
|
-
};
|
|
458
|
-
const cleanup = () => {
|
|
459
|
-
cam.off("wizardCompleted", onCompleted);
|
|
460
|
-
cam.off("wizardCancelled", onCancelled);
|
|
461
|
-
};
|
|
462
|
-
cam.on("wizardCompleted", onCompleted);
|
|
463
|
-
cam.on("wizardCancelled", onCancelled);
|
|
464
|
-
cam.send("ShowWizard", spec);
|
|
465
|
-
});
|
|
486
|
+
const completed = awaitResponse(cam, "wizardCompleted", (r) => r.id === spec.id, null);
|
|
487
|
+
const cancelled = awaitResponse(cam, "wizardCancelled", (r) => r.id === spec.id, null);
|
|
488
|
+
cam.send("ShowWizard", spec);
|
|
489
|
+
return Promise.race([
|
|
490
|
+
completed.then((r) => r ?? { id: spec.id, cancelled: true, at_step: 0 }),
|
|
491
|
+
cancelled.then((r) => ({ id: spec.id, cancelled: true, at_step: r?.at_step ?? 0 })),
|
|
492
|
+
]);
|
|
466
493
|
}
|
|
467
494
|
|
|
468
495
|
/**
|
package/src/binding.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import { test } from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { mount, selectList, confirm } from "./index.js";
|
|
5
|
+
import { mount, selectList, confirm, permission } from "./index.js";
|
|
6
6
|
|
|
7
7
|
const FAKE = join(dirname(fileURLToPath(import.meta.url)), "__fake-renderer.js");
|
|
8
8
|
|
|
@@ -72,10 +72,12 @@ test("permissionResponse convenience event fires", async () => {
|
|
|
72
72
|
await cam.close();
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test("send()
|
|
75
|
+
test("send() after close() is a no-op that returns false", async () => {
|
|
76
|
+
// Hosts send a final StatusUpdate/SessionEnded during cleanup, often after
|
|
77
|
+
// the renderer has exited; throwing there would crash the host (496035a).
|
|
76
78
|
const cam = await mountFake();
|
|
77
79
|
await cam.close();
|
|
78
|
-
assert.
|
|
80
|
+
assert.equal(cam.send("SessionStarted", {}), false);
|
|
79
81
|
});
|
|
80
82
|
|
|
81
83
|
test("close() resolves with the child's exit code", async () => {
|
|
@@ -135,3 +137,47 @@ async function waitUntil(predicate, deadlineMs) {
|
|
|
135
137
|
await new Promise((r) => setTimeout(r, 10));
|
|
136
138
|
}
|
|
137
139
|
}
|
|
140
|
+
|
|
141
|
+
test("helpers settle instead of hanging when the renderer exits", async () => {
|
|
142
|
+
const cam = await mountFake();
|
|
143
|
+
const pending = [
|
|
144
|
+
selectList(cam, { id: "pick", prompt: "Pick", options: [] }),
|
|
145
|
+
confirm(cam, { id: "ok", prompt: "OK?" }),
|
|
146
|
+
permission(cam, { request_id: "r1", tool: "bash", action: "run npm test" }),
|
|
147
|
+
];
|
|
148
|
+
// The fake echoes inbound events back; kill it before anyone answers.
|
|
149
|
+
cam.kill("SIGKILL");
|
|
150
|
+
const [sel, conf, perm] = await Promise.all(pending);
|
|
151
|
+
assert.deepEqual(sel, { id: "pick", cancelled: true });
|
|
152
|
+
assert.deepEqual(conf, { id: "ok", cancelled: true });
|
|
153
|
+
assert.equal(perm.choice, "deny");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("permission() resolves with the user's answer", async () => {
|
|
157
|
+
const cam = await mountFake();
|
|
158
|
+
const answer = permission(cam, { request_id: "r2", tool: "edit", action: "edit a.ts" });
|
|
159
|
+
// Stand in for the user: the fake echoes this response back to us.
|
|
160
|
+
cam.send("PermissionResponse", { request_id: "r2", choice: "allow_session", feedback: "" });
|
|
161
|
+
assert.equal((await answer).choice, "allow_session");
|
|
162
|
+
await cam.close();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("ui option passes --ui to the renderer", async () => {
|
|
166
|
+
const fakeArgv = join(dirname(fileURLToPath(import.meta.url)), "__fake-argv.js");
|
|
167
|
+
const cam = await mount({ bin: fakeArgv, ui: "inline", appTitle: "demo" });
|
|
168
|
+
const text = await new Promise((resolve) => cam.on("userInput", resolve));
|
|
169
|
+
assert.match(text, /--ui inline/);
|
|
170
|
+
await cam.close();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a renderer that exits at startup doesn't crash the host", async () => {
|
|
174
|
+
const cam = await mount({ bin: process.execPath, args: ["-e", "process.exit(3)"], skipDefaultArgs: true });
|
|
175
|
+
const exited = new Promise((resolve) => cam.on("exit", resolve));
|
|
176
|
+
// Keep writing while (and after) the child dies; nothing may throw.
|
|
177
|
+
for (let i = 0; i < 50; i++) {
|
|
178
|
+
cam.send("AssistantTokenDelta", { stream_id: "s", token: "x".repeat(1000) });
|
|
179
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
180
|
+
}
|
|
181
|
+
await exited;
|
|
182
|
+
assert.equal(cam.send("SessionEnded", {}), false);
|
|
183
|
+
});
|
package/src/index.d.ts
CHANGED
|
@@ -59,6 +59,13 @@ export {
|
|
|
59
59
|
WizardStepResult,
|
|
60
60
|
WizardCompleted,
|
|
61
61
|
WizardCancelled,
|
|
62
|
+
SessionStarted,
|
|
63
|
+
Splash,
|
|
64
|
+
ShowToast,
|
|
65
|
+
DiffPayload,
|
|
66
|
+
TodoItem,
|
|
67
|
+
TodoListUpdate,
|
|
68
|
+
PayloadOf,
|
|
62
69
|
// Tagged union
|
|
63
70
|
Event,
|
|
64
71
|
// Helpers
|
|
@@ -71,6 +78,8 @@ export {
|
|
|
71
78
|
mount,
|
|
72
79
|
selectList,
|
|
73
80
|
confirm,
|
|
81
|
+
permission,
|
|
82
|
+
tasksSet,
|
|
74
83
|
table,
|
|
75
84
|
keyValueView,
|
|
76
85
|
form,
|
package/src/index.js
CHANGED
package/src/types.d.ts
CHANGED
|
@@ -48,7 +48,10 @@ export type EventType =
|
|
|
48
48
|
| "WizardCompleted"
|
|
49
49
|
| "WizardCancelled"
|
|
50
50
|
| "ModeChangeRequested"
|
|
51
|
-
| "CancelRequested"
|
|
51
|
+
| "CancelRequested"
|
|
52
|
+
| "TranscriptCleared"
|
|
53
|
+
| "Splash"
|
|
54
|
+
| "ShowToast";
|
|
52
55
|
|
|
53
56
|
export type Direction = "inbound" | "outbound";
|
|
54
57
|
|
|
@@ -65,11 +68,38 @@ export interface EnvelopeMeta {
|
|
|
65
68
|
export type UserMessage = { text: string };
|
|
66
69
|
export type AssistantStreamStarted = { stream_id: string };
|
|
67
70
|
export type AssistantTokenDelta = { stream_id: string; token: string };
|
|
68
|
-
export type AssistantMessageCompleted = {
|
|
71
|
+
export type AssistantMessageCompleted = {
|
|
72
|
+
stream_id: string;
|
|
73
|
+
/** Optional final text; replaces what was streamed (e.g. after cleanup). */
|
|
74
|
+
text?: string;
|
|
75
|
+
};
|
|
69
76
|
|
|
70
|
-
export type ToolStarted = {
|
|
77
|
+
export type ToolStarted = {
|
|
78
|
+
tool_id: string;
|
|
79
|
+
/** Tool name shown in bold, e.g. "Read" or "Bash". */
|
|
80
|
+
tool: string;
|
|
81
|
+
/** Arguments shown after the name, e.g. a path or a command line. */
|
|
82
|
+
command: string;
|
|
83
|
+
/** When the tool started (epoch ms). Defaults to when the event arrives. */
|
|
84
|
+
started_at_ms?: number;
|
|
85
|
+
};
|
|
71
86
|
export type ToolOutput = { tool_id: string; chunk: string };
|
|
72
|
-
export type ToolFinished = {
|
|
87
|
+
export type ToolFinished = {
|
|
88
|
+
tool_id: string;
|
|
89
|
+
exit_code: number;
|
|
90
|
+
/** Overrides the status derived from exit_code. */
|
|
91
|
+
status?: "done" | "error" | "cancelled" | "rejected";
|
|
92
|
+
/** One-line result under the tool row, e.g. "142 lines" or "14 passed". */
|
|
93
|
+
summary?: string;
|
|
94
|
+
/** Full output; replaces anything streamed via ToolExecutionStdout. */
|
|
95
|
+
output?: string;
|
|
96
|
+
/** Output lines shown before "ctrl+o to expand". Default 0 (12 on error). */
|
|
97
|
+
preview?: number;
|
|
98
|
+
/** Highlight the output as this language (e.g. "ts" for a written file). */
|
|
99
|
+
output_lang?: string;
|
|
100
|
+
/** Show this diff under the tool row. */
|
|
101
|
+
diff?: DiffPayload;
|
|
102
|
+
};
|
|
73
103
|
|
|
74
104
|
export type PatchProposed = {
|
|
75
105
|
path: string;
|
|
@@ -83,8 +113,11 @@ export type PatchApplied = { path: string };
|
|
|
83
113
|
export type PermissionRequested = {
|
|
84
114
|
request_id: string;
|
|
85
115
|
tool: string;
|
|
116
|
+
/** Prompt title, e.g. "edit src/auth/session.ts". */
|
|
86
117
|
action: string;
|
|
87
118
|
detail?: string;
|
|
119
|
+
/** Diff preview for edits. */
|
|
120
|
+
diff?: DiffPayload;
|
|
88
121
|
};
|
|
89
122
|
export type PermissionGranted = { request_id: string };
|
|
90
123
|
export type PermissionDenied = { request_id: string };
|
|
@@ -104,6 +137,14 @@ export type RuntimeError = {
|
|
|
104
137
|
cta?: Cta;
|
|
105
138
|
};
|
|
106
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Status segments. Well-known keys: `mode` ("edit" | "plan" | "auto"),
|
|
142
|
+
* `phase` ("thinking" | "streaming" | "tool" | "running" shows the spinner;
|
|
143
|
+
* anything else, e.g. "idle", ends the turn and settles spinners),
|
|
144
|
+
* `activity` (spinner verb, e.g. "Reading files"), `model`, `tokens`,
|
|
145
|
+
* `cost`, `branch`, `warn`. Other keys are shown in the footer. An empty
|
|
146
|
+
* value removes a segment.
|
|
147
|
+
*/
|
|
107
148
|
export type StatusUpdate = {
|
|
108
149
|
segments: Record<string, string>;
|
|
109
150
|
};
|
|
@@ -261,11 +302,42 @@ export type ModeChangeRequested = {
|
|
|
261
302
|
direction: "next" | "prev";
|
|
262
303
|
};
|
|
263
304
|
|
|
305
|
+
/** Optional welcome and branding for the inline renderer. */
|
|
306
|
+
export type SessionStarted = {
|
|
307
|
+
/** First welcome line, shown in the accent color. Defaults to the app title. */
|
|
308
|
+
title?: string;
|
|
309
|
+
/** Dim lines under the title (model, directory, hints). */
|
|
310
|
+
detail?: string[];
|
|
311
|
+
/** Accent color: a terminal color name ("orange", "blue", …) or "#rrggbb". */
|
|
312
|
+
accent?: string;
|
|
313
|
+
/** Your agent's name, used in prompts like "tell <name> what to do". */
|
|
314
|
+
assistant_label?: string;
|
|
315
|
+
user_label?: string;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/** Multi-line text (may contain ANSI colors) printed as-is, e.g. a logo. */
|
|
319
|
+
export type Splash = { text: string };
|
|
320
|
+
|
|
321
|
+
/** A short message shown in the footer (inline) or as a toast (full screen). */
|
|
322
|
+
export type ShowToast = {
|
|
323
|
+
text: string;
|
|
324
|
+
kind?: "info" | "warn" | "error" | "success";
|
|
325
|
+
ttl_ms?: number;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/** A diff to show inline: full before/after text, or a unified diff. */
|
|
329
|
+
export type DiffPayload = {
|
|
330
|
+
path: string;
|
|
331
|
+
before?: string;
|
|
332
|
+
after?: string;
|
|
333
|
+
unified?: string;
|
|
334
|
+
};
|
|
335
|
+
|
|
264
336
|
// Tagged union --------------------------------------------------------------
|
|
265
337
|
|
|
266
338
|
export type Event = EnvelopeMeta &
|
|
267
339
|
(
|
|
268
|
-
| { event_type: "SessionStarted"; payload?:
|
|
340
|
+
| { event_type: "SessionStarted"; payload?: SessionStarted }
|
|
269
341
|
| { event_type: "SessionEnded"; payload?: Record<string, never> }
|
|
270
342
|
| { event_type: "SessionCompacted"; payload: SessionCompacted }
|
|
271
343
|
| { event_type: "UserMessageCreated"; payload: UserMessage }
|
|
@@ -303,8 +375,14 @@ export type Event = EnvelopeMeta &
|
|
|
303
375
|
| { event_type: "WizardCancelled"; payload: WizardCancelled }
|
|
304
376
|
| { event_type: "ModeChangeRequested"; payload: ModeChangeRequested }
|
|
305
377
|
| { event_type: "CancelRequested"; payload?: Record<string, never> }
|
|
378
|
+
| { event_type: "TranscriptCleared"; payload?: Record<string, never> }
|
|
379
|
+
| { event_type: "Splash"; payload: Splash }
|
|
380
|
+
| { event_type: "ShowToast"; payload: ShowToast }
|
|
306
381
|
);
|
|
307
382
|
|
|
383
|
+
/** The payload type for a given event type, e.g. `PayloadOf<"ShowToast">`. */
|
|
384
|
+
export type PayloadOf<T extends EventType> = Extract<Event, { event_type: T }> extends { payload?: infer P } ? P : never;
|
|
385
|
+
|
|
308
386
|
// Reader --------------------------------------------------------------------
|
|
309
387
|
|
|
310
388
|
import type { Readable } from "node:stream";
|
package/src/types.js
CHANGED
|
@@ -13,7 +13,7 @@ const KNOWN_TYPES = new Set([
|
|
|
13
13
|
"ToolExecutionStarted", "ToolExecutionStdout", "ToolExecutionStderr", "ToolExecutionFinished",
|
|
14
14
|
"PatchProposed", "PatchApplied",
|
|
15
15
|
"PermissionRequested", "PermissionGranted", "PermissionDenied",
|
|
16
|
-
"RuntimeError", "StatusUpdate", "BackgroundTaskUpdate", "ViewportMarker",
|
|
16
|
+
"RuntimeError", "StatusUpdate", "BackgroundTaskUpdate", "TodoListUpdate", "ViewportMarker",
|
|
17
17
|
"UserInputSubmitted", "PermissionResponse",
|
|
18
18
|
"SlashCommandsRegistered", "MentionCandidatesRegistered",
|
|
19
19
|
"ShowSelectList", "SelectListResponse",
|
|
@@ -22,6 +22,7 @@ const KNOWN_TYPES = new Set([
|
|
|
22
22
|
"ShowForm", "FormResponse",
|
|
23
23
|
"ShowWizard", "WizardCompleted", "WizardCancelled",
|
|
24
24
|
"ModeChangeRequested", "CancelRequested",
|
|
25
|
+
"TranscriptCleared", "Splash", "ShowToast",
|
|
25
26
|
]);
|
|
26
27
|
|
|
27
28
|
/**
|