omegon 0.10.1 → 0.10.3

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/bin/omegon.mjs CHANGED
@@ -135,20 +135,31 @@ function purgeSelfReferentialPackages() {
135
135
  }
136
136
  purgeSelfReferentialPackages();
137
137
 
138
- process.argv = injectBundledResourceArgs(process.argv);
139
-
140
138
  // ---------------------------------------------------------------------------
141
- // Pre-import splashshow a simple loading indicator while the module graph
142
- // resolves. The TUI takes over once interactive mode starts.
139
+ // CLI launchsubprocess with restart-loop support.
140
+ //
141
+ // Instead of importing the CLI directly (which makes restart impossible since
142
+ // Node can't replace its own process image), we spawn it as a child process.
143
+ // If the child exits with code 75 (EX_TEMPFAIL), we re-spawn — this is the
144
+ // restart signal from /update and /restart commands.
145
+ //
146
+ // This keeps the wrapper as the foreground process group leader throughout,
147
+ // so the re-spawned CLI always owns the terminal and can receive input.
143
148
  // ---------------------------------------------------------------------------
149
+ import { spawn as nodeSpawn } from "node:child_process";
150
+
151
+ const RESTART_EXIT_CODE = 75;
152
+
153
+ const cliArgs = injectBundledResourceArgs(process.argv).slice(2);
154
+
144
155
  const isInteractive = process.stdout.isTTY &&
145
156
  !process.argv.includes("-p") &&
146
157
  !process.argv.includes("--print") &&
147
158
  !process.argv.includes("--help") &&
148
159
  !process.argv.includes("-h");
149
160
 
150
- let preImportCleanup;
151
- if (isInteractive) {
161
+ function showPreImportSpinner() {
162
+ if (!isInteractive) return undefined;
152
163
  const PRIMARY = "\x1b[38;2;42;180;200m";
153
164
  const DIM = "\x1b[38;2;64;88;112m";
154
165
  const RST = "\x1b[0m";
@@ -157,9 +168,8 @@ if (isInteractive) {
157
168
  const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
158
169
  let frame = 0;
159
170
 
160
- // Safety net: restore cursor on any exit path (crash, SIGTERM, etc.)
161
171
  const restoreCursor = () => { try { process.stdout.write(SHOW_CURSOR); } catch {} };
162
- process.on('exit', restoreCursor);
172
+ process.on("exit", restoreCursor);
163
173
 
164
174
  process.stdout.write(HIDE_CURSOR);
165
175
  process.stdout.write(`\n ${PRIMARY}omegon${RST} ${DIM}loading…${RST}`);
@@ -170,16 +180,52 @@ if (isInteractive) {
170
180
  frame++;
171
181
  }, 80);
172
182
 
173
- preImportCleanup = () => {
183
+ return () => {
174
184
  clearInterval(spinTimer);
175
- process.removeListener('exit', restoreCursor);
176
- // Clear the loading line and restore cursor
185
+ process.removeListener("exit", restoreCursor);
177
186
  process.stdout.write(`\r\x1b[2K${SHOW_CURSOR}`);
178
187
  };
179
188
  }
180
189
 
181
- try {
182
- await import(cli);
183
- } finally {
184
- preImportCleanup?.();
190
+ function launchCli() {
191
+ return new Promise((resolve) => {
192
+ const cleanup = showPreImportSpinner();
193
+
194
+ const child = nodeSpawn(process.execPath, [cli, ...cliArgs], {
195
+ stdio: "inherit",
196
+ env: process.env,
197
+ });
198
+
199
+ // Let the child handle SIGINT (Ctrl+C) — the wrapper ignores it.
200
+ const ignoreInt = () => {};
201
+ process.on("SIGINT", ignoreInt);
202
+ // Forward SIGTERM so graceful shutdown works.
203
+ const fwdTerm = () => child.kill("SIGTERM");
204
+ process.on("SIGTERM", fwdTerm);
205
+
206
+ // Clean up spinner once the child's TUI takes over. The child will
207
+ // clear the screen on startup anyway, but a brief delay ensures the
208
+ // spinner doesn't flicker.
209
+ if (cleanup) {
210
+ setTimeout(() => cleanup(), 200);
211
+ }
212
+
213
+ child.on("exit", (code, signal) => {
214
+ process.removeListener("SIGINT", ignoreInt);
215
+ process.removeListener("SIGTERM", fwdTerm);
216
+ if (signal) {
217
+ // Re-raise the signal so the wrapper exits with the right status
218
+ process.kill(process.pid, signal);
219
+ }
220
+ resolve(code ?? 1);
221
+ });
222
+ });
185
223
  }
224
+
225
+ // Main loop — restart on exit code 75
226
+ let exitCode;
227
+ do {
228
+ exitCode = await launchCli();
229
+ } while (exitCode === RESTART_EXIT_CODE);
230
+
231
+ process.exit(exitCode);
@@ -25,7 +25,7 @@ import { dirname, join } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import { homedir, tmpdir } from "node:os";
27
27
  import type { ExtensionAPI } from "@styrene-lab/pi-coding-agent";
28
- import { resolveOmegonSubprocess } from "../lib/omegon-subprocess.ts";
28
+
29
29
  import { checkAllProviders, type AuthResult } from "../01-auth/auth.ts";
30
30
  import { loadPiConfig } from "../lib/model-preferences.ts";
31
31
  import {
@@ -436,101 +436,18 @@ export default function (pi: ExtensionAPI) {
436
436
  * exit and release the terminal), then exec's the new Omegon. This avoids
437
437
  * two TUI processes fighting over the same terminal simultaneously.
438
438
  */
439
+ /**
440
+ * Restart Omegon by exiting with code 75.
441
+ *
442
+ * The bin/omegon.mjs wrapper runs the CLI in a subprocess loop. When it sees
443
+ * exit code 75 (EX_TEMPFAIL), it re-spawns a fresh CLI process. Because the
444
+ * wrapper stays as the foreground process group leader throughout, the new
445
+ * CLI always owns the terminal and can receive input — no detached spawn,
446
+ * no competing with the shell for stdin.
447
+ */
439
448
  function restartOmegon(): never {
440
- const { command, argvPrefix } = resolveOmegonSubprocess();
441
- const userArgs = process.argv.slice(2).filter(a =>
442
- !a.startsWith("--extensions-dir=") &&
443
- !a.startsWith("--themes-dir=") &&
444
- !a.startsWith("--skills-dir=") &&
445
- !a.startsWith("--prompts-dir=") &&
446
- !a.startsWith("--extension=") && !a.startsWith("--extension ") &&
447
- !a.startsWith("--skill=") && !a.startsWith("--skill ") &&
448
- !a.startsWith("--prompt-template=") && !a.startsWith("--prompt-template ") &&
449
- !a.startsWith("--theme=") && !a.startsWith("--theme ") &&
450
- !a.startsWith("--no-skills") &&
451
- !a.startsWith("--no-prompt-templates") &&
452
- !a.startsWith("--no-themes") &&
453
- !a.startsWith("--no-extensions")
454
- );
455
-
456
- const parts = [command, ...argvPrefix, ...userArgs].map(shellEscape);
457
- const script = join(tmpdir(), `omegon-restart-${process.pid}.sh`);
458
- const oldPid = process.pid;
459
- writeFileSync(script, [
460
- "#!/bin/sh",
461
- // Trap signals so Ctrl+C works; ignore HUP so parent death doesn't kill us
462
- "trap 'stty sane 2>/dev/null; exit 130' INT TERM",
463
- "trap '' HUP",
464
- // Wait for the old process to fully die (poll with timeout)
465
- "_w=0",
466
- `while kill -0 ${oldPid} 2>/dev/null; do`,
467
- " sleep 0.1",
468
- " _w=$((_w + 1))",
469
- // Bail after ~5 seconds — proceed anyway
470
- ' [ "$_w" -ge 50 ] && break',
471
- "done",
472
- // Extra grace period for fd/terminal release
473
- "sleep 0.3",
474
- // Hard terminal reset via /dev/tty — avoids stdout buffering that
475
- // bleeds into the exec'd process. RIS clears all protocol state
476
- // (kitty keyboard protocol, bracketed paste, mouse tracking, etc.).
477
- // Do NOT use `reset` here — it outputs terminfo init strings to
478
- // stdout which the new TUI interprets as input (causing stray
479
- // characters and double renders).
480
- "printf '\\033c' >/dev/tty 2>/dev/null",
481
- "stty sane 2>/dev/null",
482
- // Clean up this script
483
- `rm -f "${script}"`,
484
- // Replace this shell with new omegon
485
- `exec ${parts.join(" ")}`,
486
- ].join("\n") + "\n", { mode: 0o755 });
487
-
488
- // Reset terminal to cooked mode BEFORE exiting so the restart script
489
- // (and the user) aren't stuck with raw-mode terminal if something goes wrong.
490
- try {
491
- // RIS (Reset to Initial State) — the only reliable way to ensure ALL
492
- // terminal protocol state is cleared. Write directly to the TTY fd
493
- // to bypass pi's TUI layer which intercepts process.stdout and would
494
- // mangle the escape sequence into visible ANSI garbage.
495
- const { openSync, writeSync, closeSync } = require("fs") as typeof import("fs");
496
- let ttyFd = -1;
497
- try {
498
- ttyFd = openSync("/dev/tty", "w");
499
- writeSync(ttyFd, "\x1bc");
500
- closeSync(ttyFd);
501
- } catch {
502
- // Fallback if /dev/tty isn't available (shouldn't happen on macOS/Linux)
503
- process.stdout.write("\x1bc");
504
- }
505
- // Pause stdin to prevent buffered input from being re-interpreted
506
- // after raw mode is disabled (prevents Ctrl+D from closing parent shell).
507
- process.stdin.pause();
508
- if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
509
- process.stdin.setRawMode(false);
510
- }
511
- // stty sane resets line discipline to known-good state.
512
- spawnSync("stty", ["sane"], { stdio: ["inherit", "ignore", "ignore"], timeout: 2000 });
513
- } catch { /* best-effort */ }
514
-
515
- // Spawn restart script detached (survives parent exit) but with SIGHUP
516
- // ignored in the script so process-group teardown doesn't kill it.
517
- // detached: true puts it in its own process group; the script's signal
518
- // traps ensure Ctrl+C still works once the new omegon exec's.
519
- const child = spawn("sh", [script], {
520
- stdio: "inherit",
521
- detached: true,
522
- env: process.env,
523
- });
524
- child.unref();
525
-
526
- // Clean exit — the script waits for us to die before starting new omegon
527
- process.exit(0);
528
- }
529
-
530
- /** Escape a string for POSIX shell */
531
- function shellEscape(s: string): string {
532
- if (/^[a-zA-Z0-9_./:=-]+$/.test(s)) return s;
533
- return `'${s.replace(/'/g, "'\\''")}'`;
449
+ const RESTART_EXIT_CODE = 75;
450
+ process.exit(RESTART_EXIT_CODE);
534
451
  }
535
452
 
536
453
  /** Run a command, collect stdout+stderr, resolve with exit code. */
@@ -23,8 +23,8 @@ import { SERMON } from "./sermon.js";
23
23
  const CHAR_INTERVAL_MS = 67;
24
24
  const WORD_PAUSE_MS = 120;
25
25
 
26
- /** Maximum visible characters on the scrawl line. */
27
- const MAX_VISIBLE = 72;
26
+ /** Minimum visible characters (floor for very narrow terminals). */
27
+ const MIN_VISIBLE = 40;
28
28
 
29
29
  // Glitch vocabulary — borrowed from the splash CRT noise aesthetic
30
30
  const NOISE_CHARS = "▓▒░█▄▀▌▐▊▋▍▎▏◆■□▪◇┼╬╪╫";
@@ -40,15 +40,18 @@ const COMBINING_GLITCH = [
40
40
  // Sermon palette — much dimmer than the spinner verb.
41
41
  // The sermon is background thought, not actionable signal.
42
42
  // Base text is near the noise floor; glitch accents stay subdued.
43
+ // IMPORTANT: glitch colors must return to SERMON_DIM, never RESET (which
44
+ // snaps to terminal default — often bright white, causing flash).
43
45
  const SERMON_DIM = "\x1b[38;2;50;55;65m"; // #323741 — barely visible
44
- const GLITCH_GLYPH = "\x1b[38;2;55;75;90m"; // #374b5a — noise glyphs, slightly brighter
45
- const GLITCH_COLOR = "\x1b[38;2;30;100;115m"; // #1e6473 — muted teal shimmer
46
- const RESET = "\x1b[0m";
46
+ const GLITCH_GLYPH = "\x1b[38;2;55;70;80m"; // #374650 — noise glyphs, only slightly above base
47
+ const GLITCH_COLOR = "\x1b[38;2;45;80;90m"; // #2d505avery muted teal, close to base
48
+ const RESET_TO_DIM = SERMON_DIM; // return to base after glitch, never full reset
49
+ const RESET = "\x1b[0m"; // only for end-of-line
47
50
 
48
- // Glitch probabilities per character per render
49
- const P_SUBSTITUTE = 0.03;
50
- const P_COLOR = 0.05;
51
- const P_COMBINING = 0.015;
51
+ // Glitch probabilities per character per render — kept subtle
52
+ const P_SUBSTITUTE = 0.02;
53
+ const P_COLOR = 0.035;
54
+ const P_COMBINING = 0.01;
52
55
 
53
56
  function randomFrom<T>(arr: readonly T[] | string): T | string {
54
57
  return arr[Math.floor(Math.random() * arr.length)];
@@ -60,14 +63,14 @@ function glitchChar(ch: string): string {
60
63
 
61
64
  const r = Math.random();
62
65
 
63
- // Substitution — replace with noise glyph, slightly brighter than base
66
+ // Substitution — replace with noise glyph, only slightly above base
64
67
  if (r < P_SUBSTITUTE) {
65
- return GLITCH_GLYPH + randomFrom(NOISE_CHARS) + RESET;
68
+ return GLITCH_GLYPH + randomFrom(NOISE_CHARS) + RESET_TO_DIM;
66
69
  }
67
70
 
68
- // Color shimmer — subdued teal flicker
71
+ // Color shimmer — very muted teal, not a flash
69
72
  if (r < P_SUBSTITUTE + P_COLOR) {
70
- return GLITCH_COLOR + ch + RESET;
73
+ return GLITCH_COLOR + ch + RESET_TO_DIM;
71
74
  }
72
75
 
73
76
  // Combining diacritics — corruption overlay at base dim
@@ -93,9 +96,9 @@ export function createSermonWidget(
93
96
  cursor = (cursor + 1) % SERMON.length;
94
97
  revealed += ch;
95
98
 
96
- // Sliding window — keep only the tail
97
- if (revealed.length > MAX_VISIBLE) {
98
- revealed = revealed.slice(revealed.length - MAX_VISIBLE);
99
+ // Sliding window — keep a generous buffer; render() trims to actual width
100
+ if (revealed.length > 300) {
101
+ revealed = revealed.slice(revealed.length - 300);
99
102
  }
100
103
 
101
104
  tui.requestRender();
@@ -111,7 +114,8 @@ export function createSermonWidget(
111
114
 
112
115
  return {
113
116
  render(width: number): string[] {
114
- const maxW = Math.min(MAX_VISIBLE, width - 4);
117
+ // Use full terminal width minus a small indent (2 chars)
118
+ const maxW = Math.max(MIN_VISIBLE, width - 4);
115
119
  const visible = revealed.length > maxW
116
120
  ? revealed.slice(revealed.length - maxW)
117
121
  : revealed;