camouflage-tui 2.4.0-beta.6 → 2.4.0-beta.7
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/package.json +1 -1
- package/src/binding.d.ts +10 -0
- package/src/binding.js +37 -0
- package/src/binding.test.js +9 -1
- package/src/index.d.ts +2 -0
- package/src/index.js +1 -1
- package/src/types.d.ts +8 -1
- package/src/types.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "camouflage-tui",
|
|
3
|
-
"version": "2.4.0-beta.
|
|
3
|
+
"version": "2.4.0-beta.7",
|
|
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",
|
package/src/binding.d.ts
CHANGED
|
@@ -69,6 +69,16 @@ export function confirm(
|
|
|
69
69
|
},
|
|
70
70
|
): Promise<ConfirmResponseEvent>;
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Hand the terminal to a child process; resolves once the renderer has let
|
|
74
|
+
* go of it. `supported: false` means run the child without a terminal
|
|
75
|
+
* (Windows, full-screen mode, older renderers, or no answer in time).
|
|
76
|
+
*/
|
|
77
|
+
export function suspendTerminal(cam: CamouflageHandle, opts?: { timeoutMs?: number }): Promise<{ supported: boolean }>;
|
|
78
|
+
|
|
79
|
+
/** Take the terminal back after `suspendTerminal`. */
|
|
80
|
+
export function resumeTerminal(cam: CamouflageHandle): void;
|
|
81
|
+
|
|
72
82
|
/** Convenience helper: show a tabular data view. Display-only. */
|
|
73
83
|
export function table(
|
|
74
84
|
cam: CamouflageHandle,
|
package/src/binding.js
CHANGED
|
@@ -344,6 +344,8 @@ export async function mount(opts = {}) {
|
|
|
344
344
|
handle.emit("cancelRequested", {});
|
|
345
345
|
} else if (ev.event_type === "MentionQuery") {
|
|
346
346
|
handle.emit("mentionQuery", { query: ev.payload?.query ?? "" });
|
|
347
|
+
} else if (ev.event_type === "TerminalSuspended") {
|
|
348
|
+
handle.emit("terminalSuspended", { id: ev.payload?.id, supported: ev.payload?.supported === true });
|
|
347
349
|
}
|
|
348
350
|
// Always also emit the raw Event for advanced consumers.
|
|
349
351
|
handle.emit("event", ev);
|
|
@@ -421,6 +423,41 @@ export function confirm(cam, spec) {
|
|
|
421
423
|
return done;
|
|
422
424
|
}
|
|
423
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Hand the terminal to a child process (a `!` shell command, an editor, a
|
|
428
|
+
* login prompt). The inline renderer clears its live area, leaves raw mode
|
|
429
|
+
* and stops reading keys before this resolves. Run the child with
|
|
430
|
+
* `stdio: "inherit"`, then call `resumeTerminal(cam)`.
|
|
431
|
+
*
|
|
432
|
+
* Resolves `{ supported: false }` when the renderer can't hand the terminal
|
|
433
|
+
* over (Windows, full-screen mode, renderers older than 2.4.0-beta.7, or no
|
|
434
|
+
* answer within `timeoutMs`); run the child without a terminal then.
|
|
435
|
+
*
|
|
436
|
+
* @param {CamouflageHandle} cam
|
|
437
|
+
* @param {{timeoutMs?: number}} [opts]
|
|
438
|
+
* @returns {Promise<{supported: boolean}>}
|
|
439
|
+
*/
|
|
440
|
+
export function suspendTerminal(cam, opts = {}) {
|
|
441
|
+
const id = `suspend-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
442
|
+
const answer = awaitResponse(cam, "terminalSuspended", (r) => r.id === id, { id, supported: false });
|
|
443
|
+
const timeout = new Promise((resolve) => {
|
|
444
|
+
const t = setTimeout(() => resolve({ id, supported: false }), opts.timeoutMs ?? 1000);
|
|
445
|
+
t.unref?.();
|
|
446
|
+
});
|
|
447
|
+
cam.send("TerminalSuspend", { id });
|
|
448
|
+
return Promise.race([answer, timeout]).then((r) => ({ supported: r.supported === true }));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Take the terminal back after `suspendTerminal`: the renderer re-enters
|
|
453
|
+
* raw mode and redraws below whatever the child printed.
|
|
454
|
+
*
|
|
455
|
+
* @param {CamouflageHandle} cam
|
|
456
|
+
*/
|
|
457
|
+
export function resumeTerminal(cam) {
|
|
458
|
+
cam.send("TerminalResume", { id: "resume" });
|
|
459
|
+
}
|
|
460
|
+
|
|
424
461
|
/**
|
|
425
462
|
* Ask the user for permission and resolve to their answer. Resolves
|
|
426
463
|
* `{ choice: "deny" }` if the renderer exits first.
|
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, permission } from "./index.js";
|
|
5
|
+
import { mount, selectList, confirm, permission, suspendTerminal, resumeTerminal } from "./index.js";
|
|
6
6
|
|
|
7
7
|
const FAKE = join(dirname(fileURLToPath(import.meta.url)), "__fake-renderer.js");
|
|
8
8
|
|
|
@@ -181,3 +181,11 @@ test("a renderer that exits at startup doesn't crash the host", async () => {
|
|
|
181
181
|
await exited;
|
|
182
182
|
assert.equal(cam.send("SessionEnded", {}), false);
|
|
183
183
|
});
|
|
184
|
+
|
|
185
|
+
test("suspendTerminal() falls back to unsupported when the renderer doesn't answer", async () => {
|
|
186
|
+
const cam = await mountFake(); // echoes TerminalSuspend back; never sends TerminalSuspended
|
|
187
|
+
const r = await suspendTerminal(cam, { timeoutMs: 50 });
|
|
188
|
+
assert.deepEqual(r, { supported: false });
|
|
189
|
+
resumeTerminal(cam);
|
|
190
|
+
await cam.close();
|
|
191
|
+
});
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -15,4 +15,4 @@ export {
|
|
|
15
15
|
encode,
|
|
16
16
|
} from "./types.js";
|
|
17
17
|
|
|
18
|
-
export { mount, selectList, confirm, permission, table, keyValueView, form, wizard, tasksSet } from "./binding.js";
|
|
18
|
+
export { mount, selectList, confirm, permission, table, keyValueView, form, wizard, tasksSet, suspendTerminal, resumeTerminal } from "./binding.js";
|
package/src/types.d.ts
CHANGED
|
@@ -53,7 +53,10 @@ export type EventType =
|
|
|
53
53
|
| "Splash"
|
|
54
54
|
| "ShowToast"
|
|
55
55
|
| "MentionQuery"
|
|
56
|
-
| "AssistantReasoningDelta"
|
|
56
|
+
| "AssistantReasoningDelta"
|
|
57
|
+
| "TerminalSuspend"
|
|
58
|
+
| "TerminalResume"
|
|
59
|
+
| "TerminalSuspended";
|
|
57
60
|
|
|
58
61
|
export type Direction = "inbound" | "outbound";
|
|
59
62
|
|
|
@@ -370,6 +373,10 @@ export type Event = EnvelopeMeta &
|
|
|
370
373
|
| { event_type: "AssistantTokenDelta"; payload: AssistantTokenDelta }
|
|
371
374
|
/** v2.4+: the model's reasoning, shown when the user presses Ctrl+R (inline UI). */
|
|
372
375
|
| { event_type: "AssistantReasoningDelta"; payload: AssistantTokenDelta }
|
|
376
|
+
/** v2.4+: hand the terminal to a child process; see `suspendTerminal()`. */
|
|
377
|
+
| { event_type: "TerminalSuspend"; payload: { id: string } }
|
|
378
|
+
| { event_type: "TerminalResume"; payload: { id: string } }
|
|
379
|
+
| { event_type: "TerminalSuspended"; payload: { id: string; supported: boolean } }
|
|
373
380
|
| { event_type: "AssistantMessageCompleted"; payload: AssistantMessageCompleted }
|
|
374
381
|
| { event_type: "ToolExecutionStarted"; payload: ToolStarted }
|
|
375
382
|
| { event_type: "ToolExecutionStdout"; payload: ToolOutput }
|
package/src/types.js
CHANGED