wotann 0.5.84 → 0.5.85

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/dist/index.js CHANGED
@@ -260,7 +260,9 @@ program
260
260
  try {
261
261
  const { createRuntime } = await import("./core/runtime.js");
262
262
  const { runRuntimeQuery } = await import("./cli/runtime-query.js");
263
- const jsonSchema = options.jsonSchema !== undefined ? parseJsonSchemaOption(options.jsonSchema) : undefined;
263
+ const jsonSchema = options.jsonSchema !== undefined
264
+ ? parseJsonSchemaOption(options.jsonSchema)
265
+ : undefined;
264
266
  const prContext = options.fromPr !== undefined ? loadPullRequestContext(options.fromPr) : undefined;
265
267
  const runtime = await createRuntime(process.cwd(), undefined, {
266
268
  ...(options.bare
@@ -402,13 +404,22 @@ program
402
404
  process.stderr.write("[wotann] Unknown renderer. Use --renderer ink or --renderer opentui.\n");
403
405
  return;
404
406
  }
405
- // Switch to the alternate screen buffer before Ink takes over
406
- // so the TUI gets the entire viewport and the user's scrollback
407
- // is preserved untouched. Crash-safe alt-buffer.ts wires
408
- // SIGINT/SIGTERM/uncaughtException to always restore the main
409
- // buffer. Default ON; disable with `--no-fullscreen` or
410
- // `WOTANN_FULLSCREEN=0`.
411
- const { isAltBufferRequested, enterAltBuffer } = await import("./ui/alt-buffer.js");
407
+ // Pre-load the heavy UI modules BEFORE entering alt-buffer so the
408
+ // window between alt-buffer entry and first Ink frame is near-zero.
409
+ // Otherwise the user stares at a pitch-black alt-buffer while
410
+ // AppV3 + Ink + react-reconciler hydrate, which is exactly the
411
+ // "npx wotann hangs and shows a black screen" symptom users have
412
+ // reported (2026-05-23 P0). Parallel imports cut the wait further.
413
+ const [{ AppV3 }, { mountInteractiveInk }, altBufferModule] = await Promise.all([
414
+ import("./ui/components/v3/index.js"),
415
+ import("./ui/mount-interactive-ink.js"),
416
+ import("./ui/alt-buffer.js"),
417
+ ]);
418
+ const { isAltBufferRequested, enterAltBuffer, exitAltBuffer } = altBufferModule;
419
+ // Switch to the alternate screen buffer NOW that the heavy modules
420
+ // are loaded. Crash-safe — alt-buffer.ts wires SIGINT/SIGTERM/
421
+ // uncaughtException to always restore the main buffer. Default ON;
422
+ // disable with `--no-fullscreen` or `WOTANN_FULLSCREEN=0`.
412
423
  if (isAltBufferRequested(options.fullscreen !== false)) {
413
424
  enterAltBuffer();
414
425
  }
@@ -419,17 +430,34 @@ program
419
430
  // (process.stdin → /dev/tty → none), and refuse-cleanly-with-
420
431
  // guidance instead of mounting Ink into the error-render-loop
421
432
  // hang ("npx wotann just hangs"). See ui/mount-interactive-ink.ts.
422
- const { AppV3 } = await import("./ui/components/v3/index.js");
423
- const { mountInteractiveInk } = await import("./ui/mount-interactive-ink.js");
424
- const { refused } = await mountInteractiveInk(React.createElement(AppV3, {
425
- version: VERSION,
426
- providers: interactive.providers,
427
- initialModel: interactive.initialModel,
428
- initialProvider: interactive.initialProvider,
429
- runtime: interactive.runtime,
430
- }));
431
- if (refused)
433
+ //
434
+ // The try/catch + exitAltBuffer pair is the recovery contract:
435
+ // if Ink throws (e.g. setRawMode passes the guard probe but
436
+ // fails on Ink's first useInput commit on some terminal combos),
437
+ // we ALWAYS restore the main buffer first so the user sees the
438
+ // diagnostic instead of a frozen black screen.
439
+ let mountResult;
440
+ try {
441
+ mountResult = await mountInteractiveInk(React.createElement(AppV3, {
442
+ version: VERSION,
443
+ providers: interactive.providers,
444
+ initialModel: interactive.initialModel,
445
+ initialProvider: interactive.initialProvider,
446
+ runtime: interactive.runtime,
447
+ }));
448
+ }
449
+ catch (error) {
450
+ exitAltBuffer();
451
+ process.stderr.write(`[wotann] Interactive TUI failed to mount: ${error instanceof Error ? error.message : String(error)}\n`);
452
+ return;
453
+ }
454
+ if (mountResult.refused) {
455
+ // mountInteractiveInk already wrote actionable guidance to
456
+ // stderr. Restore main buffer so guidance is visible instead
457
+ // of trapped behind an alt-buffer Ink never painted into.
458
+ exitAltBuffer();
432
459
  return;
460
+ }
433
461
  });
434
462
  // ── wotann cli-generate (CLI-Anything 7-phase generator) ─────────
435
463
  program
@@ -1908,10 +1936,16 @@ program
1908
1936
  console.log(chalk.green(" Session context restored. Continue where you left off.\n"));
1909
1937
  const ReactModule = await import("react");
1910
1938
  const React = ReactModule.default;
1911
- // Same fullscreen-mode hook as `wotann start` env var only here
1912
- // (no CLI flag on `wotann resume`). Default ON; disable via
1913
- // `WOTANN_FULLSCREEN=0`.
1914
- const { isAltBufferRequested, enterAltBuffer } = await import("./ui/alt-buffer.js");
1939
+ // Pre-load heavy UI modules in parallel BEFORE entering alt-buffer
1940
+ // (npx pitch-black fix 2026-05-23). Same fullscreen-mode hook as
1941
+ // `wotann start` — env var only here (no CLI flag on `wotann
1942
+ // resume`). Default ON; disable via `WOTANN_FULLSCREEN=0`.
1943
+ const [{ AppV3 }, { mountInteractiveInk }, altBufferModule] = await Promise.all([
1944
+ import("./ui/components/v3/index.js"),
1945
+ import("./ui/mount-interactive-ink.js"),
1946
+ import("./ui/alt-buffer.js"),
1947
+ ]);
1948
+ const { isAltBufferRequested, enterAltBuffer, exitAltBuffer } = altBufferModule;
1915
1949
  if (isAltBufferRequested(true)) {
1916
1950
  enterAltBuffer();
1917
1951
  }
@@ -1919,18 +1953,28 @@ program
1919
1953
  // resumed session continues where it left off (AppV3 wires it via
1920
1954
  // useState([...initialMessages])). Same guarded gate as the start
1921
1955
  // path: viewport repair + raw-mode stdin + refuse-instead-of-hang.
1922
- const { AppV3 } = await import("./ui/components/v3/index.js");
1923
- const { mountInteractiveInk } = await import("./ui/mount-interactive-ink.js");
1924
- const { refused } = await mountInteractiveInk(React.createElement(AppV3, {
1925
- version: VERSION,
1926
- providers: interactive.providers,
1927
- initialModel: session.model,
1928
- initialProvider: session.provider,
1929
- initialMessages: session.messages,
1930
- runtime: interactive.runtime,
1931
- }));
1932
- if (refused)
1956
+ // try/catch + exitAltBuffer pair ensures the user always sees the
1957
+ // diagnostic on Ink-mount failure instead of a frozen black screen.
1958
+ let mountResult;
1959
+ try {
1960
+ mountResult = await mountInteractiveInk(React.createElement(AppV3, {
1961
+ version: VERSION,
1962
+ providers: interactive.providers,
1963
+ initialModel: session.model,
1964
+ initialProvider: session.provider,
1965
+ initialMessages: session.messages,
1966
+ runtime: interactive.runtime,
1967
+ }));
1968
+ }
1969
+ catch (error) {
1970
+ exitAltBuffer();
1971
+ process.stderr.write(`[wotann] Interactive TUI failed to mount: ${error instanceof Error ? error.message : String(error)}\n`);
1933
1972
  return;
1973
+ }
1974
+ if (mountResult.refused) {
1975
+ exitAltBuffer();
1976
+ return;
1977
+ }
1934
1978
  });
1935
1979
  // ── wotann rewind ───────────────────────────────────────────
1936
1980
  //
@@ -56,6 +56,14 @@ export interface MountInteractiveResult {
56
56
  * degraded-terminal case — it returns `{ instance: null, refused: true }`
57
57
  * after writing actionable guidance, so the caller exits cleanly
58
58
  * instead of hanging.
59
+ *
60
+ * Ink-render-throws backstop (added 2026-05-23, npx pitch-black fix):
61
+ * even with the raw-mode-guard probe in front, Ink's reconciler can
62
+ * still throw during its first `useInput` commit on terminal combos
63
+ * where `setRawMode` exists, passes the probe, then fails on the
64
+ * *specific* `(true)` call Ink makes. Catching here and returning
65
+ * `refused: true` lets the caller exit the alt-buffer + show the
66
+ * user the actual failure instead of a frozen black screen.
59
67
  */
60
68
  export declare function mountInteractiveInk(element: ReactElement, opts?: MountInteractiveOptions): Promise<MountInteractiveResult>;
61
69
  export {};
@@ -33,6 +33,14 @@ export const DEFAULT_REFUSAL_MESSAGE = "[wotann] No raw-mode-capable terminal av
33
33
  * degraded-terminal case — it returns `{ instance: null, refused: true }`
34
34
  * after writing actionable guidance, so the caller exits cleanly
35
35
  * instead of hanging.
36
+ *
37
+ * Ink-render-throws backstop (added 2026-05-23, npx pitch-black fix):
38
+ * even with the raw-mode-guard probe in front, Ink's reconciler can
39
+ * still throw during its first `useInput` commit on terminal combos
40
+ * where `setRawMode` exists, passes the probe, then fails on the
41
+ * *specific* `(true)` call Ink makes. Catching here and returning
42
+ * `refused: true` lets the caller exit the alt-buffer + show the
43
+ * user the actual failure instead of a frozen black screen.
36
44
  */
37
45
  export async function mountInteractiveInk(element, opts = {}) {
38
46
  const ensureViewport = opts.ensureViewport ?? ensureRenderableViewport;
@@ -47,6 +55,18 @@ export async function mountInteractiveInk(element, opts = {}) {
47
55
  return { instance: null, refused: true };
48
56
  }
49
57
  const inkRender = opts.inkRender ?? (await import("ink")).render;
50
- const instance = inkRender(element, { stdin: inputStdin });
51
- return { instance, refused: false };
58
+ try {
59
+ const instance = inkRender(element, { stdin: inputStdin });
60
+ return { instance, refused: false };
61
+ }
62
+ catch (error) {
63
+ const sink = opts.stderr ?? process.stderr;
64
+ const message = error instanceof Error ? error.message : String(error);
65
+ sink.write(`[wotann] Interactive TUI failed to mount: ${message}\n` +
66
+ " This usually means the terminal accepted setRawMode but Ink's\n" +
67
+ " first input commit threw under this launcher. Options:\n" +
68
+ " • `npm i -g wotann` then run `wotann` in a fresh terminal\n" +
69
+ ' • non-interactive: `wotann -p "your prompt"`\n');
70
+ return { instance: null, refused: true };
71
+ }
52
72
  }
@@ -18,11 +18,26 @@
18
18
  */
19
19
  export interface RawModeStream {
20
20
  readonly isTTY?: unknown;
21
+ readonly isRaw?: unknown;
21
22
  setRawMode?: unknown;
22
23
  }
23
24
  /**
24
- * True iff `stream` is a TTY that exposes a `setRawMode` function
25
- * exactly Ink's precondition for `useInput()`. Never throws.
25
+ * True iff `stream` is a TTY that exposes a `setRawMode` function AND
26
+ * that function works when invoked — exactly Ink's runtime precondition
27
+ * for `useInput()`. Never throws.
28
+ *
29
+ * The shape check (`isTTY === true` + `typeof setRawMode === "function"`)
30
+ * caught one silent-failure class — degraded stdin — but missed a
31
+ * second one: some `npx`/terminal combinations expose `setRawMode` as
32
+ * a function that THROWS the moment Ink invokes it during the first
33
+ * `useInput` hook. The property-only check passed, Ink mounted into
34
+ * an alt-buffer, Ink's reconciler error-looped on the rejected raw
35
+ * mode call, and the user saw the reported "pitch-black screen, TUI
36
+ * never paints" symptom. The probe below catches that case: call
37
+ * `setRawMode` with the CURRENT raw-state (idempotent — terminal
38
+ * state is preserved when it works) inside a try/catch. If it throws
39
+ * here, the resolver falls through to `/dev/tty` or refuses cleanly
40
+ * instead of mounting Ink into the silent loop.
26
41
  */
27
42
  export declare function isRawModeCapable(stream: unknown): boolean;
28
43
  export interface ResolveStdinOptions {
@@ -19,14 +19,38 @@
19
19
  import { openSync } from "node:fs";
20
20
  import { ReadStream } from "node:tty";
21
21
  /**
22
- * True iff `stream` is a TTY that exposes a `setRawMode` function
23
- * exactly Ink's precondition for `useInput()`. Never throws.
22
+ * True iff `stream` is a TTY that exposes a `setRawMode` function AND
23
+ * that function works when invoked — exactly Ink's runtime precondition
24
+ * for `useInput()`. Never throws.
25
+ *
26
+ * The shape check (`isTTY === true` + `typeof setRawMode === "function"`)
27
+ * caught one silent-failure class — degraded stdin — but missed a
28
+ * second one: some `npx`/terminal combinations expose `setRawMode` as
29
+ * a function that THROWS the moment Ink invokes it during the first
30
+ * `useInput` hook. The property-only check passed, Ink mounted into
31
+ * an alt-buffer, Ink's reconciler error-looped on the rejected raw
32
+ * mode call, and the user saw the reported "pitch-black screen, TUI
33
+ * never paints" symptom. The probe below catches that case: call
34
+ * `setRawMode` with the CURRENT raw-state (idempotent — terminal
35
+ * state is preserved when it works) inside a try/catch. If it throws
36
+ * here, the resolver falls through to `/dev/tty` or refuses cleanly
37
+ * instead of mounting Ink into the silent loop.
24
38
  */
25
39
  export function isRawModeCapable(stream) {
26
40
  if (stream === null || typeof stream !== "object")
27
41
  return false;
28
42
  const s = stream;
29
- return s.isTTY === true && typeof s.setRawMode === "function";
43
+ if (s.isTTY !== true || typeof s.setRawMode !== "function")
44
+ return false;
45
+ try {
46
+ const setRawMode = s.setRawMode;
47
+ const currentIsRaw = s.isRaw === true;
48
+ setRawMode(currentIsRaw);
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
30
54
  }
31
55
  /**
32
56
  * Open the controlling terminal as a raw-capable input stream, or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wotann",
3
- "version": "0.5.84",
3
+ "version": "0.5.85",
4
4
  "description": "WOTANN — The All-Father of AI Agent Harnesses",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",