privateer-agent 0.12.31 → 0.12.32

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.
@@ -37,8 +37,19 @@
37
37
  // never lands in front of real output.
38
38
  //
39
39
  // The wave is drawn on STDERR; stdout belongs to the TUI's canvas.
40
+ //
41
+ // WHY EVERY WRITE OF OURS IS fs.writeSync. On Windows a write to a TTY stream is
42
+ // ASYNCHRONOUS — process.stderr.write only queues the bytes for the event loop — and the
43
+ // whole point of this file is that Pi's boot never gives the event loop a turn. Through
44
+ // process.stderr, every erase we issue during the wait would land after the output it was
45
+ // meant to clear, and the cursor restore on `exit` would never flush at all: a Windows
46
+ // console left with wave fragments in front of Pi's first frame and no cursor afterwards.
47
+ // fs.writeSync goes straight to fd 2, which is also what the drawing thread uses, so the
48
+ // two threads' output stays in the order it was issued. Pi's OWN stderr still goes
49
+ // through the stream — that write belongs to the caller, return value and all.
40
50
 
41
51
  import { spawnSync } from "node:child_process";
52
+ import fs from "node:fs";
42
53
  import path from "node:path";
43
54
  import { Worker } from "node:worker_threads";
44
55
 
@@ -62,6 +73,32 @@ if (enabled && process.platform === "win32") {
62
73
  }
63
74
  }
64
75
 
76
+ // WHICH GLYPHS THE CONSOLE CAN ACTUALLY DRAW. Code page 65001 above settles the ENCODING;
77
+ // it says nothing about the FONT. Legacy conhost — a plain cmd.exe or PowerShell window,
78
+ // which is still what `privateer` gets when it isn't launched from Windows Terminal —
79
+ // defaults to Lucida Console or a raster font, and those cover exactly the CP437 block
80
+ // elements (█ ▄ ▀ ░ ▒ ▓) and nothing else. The eighth-block ramp, the anchor and the
81
+ // ellipsis are all absent there, so the "wave" drew as a row of tofu boxes that changed
82
+ // shape every frame. Every modern host announces itself in the environment (Windows
83
+ // Terminal, VS Code, ConEmu/ANSICON, anything mintty-ish that sets TERM), and on those the
84
+ // eighth blocks are the better picture, so the fallback is only for the ones that don't.
85
+ const legacyConsole =
86
+ process.platform === "win32" &&
87
+ !(
88
+ process.env.WT_SESSION ||
89
+ process.env.WT_PROFILE_ID ||
90
+ process.env.TERM_PROGRAM ||
91
+ process.env.ConEmuANSI ||
92
+ process.env.ANSICON ||
93
+ process.env.TERM
94
+ );
95
+
96
+ // Eight levels either way, so the wave keeps its shape: height where the font has the
97
+ // eighth blocks, density where it only has the CP437 shades.
98
+ const BLOCKS = legacyConsole ? " \u2591\u2591\u2592\u2592\u2593\u2593\u2588" : "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
99
+ const ANCHOR = legacyConsole ? "~" : "\u2693";
100
+ const ELLIPSIS = legacyConsole ? "..." : "\u2026";
101
+
65
102
  // Bytes of stdout after TUI.start() that mean "this is the first frame, not a control
66
103
  // sequence". Everything Pi writes between raw mode and the frame is short (the paste
67
104
  // toggle, a Kitty protocol query, the cursor hide, an OSC window title — 42 bytes all
@@ -86,10 +123,17 @@ if (enabled) {
86
123
  const sab = new SharedArrayBuffer(4 * Int32Array.BYTES_PER_ELEMENT);
87
124
  const state = new Int32Array(sab);
88
125
 
89
- // Room for " ⚓ " + wave + message + elapsed, clamped so a narrow terminal doesn't
90
- // wrap (a wrapped line survives our `\r\x1b[K` erase only on its last row).
126
+ // Room for " ⚓ " + wave + " " + message + ellipsis + elapsed, clamped so a narrow
127
+ // terminal doesn't wrap (a wrapped line survives our `\r\x1b[K` erase only on its last
128
+ // row). The reserve is measured rather than guessed, because the pieces are no longer
129
+ // fixed: the anchor is TWO cells wherever ⚓ keeps its emoji presentation, one where it
130
+ // fell back to ASCII, and the ellipsis is one cell or three.
131
+ const MSGS = ["hoisting sail", "raising the colours"];
132
+ const anchorCells = ANCHOR === "\u2693" ? 2 : 1; // U+2693 carries emoji presentation
133
+ const reserve =
134
+ 2 + anchorCells + 1 + 1 + Math.max(...MSGS.map((m) => m.length)) + ELLIPSIS.length + 5;
91
135
  const cols = err.columns && err.columns > 0 ? err.columns : 80;
92
- const width = Math.max(6, Math.min(28, cols - 34));
136
+ const width = Math.max(6, Math.min(28, cols - reserve));
93
137
 
94
138
  // The worker source is plain logic with no escape sequences of its own — every ANSI
95
139
  // string is handed over in workerData, so nothing here has to survive two rounds of
@@ -122,7 +166,7 @@ if (enabled) {
122
166
  const secs = Math.round((Date.now() - t0) / 1000);
123
167
  const msg = w.msgs[Atomics.load(s, PHASE)];
124
168
  const age = secs >= 3 ? w.dim + " " + secs + "s" + w.off : "";
125
- fs.writeSync(2, w.cr + " " + w.anchor + " " + wave(frame++ * 0.35) + " " + w.dim + msg + "…" + w.off + age + w.clearEol);
169
+ fs.writeSync(2, w.cr + " " + w.anchor + " " + wave(frame++ * 0.35) + " " + w.dim + msg + w.ellipsis + w.off + age + w.clearEol);
126
170
  }
127
171
 
128
172
  // Atomics.wait doubles as the sleep: an exact 80ms tick that the main thread can cut
@@ -146,9 +190,10 @@ if (enabled) {
146
190
  sab,
147
191
  width,
148
192
  hold: HOLD_MS,
149
- blocks: "▁▂▃▄▅▆▇█",
150
- msgs: ["hoisting sail", "raising the colours"],
151
- anchor: "\x1b[38;5;69m⚓\x1b[0m",
193
+ blocks: BLOCKS,
194
+ msgs: MSGS,
195
+ ellipsis: ELLIPSIS,
196
+ anchor: `\x1b[38;5;69m${ANCHOR}\x1b[0m`,
152
197
  crest: "\x1b[38;5;109m",
153
198
  trough: "\x1b[38;5;67m",
154
199
  dim: "\x1b[2m",
@@ -176,8 +221,23 @@ if (enabled) {
176
221
  if (!Atomics.load(state, ACK)) Atomics.wait(state, ACK, 0, 50);
177
222
  }
178
223
 
224
+ // Our own control sequences, written straight to fd 2 and synchronously — see the note
225
+ // at the top of the file for why process.stderr will not do. A short write or an EAGAIN
226
+ // from a non-blocking tty is retried; anything else is swallowed, because a splash is
227
+ // never worth a crash.
228
+ function writeCtl(s) {
229
+ const buf = Buffer.from(s, "utf8");
230
+ for (let off = 0, tries = 0; off < buf.length && tries < 100; tries++) {
231
+ try {
232
+ off += fs.writeSync(2, buf, off);
233
+ } catch (e) {
234
+ if (e?.code !== "EAGAIN") return;
235
+ }
236
+ }
237
+ }
238
+
179
239
  function clearLine() {
180
- if (Atomics.load(state, DREW)) errWrite("\r\x1b[K");
240
+ if (Atomics.load(state, DREW)) writeCtl("\r\x1b[K");
181
241
  }
182
242
 
183
243
  function stop() {
@@ -187,7 +247,7 @@ if (enabled) {
187
247
  clearLine();
188
248
  // Only give the cursor back if Pi hasn't deliberately hidden it — the TUI hides it
189
249
  // for the whole session and would never get the chance to hide it again.
190
- if (Atomics.load(state, DREW) && !appHidCursor) errWrite("\x1b[?25h");
250
+ if (Atomics.load(state, DREW) && !appHidCursor) writeCtl("\x1b[?25h");
191
251
  process.stdout.write = outWrite;
192
252
  err.write = errWrite;
193
253
  worker.terminate();
@@ -246,10 +306,12 @@ if (enabled) {
246
306
  } catch (e) {
247
307
  if (e?.code !== "EIO") throw e;
248
308
  stop();
249
- errWrite(
309
+ // writeCtl, not errWrite: process.exit() below does not flush a stream write that
310
+ // Windows has merely queued, and this message is the only thing the user gets.
311
+ writeCtl(
250
312
  [
251
313
  "",
252
- " Privateer couldn't take the helm — this terminal stopped accepting keyboard",
314
+ ` ${ANCHOR} Privateer couldn't take the helm — this terminal stopped accepting keyboard`,
253
315
  " control while the agent was still loading (setRawMode EIO).",
254
316
  "",
255
317
  " That usually means the window, tab or ssh session it started in went away.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.12.31",
3
+ "version": "0.12.32",
4
4
  "description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
5
5
  "type": "module",
6
6
  "license": "MIT",