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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camouflage-tui",
3
- "version": "2.4.0-beta.6",
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.
@@ -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
@@ -78,6 +78,8 @@ export {
78
78
  mount,
79
79
  selectList,
80
80
  confirm,
81
+ suspendTerminal,
82
+ resumeTerminal,
81
83
  permission,
82
84
  tasksSet,
83
85
  table,
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
@@ -24,6 +24,7 @@ const KNOWN_TYPES = new Set([
24
24
  "ModeChangeRequested", "CancelRequested",
25
25
  "TranscriptCleared", "Splash", "ShowToast", "MentionQuery",
26
26
  "AssistantReasoningDelta",
27
+ "TerminalSuspend", "TerminalResume", "TerminalSuspended",
27
28
  ]);
28
29
 
29
30
  /**