@standardagents/code 0.9.4 → 0.9.6

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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import os10, { homedir } from 'os';
2
+ import os7, { homedir } from 'os';
3
3
  import path3 from 'path';
4
4
  import readline2 from 'readline/promises';
5
5
  import { stdout, stdin } from 'process';
@@ -10,7 +10,140 @@ import { spawn, execFileSync, spawnSync, execFile } from 'child_process';
10
10
  import readline from 'readline';
11
11
  import { fileURLToPath } from 'url';
12
12
 
13
+ // src/shared-messaging.ts
14
+ var SHARED_MESSAGING_ROUTE = "/standard-code/messaging";
15
+ var SHARED_MESSAGING_EVENT = "standard_code_messaging_changed";
16
+ function isInlineSharedAttachment(value) {
17
+ return "data" in value;
18
+ }
19
+ function isSharedAttachmentRef(value) {
20
+ return "type" in value && value.type === "file";
21
+ }
22
+ var EMPTY_ORIGIN = {
23
+ originClientId: "",
24
+ originClientKind: "unknown"
25
+ };
26
+ function emptySharedMessagingSnapshot() {
27
+ return {
28
+ version: 1,
29
+ pending: { version: 1, revision: 0, items: [] },
30
+ draft: {
31
+ version: 1,
32
+ revision: 0,
33
+ content: "",
34
+ attachments: [],
35
+ updatedAt: 0,
36
+ ...EMPTY_ORIGIN
37
+ }
38
+ };
39
+ }
40
+ function mergeSharedMessagingSnapshot(current, incoming) {
41
+ return {
42
+ version: 1,
43
+ pending: incoming.pending.revision >= current.pending.revision ? incoming.pending : current.pending,
44
+ draft: incoming.draft.revision >= current.draft.revision ? incoming.draft : current.draft
45
+ };
46
+ }
47
+ function record(value) {
48
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
49
+ }
50
+ function finiteNumber(value, label) {
51
+ if (typeof value !== "number" || !Number.isFinite(value)) {
52
+ throw new TypeError(`invalid shared messaging ${label}`);
53
+ }
54
+ return value;
55
+ }
56
+ function clientKind(value) {
57
+ return value === "tui" || value === "web" || value === "mac" || value === "ios" ? value : "unknown";
58
+ }
59
+ function origin(value) {
60
+ return {
61
+ originClientId: typeof value?.originClientId === "string" ? value.originClientId : "",
62
+ originClientKind: clientKind(value?.originClientKind)
63
+ };
64
+ }
65
+ function attachments(value) {
66
+ if (!Array.isArray(value)) throw new TypeError("invalid shared messaging attachments");
67
+ const parsed = [];
68
+ for (const raw of value) {
69
+ const item = record(raw);
70
+ if (!item || typeof item.name !== "string" || typeof item.mimeType !== "string") {
71
+ throw new TypeError("invalid shared messaging attachment");
72
+ }
73
+ const dimensions = {
74
+ ...typeof item.width === "number" && Number.isFinite(item.width) ? { width: item.width } : {},
75
+ ...typeof item.height === "number" && Number.isFinite(item.height) ? { height: item.height } : {}
76
+ };
77
+ if (typeof item.data === "string") {
78
+ parsed.push({ name: item.name, mimeType: item.mimeType, data: item.data, ...dimensions });
79
+ } else if (item.type === "file" && typeof item.id === "string" && typeof item.path === "string" && typeof item.size === "number" && Number.isFinite(item.size)) {
80
+ parsed.push({
81
+ id: item.id,
82
+ type: "file",
83
+ path: item.path,
84
+ name: item.name,
85
+ mimeType: item.mimeType,
86
+ size: item.size,
87
+ ...typeof item.description === "string" ? { description: item.description } : {},
88
+ ...dimensions
89
+ });
90
+ } else {
91
+ throw new TypeError("invalid shared messaging attachment");
92
+ }
93
+ }
94
+ return parsed;
95
+ }
96
+ function pendingInput(value) {
97
+ const item = record(value);
98
+ if (!item || typeof item.id !== "string" || typeof item.content !== "string") {
99
+ throw new TypeError("invalid shared messaging pending item");
100
+ }
101
+ return {
102
+ id: item.id,
103
+ content: item.content,
104
+ attachments: attachments(item.attachments),
105
+ createdAt: finiteNumber(item.createdAt, "pending createdAt"),
106
+ updatedAt: finiteNumber(item.updatedAt, "pending updatedAt"),
107
+ ...origin(item)
108
+ };
109
+ }
110
+ function parseSharedMessagingSnapshot(value) {
111
+ const root = record(value);
112
+ const pending = record(root?.pending);
113
+ const draft = record(root?.draft);
114
+ if (!root || root.version !== 1 || !pending || pending.version !== 1 || !draft || draft.version !== 1) {
115
+ throw new TypeError("unsupported or malformed shared messaging snapshot");
116
+ }
117
+ if (!Array.isArray(pending.items) || typeof draft.content !== "string") {
118
+ throw new TypeError("malformed shared messaging snapshot");
119
+ }
120
+ return {
121
+ version: 1,
122
+ pending: {
123
+ version: 1,
124
+ revision: finiteNumber(pending.revision, "pending revision"),
125
+ items: pending.items.map(pendingInput)
126
+ },
127
+ draft: {
128
+ version: 1,
129
+ revision: finiteNumber(draft.revision, "draft revision"),
130
+ content: draft.content,
131
+ attachments: attachments(draft.attachments),
132
+ updatedAt: finiteNumber(draft.updatedAt, "draft updatedAt"),
133
+ ...origin(draft)
134
+ }
135
+ };
136
+ }
137
+
13
138
  // src/api.ts
139
+ var ApiHttpError = class extends Error {
140
+ constructor(status, message) {
141
+ super(message);
142
+ this.status = status;
143
+ this.name = "ApiHttpError";
144
+ }
145
+ status;
146
+ };
14
147
  function classifyConnectError(err, endpoint) {
15
148
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
16
149
  const codes = /* @__PURE__ */ new Set();
@@ -102,7 +235,10 @@ var ApiClient = class {
102
235
  });
103
236
  const text = await res.text();
104
237
  if (!res.ok) {
105
- throw new Error(`${init?.method || "GET"} ${pathname} -> ${res.status}: ${text.slice(0, 300)}`);
238
+ throw new ApiHttpError(
239
+ res.status,
240
+ `${init?.method || "GET"} ${pathname} -> ${res.status}: ${text.slice(0, 300)}`
241
+ );
106
242
  }
107
243
  try {
108
244
  return JSON.parse(text);
@@ -183,21 +319,36 @@ var ApiClient = class {
183
319
  */
184
320
  async listThreads(agentId, requireTags) {
185
321
  const ids = Array.isArray(agentId) ? agentId : [agentId];
186
- const pages = await Promise.all(
187
- ids.map(
322
+ const [me, ...pages] = await Promise.all([
323
+ this.currentUserId(),
324
+ ...ids.map(
188
325
  (id) => this.json(
189
326
  `/api/threads?agent_id=${encodeURIComponent(id)}&limit=100`
190
327
  ).catch(() => [])
191
328
  )
192
- );
329
+ ]);
193
330
  const arr = pages.flatMap((res) => Array.isArray(res) ? res : res.threads || []);
194
331
  return arr.map((t) => ({
195
332
  id: t.id,
196
333
  tags: Array.isArray(t.tags) ? t.tags : [],
197
334
  created_at: t.created_at,
198
335
  title: t.title,
199
- preview: t.preview || t.last_message
200
- })).filter((t) => requireTags.every((tag) => t.tags.includes(tag)));
336
+ preview: t.preview || t.last_message,
337
+ user_id: t.user_id ?? null
338
+ })).filter((t) => !me || !t.user_id || t.user_id === me).filter((t) => requireTags.every((tag) => t.tags.includes(tag)));
339
+ }
340
+ /** The authenticated user's id (cached). Null when the instance doesn't
341
+ * report one (super-admin sessions, very old instances). */
342
+ meUserId;
343
+ async currentUserId() {
344
+ if (this.meUserId !== void 0) return this.meUserId;
345
+ try {
346
+ const res = await this.json(`/api/auth/me`);
347
+ this.meUserId = typeof res?.user?.id === "string" && res.user.id ? res.user.id : null;
348
+ } catch {
349
+ this.meUserId = null;
350
+ }
351
+ return this.meUserId;
201
352
  }
202
353
  /**
203
354
  * Subagent child threads of a thread, each with its current lifecycle status
@@ -225,14 +376,85 @@ var ApiClient = class {
225
376
  * `mimeType` — which the server stores in the thread filesystem and injects
226
377
  * into the LLM's vision context as real image content blocks.
227
378
  */
228
- async sendMessage(threadId, content, attachments) {
379
+ async sendMessage(threadId, content, attachments2) {
229
380
  const body = { role: "user", content };
230
- if (attachments && attachments.length > 0) body.attachments = attachments;
381
+ if (attachments2 && attachments2.length > 0) body.attachments = attachments2;
231
382
  await this.json(`/api/threads/${threadId}/messages`, {
232
383
  method: "POST",
233
384
  body: JSON.stringify(body)
234
385
  });
235
386
  }
387
+ // ── portable Standard Code shared messaging endpoints ────────────────────
388
+ messagingPath(threadId, suffix = "") {
389
+ return `/api/threads/${threadId}${SHARED_MESSAGING_ROUTE}${suffix}`;
390
+ }
391
+ async getSharedMessaging(threadId) {
392
+ return parseSharedMessagingSnapshot(await this.json(this.messagingPath(threadId)));
393
+ }
394
+ async appendPendingInput(threadId, mutation) {
395
+ return parseSharedMessagingSnapshot(
396
+ await this.json(this.messagingPath(threadId, "/pending"), {
397
+ method: "POST",
398
+ body: JSON.stringify(mutation)
399
+ })
400
+ );
401
+ }
402
+ async steerInput(threadId, mutation) {
403
+ return parseSharedMessagingSnapshot(
404
+ await this.json(this.messagingPath(threadId, "/steer"), {
405
+ method: "POST",
406
+ body: JSON.stringify(mutation)
407
+ })
408
+ );
409
+ }
410
+ async updatePendingInput(threadId, pendingId, mutation) {
411
+ return parseSharedMessagingSnapshot(
412
+ await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}`), {
413
+ method: "PATCH",
414
+ body: JSON.stringify(mutation)
415
+ })
416
+ );
417
+ }
418
+ async dismissPendingInput(threadId, pendingId, origin2) {
419
+ return parseSharedMessagingSnapshot(
420
+ await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}`), {
421
+ method: "DELETE",
422
+ body: JSON.stringify(origin2)
423
+ })
424
+ );
425
+ }
426
+ async steerPendingInput(threadId, pendingId, origin2) {
427
+ return parseSharedMessagingSnapshot(
428
+ await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}/steer`), {
429
+ method: "POST",
430
+ body: JSON.stringify(origin2)
431
+ })
432
+ );
433
+ }
434
+ async putSharedDraft(threadId, mutation) {
435
+ return parseSharedMessagingSnapshot(
436
+ await this.json(this.messagingPath(threadId, "/draft"), {
437
+ method: "PUT",
438
+ body: JSON.stringify(mutation)
439
+ })
440
+ );
441
+ }
442
+ async clearSharedDraft(threadId, origin2) {
443
+ return parseSharedMessagingSnapshot(
444
+ await this.json(this.messagingPath(threadId, "/draft"), {
445
+ method: "DELETE",
446
+ body: JSON.stringify(origin2)
447
+ })
448
+ );
449
+ }
450
+ async requestSharedStop(threadId, origin2) {
451
+ return parseSharedMessagingSnapshot(
452
+ await this.json(this.messagingPath(threadId, "/stop"), {
453
+ method: "POST",
454
+ body: JSON.stringify(origin2)
455
+ })
456
+ );
457
+ }
236
458
  async getMessages(threadId, limit = 50, order) {
237
459
  const orderParam = order ? `&order=${order}` : "";
238
460
  const res = await this.json(
@@ -240,6 +462,14 @@ var ApiClient = class {
240
462
  );
241
463
  return Array.isArray(res) ? res : res.messages || [];
242
464
  }
465
+ /**
466
+ * One server-derived projection for busy/idle, current tool, conversation,
467
+ * goal, and live children. Streams provide immediacy; this snapshot settles
468
+ * state after reconnects and prevents each UI from inventing lifecycle rules.
469
+ */
470
+ async getSessionState(threadId, limit = 500) {
471
+ return this.json(`/api/threads/${threadId}/session_state?limit=${limit}`);
472
+ }
243
473
  async getLogs(threadId, limit = 100) {
244
474
  const res = await this.json(
245
475
  `/api/threads/${threadId}/logs?limit=${limit}&order=desc`
@@ -403,12 +633,6 @@ var ApiClient = class {
403
633
  async compact(threadId) {
404
634
  await this.json(`/api/threads/${threadId}/compact`, { method: "POST" });
405
635
  }
406
- async stop(threadId) {
407
- try {
408
- await this.json(`/api/threads/${threadId}/stop`, { method: "POST" });
409
- } catch {
410
- }
411
- }
412
636
  /**
413
637
  * Run a user-typed `!command` on the thread's execution owner (wherever the
414
638
  * session runs — e.g. a remote VPS daemon). The instance forwards it over
@@ -504,6 +728,7 @@ var Heartbeat = class {
504
728
  this.onDead = onDead;
505
729
  this.intervalMs = options.intervalMs ?? HEARTBEAT_INTERVAL_MS;
506
730
  this.silenceMs = options.silenceMs ?? CONNECTION_SILENCE_TIMEOUT_MS;
731
+ this.request = options.request ?? "ping";
507
732
  }
508
733
  ws;
509
734
  onDead;
@@ -511,6 +736,7 @@ var Heartbeat = class {
511
736
  lastRecvAt = 0;
512
737
  intervalMs;
513
738
  silenceMs;
739
+ request;
514
740
  start() {
515
741
  this.stop();
516
742
  this.lastRecvAt = Date.now();
@@ -520,6 +746,10 @@ var Heartbeat = class {
520
746
  markAlive() {
521
747
  this.lastRecvAt = Date.now();
522
748
  }
749
+ /** Change the heartbeat frame without restarting the connection timer. */
750
+ setRequest(request) {
751
+ this.request = request;
752
+ }
523
753
  stop() {
524
754
  if (this.timer) {
525
755
  clearInterval(this.timer);
@@ -532,7 +762,7 @@ var Heartbeat = class {
532
762
  return;
533
763
  }
534
764
  try {
535
- if (this.ws.readyState === WebSocket.OPEN) this.ws.send("ping");
765
+ if (this.ws.readyState === WebSocket.OPEN) this.ws.send(this.request);
536
766
  else this.fail();
537
767
  } catch {
538
768
  this.fail();
@@ -554,6 +784,56 @@ var DIM = "\x1B[2m";
554
784
  var ADD_BG = "\x1B[48;5;22m\x1B[38;5;254m";
555
785
  var DEL_BG = "\x1B[48;5;52m\x1B[38;5;254m";
556
786
  var MAX_SIDE_LINES = 4;
787
+ function terminalGlyphWidth(ch) {
788
+ const cp = ch.codePointAt(0);
789
+ return cp >= 4352 && cp <= 4447 || cp >= 11904 && cp <= 42191 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 127744 && cp <= 129791 || cp >= 131072 ? 2 : 1;
790
+ }
791
+ function truncateMiddle(text, maxColumns) {
792
+ const limit = Math.max(0, Math.floor(maxColumns));
793
+ if (limit === 0) return "";
794
+ const chars = [...text];
795
+ const totalWidth = chars.reduce((width, ch) => width + terminalGlyphWidth(ch), 0);
796
+ if (totalWidth <= limit) return text;
797
+ if (limit === 1) return "\u2026";
798
+ const available = limit - 1;
799
+ const headBudget = Math.ceil(available / 2);
800
+ const tailBudget = Math.floor(available / 2);
801
+ const head = [];
802
+ const tail = [];
803
+ let headWidth = 0;
804
+ let tailWidth = 0;
805
+ for (let i = 0; i < chars.length; i++) {
806
+ const width = terminalGlyphWidth(chars[i]);
807
+ if (headWidth + width > headBudget) break;
808
+ head.push(chars[i]);
809
+ headWidth += width;
810
+ }
811
+ for (let i = chars.length - 1; i >= head.length; i--) {
812
+ const width = terminalGlyphWidth(chars[i]);
813
+ if (tailWidth + width > tailBudget) break;
814
+ tail.unshift(chars[i]);
815
+ tailWidth += width;
816
+ }
817
+ const isSeparator = (ch) => ch === "/" || ch === "\\";
818
+ let headEnd = head.length;
819
+ let tailStart = chars.length - tail.length;
820
+ for (let i = headEnd - 1; i >= 0; i--) {
821
+ if (isSeparator(chars[i])) {
822
+ headEnd = i + 1;
823
+ break;
824
+ }
825
+ }
826
+ for (let i = tailStart; i < chars.length; i++) {
827
+ if (isSeparator(chars[i])) {
828
+ tailStart = i;
829
+ break;
830
+ }
831
+ }
832
+ if (headEnd < tailStart && headEnd > 0 && tailStart < chars.length) {
833
+ return `${chars.slice(0, headEnd).join("")}\u2026${chars.slice(tailStart).join("")}`;
834
+ }
835
+ return `${head.join("")}\u2026${tail.join("")}`;
836
+ }
557
837
  function clamp(s, max) {
558
838
  return s.length > max ? s.slice(0, Math.max(0, max - 1)) + "\u2026" : s;
559
839
  }
@@ -727,6 +1007,7 @@ var Bridge = class {
727
1007
  ws = null;
728
1008
  closed = false;
729
1009
  heartbeat = null;
1010
+ activeToolRequests = 0;
730
1011
  reconnectAttempt = 0;
731
1012
  reconnectTimer = null;
732
1013
  resolveConnected = null;
@@ -850,9 +1131,14 @@ var Bridge = class {
850
1131
  }
851
1132
  startHeartbeat(ws) {
852
1133
  this.stopHeartbeat();
853
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
1134
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), {
1135
+ request: this.activeToolRequests > 0 ? "ping" : "stream_ping"
1136
+ });
854
1137
  this.heartbeat.start();
855
1138
  }
1139
+ updateHeartbeatMode() {
1140
+ this.heartbeat?.setRequest(this.activeToolRequests > 0 ? "ping" : "stream_ping");
1141
+ }
856
1142
  stopHeartbeat() {
857
1143
  if (this.heartbeat) {
858
1144
  this.heartbeat.stop();
@@ -938,7 +1224,14 @@ var Bridge = class {
938
1224
  if (msg.type !== "tool_request") return;
939
1225
  if (!this.owner) return;
940
1226
  const req = msg;
941
- await this.handleToolRequest(req);
1227
+ this.activeToolRequests++;
1228
+ this.updateHeartbeatMode();
1229
+ try {
1230
+ await this.handleToolRequest(req);
1231
+ } finally {
1232
+ this.activeToolRequests = Math.max(0, this.activeToolRequests - 1);
1233
+ this.updateHeartbeatMode();
1234
+ }
942
1235
  }
943
1236
  /**
944
1237
  * Reply to a tool request. Durable calls (the agent parked them) deliver the
@@ -1112,7 +1405,7 @@ function detailSuffix(tool, result) {
1112
1405
  const lines = result.split("\n").length;
1113
1406
  return ` (${lines} line${lines === 1 ? "" : "s"})`;
1114
1407
  }
1115
- var LOG_DIR = path3.join(os10.homedir(), ".standardagents", "process-logs");
1408
+ var LOG_DIR = path3.join(os7.homedir(), ".standardagents", "process-logs");
1116
1409
  var KEY2 = "bg_processes";
1117
1410
  function isAlive(pid) {
1118
1411
  try {
@@ -1189,7 +1482,7 @@ var ProcessRegistry = class {
1189
1482
  }
1190
1483
  };
1191
1484
  function configFile() {
1192
- return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(os10.homedir(), ".standardagents", "mcp.json");
1485
+ return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(os7.homedir(), ".standardagents", "mcp.json");
1193
1486
  }
1194
1487
  function loadMcpConfig() {
1195
1488
  try {
@@ -1491,7 +1784,7 @@ var HostTools = class {
1491
1784
  }
1492
1785
  }
1493
1786
  const hash = crypto.createHash("sha256").update(JSON.stringify(files)).digest("hex").slice(0, 12);
1494
- const skillDir = path3.join(os10.tmpdir(), "standardcode-skills", `${skill}-${hash}`);
1787
+ const skillDir = path3.join(os7.tmpdir(), "standardcode-skills", `${skill}-${hash}`);
1495
1788
  for (const f of files) {
1496
1789
  const dest = path3.resolve(skillDir, f.path);
1497
1790
  if (path3.relative(skillDir, dest).startsWith("..")) {
@@ -2352,6 +2645,23 @@ var ExecutionSession = class {
2352
2645
  }
2353
2646
  };
2354
2647
 
2648
+ // src/theme.ts
2649
+ function detectLight() {
2650
+ const forced = (process.env.STANDARD_CODE_THEME || "").toLowerCase();
2651
+ if (forced === "light") return true;
2652
+ if (forced === "dark") return false;
2653
+ const fgbg = process.env.COLORFGBG;
2654
+ if (fgbg) {
2655
+ const parts = fgbg.split(";");
2656
+ const bg = Number(parts[parts.length - 1]);
2657
+ if (!Number.isNaN(bg)) return bg === 7 || bg === 15 || bg >= 230;
2658
+ }
2659
+ return false;
2660
+ }
2661
+ var isLightTheme = detectLight();
2662
+ var themeWhite = isLightTheme ? "\x1B[30m" : "\x1B[97m";
2663
+ var themeGray = isLightTheme ? "\x1B[38;5;244m" : "\x1B[90m";
2664
+
2355
2665
  // src/stream.ts
2356
2666
  var MessageStream = class {
2357
2667
  constructor(api, threadId, hooks) {
@@ -2401,6 +2711,7 @@ var MessageStream = class {
2401
2711
  ws.addEventListener("open", () => {
2402
2712
  this.reconnectAttempt = 0;
2403
2713
  this.startHeartbeat(ws);
2714
+ this.hooks.onOpen?.();
2404
2715
  this.resolveConnected?.();
2405
2716
  });
2406
2717
  ws.addEventListener("message", (ev) => {
@@ -2418,7 +2729,7 @@ var MessageStream = class {
2418
2729
  }
2419
2730
  startHeartbeat(ws) {
2420
2731
  this.stopHeartbeat();
2421
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
2732
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
2422
2733
  this.heartbeat.start();
2423
2734
  }
2424
2735
  stopHeartbeat() {
@@ -2467,10 +2778,11 @@ var MessageStream = class {
2467
2778
  }
2468
2779
  if (msg.type === "message_data" && (msg.depth ?? 0) === 0) {
2469
2780
  const data = msg.data || {};
2781
+ this.hooks.onMessage?.(data);
2470
2782
  if (data.role === "assistant" && typeof data.content === "string" && data.content.trim()) {
2471
2783
  const tc = data.tool_calls;
2472
- const hasToolCalls2 = Array.isArray(tc) ? tc.length > 0 : typeof tc === "string" && tc.trim() !== "" && tc.trim() !== "null" && tc.trim() !== "[]";
2473
- this.hooks.onAssistantText(data.content, hasToolCalls2);
2784
+ const hasToolCalls = Array.isArray(tc) ? tc.length > 0 : typeof tc === "string" && tc.trim() !== "" && tc.trim() !== "null" && tc.trim() !== "[]";
2785
+ this.hooks.onAssistantText(data.content, hasToolCalls);
2474
2786
  }
2475
2787
  if (data.role === "assistant" && data.status === "failed" && data.error) {
2476
2788
  this.hooks.onError(String(data.error));
@@ -2520,7 +2832,7 @@ var SystemEvents = class {
2520
2832
  }
2521
2833
  startHeartbeat(ws) {
2522
2834
  this.stopHeartbeat();
2523
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
2835
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
2524
2836
  this.heartbeat.start();
2525
2837
  }
2526
2838
  stopHeartbeat() {
@@ -2653,6 +2965,664 @@ var SubagentActivity = class {
2653
2965
  }
2654
2966
  };
2655
2967
 
2968
+ // src/session-state.ts
2969
+ var emptyLiveDraft = () => ({ version: 1, text: "", messageIds: [] });
2970
+ function normalizeLiveDraft(state) {
2971
+ return {
2972
+ version: 1,
2973
+ text: typeof state?.text === "string" ? state.text : "",
2974
+ messageIds: Array.isArray(state?.messageIds) ? [...new Set(state.messageIds.filter((id) => typeof id === "string" && !!id))] : []
2975
+ };
2976
+ }
2977
+ function appendLiveDraft(state, chunks) {
2978
+ const current = normalizeLiveDraft(state);
2979
+ const nextIds = [...current.messageIds];
2980
+ let text = current.text;
2981
+ for (const chunk of Array.isArray(chunks) ? chunks : [chunks]) {
2982
+ if (typeof chunk?.text !== "string" || !chunk.text) continue;
2983
+ text += chunk.text;
2984
+ if (typeof chunk.messageId === "string" && chunk.messageId && !nextIds.includes(chunk.messageId)) {
2985
+ nextIds.push(chunk.messageId);
2986
+ }
2987
+ }
2988
+ return { version: 1, text, messageIds: nextIds.slice(-64) };
2989
+ }
2990
+ var createdAt = (message) => Number(message.created_at ?? message.createdAt ?? 0);
2991
+ function messageText(content) {
2992
+ if (typeof content === "string") return content;
2993
+ if (Array.isArray(content)) {
2994
+ return content.map((block) => typeof block === "string" ? block : typeof block?.text === "string" ? block.text : "").join("");
2995
+ }
2996
+ return "";
2997
+ }
2998
+ function parseObject(value) {
2999
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
3000
+ if (typeof value !== "string" || !value.trim()) return {};
3001
+ try {
3002
+ const parsed = JSON.parse(value);
3003
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
3004
+ } catch {
3005
+ return {};
3006
+ }
3007
+ }
3008
+ function parseToolCalls(message) {
3009
+ if (Array.isArray(message.toolCalls)) return message.toolCalls;
3010
+ let raw = message.tool_calls;
3011
+ if (typeof raw === "string") {
3012
+ try {
3013
+ raw = JSON.parse(raw);
3014
+ } catch {
3015
+ return [];
3016
+ }
3017
+ }
3018
+ if (!Array.isArray(raw)) return [];
3019
+ return raw.flatMap((item) => {
3020
+ const id = typeof item?.id === "string" ? item.id : "";
3021
+ const nameValue = item?.function?.name ?? item?.name;
3022
+ const name = typeof nameValue === "string" ? nameValue : "";
3023
+ if (!id || !name) return [];
3024
+ return [{ id, name, arguments: parseObject(item?.function?.arguments ?? item?.arguments ?? item?.args) }];
3025
+ });
3026
+ }
3027
+ function deriveSessionActivity(messages) {
3028
+ const visible = messages.filter((message) => message.silent !== true && message.metadata?.silent !== true).sort((a, b) => createdAt(a) - createdAt(b));
3029
+ if (visible.some((message) => message.status === "pending")) return { busy: true, currentTool: unresolvedTool(visible) };
3030
+ const last = visible.at(-1);
3031
+ if (!last) return { busy: false, currentTool: null };
3032
+ const currentTool = unresolvedTool(visible);
3033
+ if (currentTool) return { busy: true, currentTool };
3034
+ if (last.role === "user" || last.role === "tool") return { busy: true, currentTool: null };
3035
+ if (last.role === "assistant" && last.status !== "failed" && !messageText(last.content).trim()) {
3036
+ return { busy: true, currentTool: null };
3037
+ }
3038
+ return { busy: false, currentTool: null };
3039
+ }
3040
+ function threadBusy(messages) {
3041
+ return deriveSessionActivity(messages).busy;
3042
+ }
3043
+ function unresolvedTool(messages) {
3044
+ const resultIds = new Set(messages.flatMap((message) => {
3045
+ const id = message.tool_call_id ?? message.toolCallId;
3046
+ return message.role === "tool" && typeof id === "string" ? [id] : [];
3047
+ }));
3048
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
3049
+ if (messages[i].role === "user") return null;
3050
+ if (messages[i].role !== "assistant") continue;
3051
+ const calls = parseToolCalls(messages[i]);
3052
+ if (!calls.length) return null;
3053
+ return calls.find((tool) => !resultIds.has(tool.id)) ?? null;
3054
+ }
3055
+ return null;
3056
+ }
3057
+
3058
+ // src/transcript-delivery.ts
3059
+ function transcriptMessageReady(message, text) {
3060
+ if (message.status === "pending") return false;
3061
+ if (message.role === "assistant" && message.status !== "failed" && !text.trim()) return false;
3062
+ return true;
3063
+ }
3064
+
3065
+ // src/progressive-markdown.ts
3066
+ var punctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
3067
+ var isWhitespace = (value) => value === void 0 || /\s/.test(value);
3068
+ var isPunctuation = (value) => value !== void 0 && punctuation.test(value);
3069
+ function runLength(source, start, marker) {
3070
+ let end = start;
3071
+ while (source[end] === marker) end++;
3072
+ return end - start;
3073
+ }
3074
+ function delimiterFlanking(source, at, length, marker) {
3075
+ const previous = at > 0 && source[at - 1] !== "\n" ? source[at - 1] : void 0;
3076
+ const next = at + length < source.length && source[at + length] !== "\n" ? source[at + length] : void 0;
3077
+ const previousWhitespace = isWhitespace(previous);
3078
+ const nextWhitespace = isWhitespace(next);
3079
+ const previousPunctuation = isPunctuation(previous);
3080
+ const nextPunctuation = isPunctuation(next);
3081
+ const leftFlanking = !nextWhitespace && (!nextPunctuation || previousWhitespace || previousPunctuation);
3082
+ const rightFlanking = !previousWhitespace && (!previousPunctuation || nextWhitespace || nextPunctuation);
3083
+ if (marker === "_") {
3084
+ return {
3085
+ canOpen: leftFlanking && (!rightFlanking || previousPunctuation),
3086
+ canClose: rightFlanking && (!leftFlanking || nextPunctuation)
3087
+ };
3088
+ }
3089
+ return { canOpen: leftFlanking, canClose: rightFlanking };
3090
+ }
3091
+ function sameStyles(a, b) {
3092
+ return a.length === b.length && a.every((style, index) => style === b[index]);
3093
+ }
3094
+ function parseProgressiveInline(source, baseOffset = 0) {
3095
+ const runs = [];
3096
+ const stack = [];
3097
+ let inlineTicks = 0;
3098
+ let i = 0;
3099
+ const styles = () => [
3100
+ ...stack.map((frame) => frame.style),
3101
+ ...inlineTicks > 0 ? ["code"] : []
3102
+ ];
3103
+ const append = (text, at, href) => {
3104
+ if (!text) return;
3105
+ const active = styles();
3106
+ const last = runs.at(-1);
3107
+ if (last && last._end === at && last.href === href && sameStyles(last.styles, active)) {
3108
+ last.text += text;
3109
+ last._end = at + text.length;
3110
+ return;
3111
+ }
3112
+ runs.push({ id: `i:${baseOffset + at}`, text, styles: active, ...{}, _end: at + text.length });
3113
+ };
3114
+ while (i < source.length) {
3115
+ const character = source[i];
3116
+ if (character === "\\") {
3117
+ if (i + 1 < source.length && isPunctuation(source[i + 1])) {
3118
+ append(source[i + 1], i + 1);
3119
+ i += 2;
3120
+ continue;
3121
+ }
3122
+ if (i + 1 === source.length) {
3123
+ i++;
3124
+ continue;
3125
+ }
3126
+ }
3127
+ if (inlineTicks > 0) {
3128
+ if (character === "`") {
3129
+ const length = runLength(source, i, "`");
3130
+ if (length === inlineTicks) {
3131
+ inlineTicks = 0;
3132
+ i += length;
3133
+ continue;
3134
+ }
3135
+ if (i + length === source.length) {
3136
+ i += length;
3137
+ continue;
3138
+ }
3139
+ append("`".repeat(length), i);
3140
+ i += length;
3141
+ continue;
3142
+ }
3143
+ append(character, i);
3144
+ i++;
3145
+ continue;
3146
+ }
3147
+ if (character === "`") {
3148
+ const length = runLength(source, i, "`");
3149
+ if (i + length < source.length) inlineTicks = length;
3150
+ i += length;
3151
+ continue;
3152
+ }
3153
+ if (character === "[") {
3154
+ const destinationAt = source.indexOf("](", i + 1);
3155
+ if (destinationAt >= 0) {
3156
+ const destinationEnd = source.indexOf(")", destinationAt + 2);
3157
+ const end = destinationEnd >= 0 ? destinationEnd : source.length;
3158
+ const href = source.slice(destinationAt + 2, end);
3159
+ const labelStart = i + 1;
3160
+ const labelRuns = parseProgressiveInline(source.slice(labelStart, destinationAt), baseOffset + labelStart);
3161
+ for (const run3 of labelRuns) {
3162
+ runs.push({
3163
+ ...run3,
3164
+ styles: run3.styles.includes("link") ? run3.styles : [...run3.styles, "link"],
3165
+ ...destinationEnd >= 0 && href ? { href } : {},
3166
+ _end: run3.id.startsWith("i:") ? Number(run3.id.slice(2)) - baseOffset + run3.text.length : end
3167
+ });
3168
+ }
3169
+ i = destinationEnd >= 0 ? destinationEnd + 1 : source.length;
3170
+ continue;
3171
+ }
3172
+ }
3173
+ if (character === "*" || character === "_" || character === "~") {
3174
+ const run3 = runLength(source, i, character);
3175
+ const usable = character === "~" ? run3 - run3 % 2 : run3;
3176
+ if (usable > 0) {
3177
+ const { canOpen, canClose } = delimiterFlanking(source, i, run3, character);
3178
+ const frames = [];
3179
+ if (character === "~") {
3180
+ for (let n = 0; n < usable; n += 2) frames.push({ marker: "~", length: 2, style: "strike" });
3181
+ } else {
3182
+ let remaining = usable;
3183
+ while (remaining >= 2) {
3184
+ frames.push({ marker: character, length: 2, style: "strong" });
3185
+ remaining -= 2;
3186
+ }
3187
+ if (remaining) frames.push({ marker: character, length: 1, style: "emphasis" });
3188
+ }
3189
+ let consumed = 0;
3190
+ let closed = 0;
3191
+ if (canClose) {
3192
+ for (const frame of [...frames].reverse()) {
3193
+ const top = stack.at(-1);
3194
+ if (top && top.marker === frame.marker && top.length === frame.length) {
3195
+ stack.pop();
3196
+ consumed += frame.length;
3197
+ closed += frame.length;
3198
+ }
3199
+ }
3200
+ }
3201
+ if (canOpen) {
3202
+ for (const frame of frames) {
3203
+ if (consumed >= frame.length) consumed -= frame.length;
3204
+ else stack.push(frame);
3205
+ }
3206
+ }
3207
+ if (canOpen || closed > 0) {
3208
+ i += usable;
3209
+ if (run3 > usable) append(character.repeat(run3 - usable), i);
3210
+ i += run3 - usable;
3211
+ continue;
3212
+ }
3213
+ if (i + run3 === source.length) {
3214
+ i += run3;
3215
+ continue;
3216
+ }
3217
+ }
3218
+ if (usable === 0 && i + run3 === source.length) {
3219
+ i += run3;
3220
+ continue;
3221
+ }
3222
+ }
3223
+ append(character, i);
3224
+ i++;
3225
+ }
3226
+ return runs.map(({ _end: _, ...run3 }) => run3);
3227
+ }
3228
+ function sourceLines(source) {
3229
+ if (!source) return [];
3230
+ const lines = [];
3231
+ let start = 0;
3232
+ while (start <= source.length) {
3233
+ const newline = source.indexOf("\n", start);
3234
+ if (newline < 0) {
3235
+ lines.push({ text: source.slice(start).replace(/\r$/, ""), start, end: source.length, next: source.length, terminated: false });
3236
+ break;
3237
+ }
3238
+ lines.push({ text: source.slice(start, newline).replace(/\r$/, ""), start, end: newline, next: newline + 1, terminated: true });
3239
+ start = newline + 1;
3240
+ if (start === source.length) {
3241
+ lines.push({ text: "", start, end: start, next: start, terminated: false });
3242
+ break;
3243
+ }
3244
+ }
3245
+ return lines;
3246
+ }
3247
+ function blockFence(line) {
3248
+ const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
3249
+ if (!match) return null;
3250
+ const marker = match[1][0];
3251
+ const info = match[2];
3252
+ if (marker === "`" && info.includes("`")) return null;
3253
+ return { marker, length: match[1].length, info: info.trim() };
3254
+ }
3255
+ var tableSeparator = (line) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line) && line.includes("-");
3256
+ var partialTableSeparator = (line) => /^\s*\|?\s*:?-*:?\s*(\|\s*:?-*:?\s*)*\|?\s*$/.test(line) && /[-|:]/.test(line);
3257
+ function tableCells(line) {
3258
+ let start = 0;
3259
+ let end = line.length;
3260
+ while (start < end && /\s/.test(line[start])) start++;
3261
+ while (end > start && /\s/.test(line[end - 1])) end--;
3262
+ if (line[start] === "|") start++;
3263
+ if (line[end - 1] === "|") end--;
3264
+ const cells = [];
3265
+ let cellStart = start;
3266
+ for (let i = start; i <= end; i++) {
3267
+ if (i === end || line[i] === "|" && (i === 0 || line[i - 1] !== "\\")) {
3268
+ const raw = line.slice(cellStart, i);
3269
+ const leading = raw.match(/^\s*/)?.[0].length ?? 0;
3270
+ cells.push({ text: raw.trim().replace(/\\\|/g, "|"), offset: cellStart + leading });
3271
+ cellStart = i + 1;
3272
+ }
3273
+ }
3274
+ return cells;
3275
+ }
3276
+ function cell(text, offset) {
3277
+ return { id: `cell:${offset}`, runs: parseProgressiveInline(text, offset) };
3278
+ }
3279
+ function startsSpecialBlock(lines, index) {
3280
+ const line = lines[index]?.text ?? "";
3281
+ const next = lines[index + 1]?.text;
3282
+ return !!blockFence(line) || /^#{1,6}\s+/.test(line) || /^\s*>\s?/.test(line) || /^\s*[-*+]\s+/.test(line) || /^\s*\d+[.)]\s+/.test(line) || /^\s*([-*_])(\s*\1){2,}\s*$/.test(line) || /^ {0,3}\|/.test(line) || line.includes("|") && next !== void 0 && tableSeparator(next);
3283
+ }
3284
+ function parseStreamingMarkdown(source) {
3285
+ const lines = sourceLines(source);
3286
+ const blocks = [];
3287
+ let i = 0;
3288
+ while (i < lines.length) {
3289
+ const line = lines[i];
3290
+ if (!line.text.trim()) {
3291
+ i++;
3292
+ continue;
3293
+ }
3294
+ const id = `b:${line.start}`;
3295
+ const opening = blockFence(line.text);
3296
+ if (opening) {
3297
+ const codeStart = line.next;
3298
+ let j2 = i + 1;
3299
+ let closing;
3300
+ while (j2 < lines.length) {
3301
+ const candidate = blockFence(lines[j2].text);
3302
+ if (candidate && candidate.marker === opening.marker && candidate.length >= opening.length && !candidate.info) {
3303
+ closing = lines[j2];
3304
+ break;
3305
+ }
3306
+ j2++;
3307
+ }
3308
+ let codeEnd = closing?.start ?? source.length;
3309
+ if (!closing) {
3310
+ const tailStart = Math.max(codeStart, source.lastIndexOf("\n") + 1);
3311
+ const tail = source.slice(tailStart);
3312
+ const pending = /^ {0,3}(`+|~+)[ \t]*$/.exec(tail);
3313
+ if (pending && pending[1][0] === opening.marker && pending[1].length < opening.length) codeEnd = tailStart;
3314
+ }
3315
+ if (codeEnd > codeStart && source[codeEnd - 1] === "\n") codeEnd--;
3316
+ blocks.push({
3317
+ id,
3318
+ kind: "code",
3319
+ start: line.start,
3320
+ end: closing?.next ?? source.length,
3321
+ complete: !!closing && closing.terminated,
3322
+ language: opening.info || "code",
3323
+ text: source.slice(codeStart, codeEnd)
3324
+ });
3325
+ i = closing ? j2 + 1 : lines.length;
3326
+ continue;
3327
+ }
3328
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line.text);
3329
+ if (heading) {
3330
+ const contentAt = line.start + heading[1].length + 1;
3331
+ blocks.push({
3332
+ id,
3333
+ kind: "heading",
3334
+ start: line.start,
3335
+ end: line.end,
3336
+ complete: line.terminated,
3337
+ level: heading[1].length,
3338
+ runs: parseProgressiveInline(heading[2], contentAt)
3339
+ });
3340
+ i++;
3341
+ continue;
3342
+ }
3343
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line.text)) {
3344
+ blocks.push({ id, kind: "rule", start: line.start, end: line.end, complete: line.terminated });
3345
+ i++;
3346
+ continue;
3347
+ }
3348
+ const separatorNext = line.text.includes("|") && i + 1 < lines.length && tableSeparator(lines[i + 1].text);
3349
+ if (separatorNext) {
3350
+ const separator = tableCells(lines[i + 1].text);
3351
+ const align = separator.map(({ text: text2 }) => text2.startsWith(":") && text2.endsWith(":") ? "center" : text2.endsWith(":") ? "right" : text2.startsWith(":") ? "left" : "");
3352
+ const header = tableCells(line.text).map((entry) => cell(entry.text, line.start + entry.offset));
3353
+ const rows = [];
3354
+ let j2 = i + 2;
3355
+ while (j2 < lines.length && lines[j2].text.trim() && lines[j2].text.includes("|")) {
3356
+ rows.push(tableCells(lines[j2].text).map((entry) => cell(entry.text, lines[j2].start + entry.offset)));
3357
+ j2++;
3358
+ }
3359
+ blocks.push({
3360
+ id,
3361
+ kind: "table",
3362
+ start: line.start,
3363
+ end: lines[Math.max(i + 1, j2 - 1)].end,
3364
+ complete: j2 < lines.length && lines[j2].terminated,
3365
+ header,
3366
+ rows,
3367
+ align
3368
+ });
3369
+ i = j2;
3370
+ continue;
3371
+ }
3372
+ if (/^ {0,3}\|/.test(line.text)) {
3373
+ const streamingSeparator = i + 1 >= lines.length || !lines[i + 1].terminated && (!lines[i + 1].text || partialTableSeparator(lines[i + 1].text));
3374
+ if (streamingSeparator) {
3375
+ const header = tableCells(line.text).map((entry) => cell(entry.text, line.start + entry.offset));
3376
+ if (header.some((entry) => entry.runs.length)) {
3377
+ blocks.push({
3378
+ id,
3379
+ kind: "table",
3380
+ start: line.start,
3381
+ end: lines[Math.min(i + 1, lines.length - 1)].end,
3382
+ complete: false,
3383
+ header,
3384
+ rows: []
3385
+ });
3386
+ } else {
3387
+ blocks.push({ id, kind: "paragraph", start: line.start, end: line.end, complete: false, runs: [] });
3388
+ }
3389
+ i = lines.length;
3390
+ continue;
3391
+ }
3392
+ }
3393
+ const quote = /^\s*>\s?(.*)$/.exec(line.text);
3394
+ if (quote) {
3395
+ const parts = [];
3396
+ let j2 = i;
3397
+ let firstContentAt = line.start + line.text.indexOf(quote[1]);
3398
+ while (j2 < lines.length) {
3399
+ const match = /^\s*>\s?(.*)$/.exec(lines[j2].text);
3400
+ if (!match) break;
3401
+ if (!parts.length) firstContentAt = lines[j2].start + lines[j2].text.indexOf(match[1]);
3402
+ parts.push(match[1]);
3403
+ j2++;
3404
+ }
3405
+ blocks.push({
3406
+ id,
3407
+ kind: "quote",
3408
+ start: line.start,
3409
+ end: lines[j2 - 1].end,
3410
+ complete: j2 < lines.length && lines[j2].terminated,
3411
+ runs: parseProgressiveInline(parts.join("\n"), firstContentAt)
3412
+ });
3413
+ i = j2;
3414
+ continue;
3415
+ }
3416
+ const list = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line.text);
3417
+ if (list) {
3418
+ const ordered = /^\d/.test(list[2]);
3419
+ const items = [];
3420
+ let j2 = i;
3421
+ while (j2 < lines.length) {
3422
+ const match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(lines[j2].text);
3423
+ if (!match || /^\d/.test(match[2]) !== ordered) break;
3424
+ const contentAt = lines[j2].start + match[1].length + match[2].length + 1;
3425
+ items.push(cell(match[3], contentAt));
3426
+ j2++;
3427
+ }
3428
+ blocks.push({
3429
+ id,
3430
+ kind: "list",
3431
+ start: line.start,
3432
+ end: lines[j2 - 1].end,
3433
+ complete: j2 < lines.length && lines[j2].terminated,
3434
+ ordered,
3435
+ items
3436
+ });
3437
+ i = j2;
3438
+ continue;
3439
+ }
3440
+ if (!line.terminated && (/^ {0,3}#{1,6}\s*$/.test(line.text) || /^\s*[-*_]{1,2}\s*$/.test(line.text))) {
3441
+ blocks.push({ id, kind: "paragraph", start: line.start, end: line.end, complete: false, runs: [] });
3442
+ i++;
3443
+ continue;
3444
+ }
3445
+ const paragraph = [line];
3446
+ let j = i + 1;
3447
+ while (j < lines.length && lines[j].text.trim() && !startsSpecialBlock(lines, j)) {
3448
+ paragraph.push(lines[j]);
3449
+ j++;
3450
+ }
3451
+ const text = paragraph.map((part) => part.text).join("\n");
3452
+ blocks.push({
3453
+ id,
3454
+ kind: "paragraph",
3455
+ start: line.start,
3456
+ end: paragraph.at(-1).end,
3457
+ complete: j < lines.length && lines[j].terminated,
3458
+ runs: parseProgressiveInline(text, line.start)
3459
+ });
3460
+ i = j;
3461
+ }
3462
+ return { version: 3, sourceLength: source.length, blocks };
3463
+ }
3464
+
3465
+ // src/markdown.ts
3466
+ var ESC = "\x1B[";
3467
+ var R = ESC + "0m";
3468
+ var BOLD = ESC + "1m";
3469
+ var DIM3 = ESC + "2m";
3470
+ var ITAL = ESC + "3m";
3471
+ var UNDER = ESC + "4m";
3472
+ var TEAL = ESC + "38;5;37m";
3473
+ var CYAN = ESC + "36m";
3474
+ var GRAY = ESC + "90m";
3475
+ var ANSI = /\x1b\[[0-9;]*m/g;
3476
+ function visibleWidth(s) {
3477
+ return s.replace(ANSI, "").length;
3478
+ }
3479
+ function padEndVisible(s, width) {
3480
+ const pad = width - visibleWidth(s);
3481
+ return pad > 0 ? s + " ".repeat(pad) : s;
3482
+ }
3483
+ function inline(s) {
3484
+ const codes = [];
3485
+ s = s.replace(/`([^`]+)`/g, (_, code) => {
3486
+ codes.push(code);
3487
+ return "\0" + (codes.length - 1) + "\0";
3488
+ });
3489
+ s = s.replace(
3490
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
3491
+ (_, text, url) => `${CYAN}${UNDER}${text}${R} ${DIM3}${url}${R}`
3492
+ );
3493
+ s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => `${BOLD}${t}${R}`);
3494
+ s = s.replace(/\*([^*\n]+)\*/g, (_, t) => `${ITAL}${t}${R}`);
3495
+ s = s.replace(/__([^_]+)__/g, (_, t) => `${BOLD}${t}${R}`);
3496
+ s = s.replace(/(^|[^\w])_([^_\n]+)_($|[^\w])/g, (_, a, t, b) => `${a}${ITAL}${t}${R}${b}`);
3497
+ s = s.replace(/~~([^~]+)~~/g, (_, t) => `${DIM3}${t}${R}`);
3498
+ s = s.replace(/\x00(\d+)\x00/g, (_, i) => `${TEAL}${codes[+i].replace(/ /g, String.fromCharCode(160))}${R}`);
3499
+ return s;
3500
+ }
3501
+ function wrapStyled(text, width) {
3502
+ if (width < 4 || visibleWidth(text) <= width) return [text];
3503
+ const words = text.split(" ");
3504
+ const lines = [];
3505
+ let cur = "";
3506
+ let curLen = 0;
3507
+ for (const w of words) {
3508
+ const wLen = visibleWidth(w);
3509
+ if (cur === "") {
3510
+ cur = w;
3511
+ curLen = wLen;
3512
+ } else if (curLen + 1 + wLen <= width) {
3513
+ cur += " " + w;
3514
+ curLen += 1 + wLen;
3515
+ } else {
3516
+ lines.push(cur);
3517
+ cur = w;
3518
+ curLen = wLen;
3519
+ }
3520
+ }
3521
+ if (cur !== "" || lines.length === 0) lines.push(cur);
3522
+ return lines;
3523
+ }
3524
+ function wrapBlock(out, cols2, leadFirst, leadRest, leadWidth, text) {
3525
+ const wrapped = wrapStyled(text, Math.max(8, cols2 - leadWidth));
3526
+ wrapped.forEach((ln, idx) => out.push((idx === 0 ? leadFirst : leadRest) + ln));
3527
+ }
3528
+ function renderTable(rows) {
3529
+ const cols2 = Math.max(...rows.map((r) => r.length));
3530
+ const widths = [];
3531
+ for (let c4 = 0; c4 < cols2; c4++) {
3532
+ widths[c4] = Math.max(...rows.map((r) => visibleWidth(inline(r[c4] ?? ""))));
3533
+ }
3534
+ const sep = `${GRAY} \u2502 ${R}`;
3535
+ const out = [];
3536
+ rows.forEach((r, ri) => {
3537
+ const cells = [];
3538
+ for (let c4 = 0; c4 < cols2; c4++) {
3539
+ const raw = r[c4] ?? "";
3540
+ const styled = ri === 0 ? `${BOLD}${inline(raw)}${R}` : inline(raw);
3541
+ cells.push(padEndVisible(styled, widths[c4]));
3542
+ }
3543
+ out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
3544
+ if (ri === 0) {
3545
+ const rule = widths.map((w) => `${GRAY}${"\u2500".repeat(w)}${R}`).join(`${GRAY}\u2500\u253C\u2500${R}`);
3546
+ out.push(" " + rule);
3547
+ }
3548
+ });
3549
+ return out;
3550
+ }
3551
+ function streamingRun(run3) {
3552
+ let text = run3.styles.includes("code") ? run3.text.replace(/ /g, String.fromCharCode(160)) : run3.text;
3553
+ if (run3.styles.includes("code")) text = `${TEAL}${text}${R}`;
3554
+ if (run3.styles.includes("strike")) text = `${DIM3}${text}${R}`;
3555
+ if (run3.styles.includes("strong")) text = `${BOLD}${text}${R}`;
3556
+ if (run3.styles.includes("emphasis")) text = `${ITAL}${text}${R}`;
3557
+ if (run3.href || run3.styles.includes("link")) text = `${CYAN}${UNDER}${text}${R}`;
3558
+ return text;
3559
+ }
3560
+ function streamingInline(runs) {
3561
+ return runs.map(streamingRun).join("");
3562
+ }
3563
+ function plainCell(cell2) {
3564
+ return cell2.runs.map((run3) => run3.text).join("");
3565
+ }
3566
+ function renderStreamingMarkdown(src, cols2 = 80) {
3567
+ const document = parseStreamingMarkdown(src);
3568
+ const out = [];
3569
+ for (const block of document.blocks) {
3570
+ if (out.length && out.at(-1) !== "") out.push("");
3571
+ switch (block.kind) {
3572
+ case "code": {
3573
+ const lines = (block.text ?? "").split("\n");
3574
+ if (lines.length === 1 && !lines[0]) out.push(`${GRAY}\u2502${R} `);
3575
+ else for (const line of lines) out.push(`${GRAY}\u2502${R} ${line}`);
3576
+ break;
3577
+ }
3578
+ case "heading": {
3579
+ const text = streamingInline(block.runs ?? []);
3580
+ for (const line of wrapStyled(text, cols2)) out.push(`${BOLD}${TEAL}${line}${R}`);
3581
+ break;
3582
+ }
3583
+ case "rule":
3584
+ out.push(`${GRAY}\u2500\u2500\u2500\u2500\u2500\u2500${R}`);
3585
+ break;
3586
+ case "quote": {
3587
+ const text = streamingInline(block.runs ?? []);
3588
+ for (const logical of text.split("\n")) {
3589
+ for (const line of wrapStyled(logical, Math.max(8, cols2 - 2))) out.push(`${GRAY}\u2502${R} ${DIM3}${line}${R}`);
3590
+ }
3591
+ break;
3592
+ }
3593
+ case "list": {
3594
+ for (let index = 0; index < (block.items ?? []).length; index++) {
3595
+ const marker = block.ordered ? `${index + 1}.` : "\u2022";
3596
+ const lead = block.ordered ? `${BOLD}${marker}${R} ` : `${TEAL}${marker}${R} `;
3597
+ wrapBlock(
3598
+ out,
3599
+ cols2,
3600
+ lead,
3601
+ " ".repeat(marker.length + 1),
3602
+ marker.length + 1,
3603
+ streamingInline(block.items[index].runs)
3604
+ );
3605
+ }
3606
+ break;
3607
+ }
3608
+ case "table": {
3609
+ const rows = [
3610
+ (block.header ?? []).map(plainCell),
3611
+ ...(block.rows ?? []).map((row) => row.map(plainCell))
3612
+ ];
3613
+ if (rows[0].length) out.push(...renderTable(rows));
3614
+ break;
3615
+ }
3616
+ case "paragraph": {
3617
+ const text = streamingInline(block.runs ?? []);
3618
+ for (const logical of text.split("\n")) wrapBlock(out, cols2, "", "", 0, logical);
3619
+ break;
3620
+ }
3621
+ }
3622
+ }
3623
+ return out;
3624
+ }
3625
+
2656
3626
  // src/wordmill.ts
2657
3627
  var MILL_WORDS = [
2658
3628
  "Working",
@@ -2694,8 +3664,8 @@ var HOLD_MS = 2600;
2694
3664
  var LAZY_MS = 320;
2695
3665
  var LAZY_PERIOD = 200;
2696
3666
  var FAST_PERIOD = 70;
2697
- var BOLD = "\x1B[1m";
2698
- var DIM3 = "\x1B[2m";
3667
+ var BOLD2 = "\x1B[1m";
3668
+ var DIM4 = "\x1B[2m";
2699
3669
  var OFF = "\x1B[22m";
2700
3670
  function glyphAt(slot, bucket) {
2701
3671
  let h = (slot + 1) * 2654435761 ^ (bucket + 1) * 40503;
@@ -2730,7 +3700,7 @@ var WordMill = class {
2730
3700
  if (this.phaseAt === 0) this.phaseAt = now;
2731
3701
  if (!this.target) {
2732
3702
  if (now - this.phaseAt >= HOLD_MS) this.beginMorph(now);
2733
- else return `${BOLD}${this.word}${OFF}`;
3703
+ else return `${BOLD2}${this.word}${OFF}`;
2734
3704
  }
2735
3705
  return this.morphFrame(now);
2736
3706
  }
@@ -2756,21 +3726,21 @@ var WordMill = class {
2756
3726
  this.word = target;
2757
3727
  this.target = null;
2758
3728
  this.phaseAt = now;
2759
- return `${BOLD}${this.word}${OFF}`;
3729
+ return `${BOLD2}${this.word}${OFF}`;
2760
3730
  }
2761
3731
  let out = "";
2762
3732
  for (let i = 0; i < this.slots.length; i++) {
2763
3733
  const s = this.slots[i];
2764
3734
  if (t < s.start) {
2765
3735
  const ch = this.word[i];
2766
- if (ch) out += `${BOLD}${ch}${OFF}`;
3736
+ if (ch) out += `${BOLD2}${ch}${OFF}`;
2767
3737
  } else if (t < s.land) {
2768
3738
  const age = t - s.start;
2769
3739
  const period = age < LAZY_MS ? LAZY_PERIOD : FAST_PERIOD;
2770
- out += `${DIM3}${glyphAt(i, Math.floor(t / period))}${OFF}`;
3740
+ out += `${DIM4}${glyphAt(i, Math.floor(t / period))}${OFF}`;
2771
3741
  } else {
2772
3742
  const ch = target[i];
2773
- if (ch) out += `${BOLD}${ch}${OFF}`;
3743
+ if (ch) out += `${BOLD2}${ch}${OFF}`;
2774
3744
  }
2775
3745
  }
2776
3746
  return out;
@@ -2816,7 +3786,7 @@ function fromFile(filePath) {
2816
3786
  }
2817
3787
  }
2818
3788
  async function readDarwin() {
2819
- const tmp = path3.join(os10.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
3789
+ const tmp = path3.join(os7.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
2820
3790
  const script = [
2821
3791
  `set d to the clipboard as \xABclass PNGf\xBB`,
2822
3792
  `set f to open for access POSIX file "${tmp}" with write permission`,
@@ -2853,7 +3823,7 @@ async function readLinux() {
2853
3823
  return null;
2854
3824
  }
2855
3825
  async function readWindows() {
2856
- const tmp = path3.join(os10.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
3826
+ const tmp = path3.join(os7.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
2857
3827
  const ps = [
2858
3828
  "Add-Type -AssemblyName System.Windows.Forms;",
2859
3829
  "$img = [System.Windows.Forms.Clipboard]::GetImage();",
@@ -2881,6 +3851,11 @@ async function readClipboardImage() {
2881
3851
  function imagePlaceholder(seq) {
2882
3852
  return `[#Image ${seq}]`;
2883
3853
  }
3854
+ function ensureImagePlaceholders(text, images) {
3855
+ const missing = images.map((image) => imagePlaceholder(image.seq)).filter((placeholder) => !text.includes(placeholder));
3856
+ if (!missing.length) return text;
3857
+ return [text.trimEnd(), ...missing].filter(Boolean).join(" ");
3858
+ }
2884
3859
  var INPUT_BOX_MARGIN = 1;
2885
3860
  function inputBoxBorderColor() {
2886
3861
  return "\x1B[38;5;240m";
@@ -3071,11 +4046,16 @@ var C = {
3071
4046
  red: "\x1B[31m",
3072
4047
  blue: "\x1B[34m",
3073
4048
  magenta: "\x1B[35m",
3074
- gray: "\x1B[90m",
4049
+ gray: themeGray,
3075
4050
  teal: "\x1B[38;5;37m"
3076
4051
  };
4052
+ var SYNC_OUTPUT_BEGIN = "\x1B[?2026h";
4053
+ var SYNC_OUTPUT_END = "\x1B[?2026l";
4054
+ var CURSOR_HIDE = "\x1B[?25l";
4055
+ var CURSOR_SHOW = "\x1B[?25h";
3077
4056
  var FRAMES = ["\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
3078
- var SPINNER_MS = 70;
4057
+ var SPINNER_FRAME_MS = 70;
4058
+ var ANIMATION_TICK_MS = 1e3 / 60;
3079
4059
  var SUBAGENT_COLORS = [
3080
4060
  "\x1B[35m",
3081
4061
  // magenta
@@ -3148,6 +4128,7 @@ var Tui = class _Tui {
3148
4128
  // answer text (plain). Bounded to a tail; cleared when the message commits.
3149
4129
  streamThinking = "";
3150
4130
  streamResponse = "";
4131
+ streamDraft = emptyLiveDraft();
3151
4132
  streamMessageId = null;
3152
4133
  // the message currently previewing
3153
4134
  streamRedrawTimer = null;
@@ -3177,7 +4158,7 @@ var Tui = class _Tui {
3177
4158
  // rows re-wrap (a full-width ruler becomes 2+ physical rows when narrowed),
3178
4159
  // so the caret-relative move-up from the last paint is stale. We store the
3179
4160
  // visible width of EVERY region row (not just above the body) plus the caret
3180
- // row index so moveToRegionTop can recompute physical height under the new
4161
+ // row index so regionTopSequence can recompute physical height under the new
3181
4162
  // wrap. Resize events are debounced — drag-resizing fires dozens of events
3182
4163
  // and redrawing each one desyncs and leaves ghost chrome.
3183
4164
  lastDrawnCols = 0;
@@ -3204,6 +4185,9 @@ var Tui = class _Tui {
3204
4185
  // placeholder at the caret; on submit only images whose placeholder is still
3205
4186
  // present in the text are handed to onSubmit. Cleared with the input.
3206
4187
  pendingImages = [];
4188
+ /** Portable file refs mirrored from another client. Their bytes remain in
4189
+ * the thread filesystem, so the TUI shows names without decoding them. */
4190
+ externalAttachmentNames = [];
3207
4191
  imagePasteBusy = false;
3208
4192
  // one clipboard read at a time
3209
4193
  // Sent-message history for ↑/↓ recall (oldest → newest). `historyIdx` is the
@@ -3217,6 +4201,9 @@ var Tui = class _Tui {
3217
4201
  // event hooks (wired by index.ts)
3218
4202
  onSubmit = () => {
3219
4203
  };
4204
+ /** Shift+Return sends a steering input through the portable messaging endpoint. */
4205
+ onSteer = () => {
4206
+ };
3220
4207
  onInterrupt = () => {
3221
4208
  };
3222
4209
  /** Up on the top row: return true to consume it (e.g. pull a queued message)
@@ -3225,6 +4212,11 @@ var Tui = class _Tui {
3225
4212
  /** Enter while the `[⚙ n bg]` badge is selected — opens the bg process panel. */
3226
4213
  onBgBadge = () => {
3227
4214
  };
4215
+ /** Fired (deduped) when the composer changes so index.ts can persist the
4216
+ * shared packed-endpoint draft, including attachments. */
4217
+ onDraftChange = () => {
4218
+ };
4219
+ lastDraftSeen = "";
3228
4220
  onQuit = () => process.exit(0);
3229
4221
  levelListeners = [];
3230
4222
  /**
@@ -3307,7 +4299,7 @@ var Tui = class _Tui {
3307
4299
  dispatch(str, key) {
3308
4300
  const seq = key && key.sequence || str || "";
3309
4301
  if (seq === "\n" || seq === "\x1B[13;2u" || seq === "\x1B[27;2;13~") {
3310
- this.insertAtCursor("\n");
4302
+ if (!this.takeoverHandler && !this.pasting && !this.paletteOpen()) this.submitInput(true);
3311
4303
  return;
3312
4304
  }
3313
4305
  if (key && key.ctrl && key.name === "c") {
@@ -3376,10 +4368,7 @@ var Tui = class _Tui {
3376
4368
  return;
3377
4369
  }
3378
4370
  if (key.name === "return" || key.name === "enter") {
3379
- if (key.shift) {
3380
- this.insertAtCursor("\n");
3381
- return;
3382
- }
4371
+ if (key.shift) return;
3383
4372
  if (matches.length) this.runCommand(matches[cur]);
3384
4373
  return;
3385
4374
  }
@@ -3442,23 +4431,15 @@ var Tui = class _Tui {
3442
4431
  return;
3443
4432
  }
3444
4433
  if (key.name === "return" || key.name === "enter") {
3445
- if (key.shift) {
4434
+ if (key.meta) {
3446
4435
  this.insertAtCursor("\n");
3447
4436
  return;
3448
4437
  }
3449
- const text = this.inputBuffer;
3450
- const images = this.pendingImages.filter((img) => text.includes(imagePlaceholder(img.seq)));
3451
- this.inputBuffer = "";
3452
- this.cursorPos = 0;
3453
- this.pendingImages = [];
3454
- this.historyIdx = null;
3455
- this.historyDraft = "";
3456
- this.historyDraftImages = [];
3457
- this.renderBottom();
3458
- if (text.trim()) {
3459
- this.addHistoryEntry(text.trim());
3460
- this.onSubmit(text.trim(), images);
4438
+ if (key.shift) {
4439
+ this.submitInput(true);
4440
+ return;
3461
4441
  }
4442
+ this.submitInput(false);
3462
4443
  return;
3463
4444
  }
3464
4445
  if (key.ctrl && key.name === "v") {
@@ -3489,6 +4470,23 @@ var Tui = class _Tui {
3489
4470
  this.insertAtCursor(str);
3490
4471
  }
3491
4472
  }
4473
+ submitInput(steer) {
4474
+ const text = this.inputBuffer;
4475
+ const images = this.pendingImages.filter((img) => text.includes(imagePlaceholder(img.seq)));
4476
+ const hasExternalAttachments = this.externalAttachmentNames.length > 0;
4477
+ this.inputBuffer = "";
4478
+ this.cursorPos = 0;
4479
+ this.pendingImages = [];
4480
+ this.externalAttachmentNames = [];
4481
+ this.historyIdx = null;
4482
+ this.historyDraft = "";
4483
+ this.historyDraftImages = [];
4484
+ this.renderBottom();
4485
+ if (!text.trim() && !hasExternalAttachments && images.length === 0) return;
4486
+ if (text.trim()) this.addHistoryEntry(text.trim());
4487
+ if (steer) this.onSteer(text.trim(), images);
4488
+ else this.onSubmit(text.trim(), images);
4489
+ }
3492
4490
  insertAtCursor(text) {
3493
4491
  this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos) + text + this.inputBuffer.slice(this.cursorPos);
3494
4492
  this.cursorPos += text.length;
@@ -3691,7 +4689,7 @@ var Tui = class _Tui {
3691
4689
  }
3692
4690
  spinnerFrame() {
3693
4691
  const now = Date.now();
3694
- return `${C.bold}${brandCycleColor(now)}${FRAMES[Math.floor(now / SPINNER_MS) % FRAMES.length]}${C.reset}`;
4692
+ return `${C.bold}${brandCycleColor(now)}${FRAMES[Math.floor(now / SPINNER_FRAME_MS) % FRAMES.length]}${C.reset}`;
3695
4693
  }
3696
4694
  /** "↑X ↓Y" cumulative token totals (greyed — low-priority). */
3697
4695
  tokensText() {
@@ -3731,7 +4729,7 @@ var Tui = class _Tui {
3731
4729
  }
3732
4730
  /** Is the `/` command palette currently showing? (input starts with "/".) */
3733
4731
  paletteOpen() {
3734
- return this.started && !this.takeoverHandler && this.commands.length > 0 && this.inputBuffer.startsWith("/");
4732
+ return this.started && !this.takeoverHandler && this.commands.length > 0 && this.externalAttachmentNames.length === 0 && this.inputBuffer.startsWith("/");
3735
4733
  }
3736
4734
  /** Commands matching the text typed after "/", in declared order. */
3737
4735
  filteredCommands() {
@@ -3790,9 +4788,10 @@ var Tui = class _Tui {
3790
4788
  /** The prompt line prefix (with ANSI colour) that precedes the typed text. */
3791
4789
  promptPrefix() {
3792
4790
  const q = this.queuedCount > 0 ? `${C.yellow}[\u23F3 ${this.queuedCount} queued]${C.reset} ` : "";
4791
+ const attachments2 = this.externalAttachmentNames.length > 0 ? `${C.gray}[\u{1F4CE} ${this.externalAttachmentNames.length}]${C.reset} ` : "";
3793
4792
  const bgText = `[\u2699 ${this.bgCount} bg]`;
3794
4793
  const bg = this.bgCount > 0 ? this.bgBadgeSelected ? `${C.cyan}\x1B[7m${bgText}\x1B[27m${C.reset} ` : `${C.cyan}${bgText}${C.reset} ` : "";
3795
- return `${q}${bg}${this.levelColor()}\u276F${C.reset} `;
4794
+ return `${q}${attachments2}${bg}${this.levelColor()}\u276F${C.reset} `;
3796
4795
  }
3797
4796
  visibleWidth(s) {
3798
4797
  let w = 0;
@@ -3867,20 +4866,21 @@ var Tui = class _Tui {
3867
4866
  if (matches.length === 0) return [` ${C.gray}no matching command${C.reset}`];
3868
4867
  const cur = Math.min(this.slashIdx, matches.length - 1);
3869
4868
  const pointerW = 2;
4869
+ const rowCols = Math.max(1, cols2 - 1);
3870
4870
  return matches.map((cmd, i) => {
3871
4871
  const sel = i === cur;
3872
4872
  const hint = (typeof cmd.hint === "function" ? cmd.hint() : cmd.hint) ?? "";
3873
4873
  const hintW = hint.length;
3874
4874
  const name = `/${cmd.name}`;
3875
4875
  let visible = `${name} ${cmd.label}`;
3876
- const labelMax = Math.max(6, cols2 - pointerW - (hintW ? hintW + 2 : 0));
4876
+ const labelMax = Math.max(6, rowCols - pointerW - (hintW ? hintW + 2 : 0));
3877
4877
  if (visible.length > labelMax) visible = visible.slice(0, labelMax - 1) + "\u2026";
3878
4878
  const desc = visible.slice(name.length);
3879
4879
  const pointer = sel ? `${C.magenta}\u276F${C.reset} ` : " ";
3880
4880
  const nameStyled = sel ? `${C.bold}${C.cyan}${name}${C.reset}` : `${C.cyan}${name}${C.reset}`;
3881
4881
  let line = `${pointer}${nameStyled}${C.gray}${desc}${C.reset}`;
3882
4882
  if (hintW) {
3883
- const gap = Math.max(2, cols2 - pointerW - visible.length - hintW);
4883
+ const gap = Math.max(2, rowCols - pointerW - visible.length - hintW);
3884
4884
  line += `${" ".repeat(gap)}${C.gray}${hint}${C.reset}`;
3885
4885
  }
3886
4886
  return line;
@@ -3902,9 +4902,9 @@ var Tui = class _Tui {
3902
4902
  * the NEW wrap: rows above the caret row + (caret row's rewrap − 1) so we
3903
4903
  * prefer a slight over-move (clean wipe) over under-move (ghost chrome).
3904
4904
  */
3905
- moveToRegionTop() {
3906
- process.stdout.write("\r");
3907
- if (!this.bottomDrawn) return;
4905
+ regionTopSequence() {
4906
+ let sequence = "\r";
4907
+ if (!this.bottomDrawn) return sequence;
3908
4908
  const cols2 = process.stdout.columns || 80;
3909
4909
  let up;
3910
4910
  if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0 && this.drawnRegionWidths.length) {
@@ -3921,7 +4921,8 @@ var Tui = class _Tui {
3921
4921
  } else {
3922
4922
  up = this.lastCursorRow;
3923
4923
  }
3924
- if (up > 0) process.stdout.write(`\x1B[${up}A`);
4924
+ if (up > 0) sequence += `\x1B[${up}A`;
4925
+ return sequence;
3925
4926
  }
3926
4927
  /**
3927
4928
  * Render the bottom region: status + subagents above a side-margined expanding
@@ -3935,9 +4936,17 @@ var Tui = class _Tui {
3935
4936
  */
3936
4937
  renderBottom() {
3937
4938
  if (!this.started || this.takeoverHandler) return;
4939
+ if (this.inputBuffer !== this.lastDraftSeen) {
4940
+ this.lastDraftSeen = this.inputBuffer;
4941
+ const images = this.pendingImages.filter((img) => this.inputBuffer.includes(imagePlaceholder(img.seq)));
4942
+ try {
4943
+ this.onDraftChange(this.inputBuffer, images);
4944
+ } catch {
4945
+ }
4946
+ }
3938
4947
  if (this.resizePending) return;
3939
4948
  const cols2 = process.stdout.columns || 80;
3940
- this.moveToRegionTop();
4949
+ const moveToTop = this.regionTopSequence();
3941
4950
  const hudWidths = [];
3942
4951
  const hudRows = [];
3943
4952
  const rowCap = Math.max(1, cols2 - 1);
@@ -3958,7 +4967,7 @@ var Tui = class _Tui {
3958
4967
  if (quitLine) writeHudRow(workPad + quitLine);
3959
4968
  const statusLine = this.statusLineText(workCols);
3960
4969
  if (statusLine) writeHudRow(workPad + statusLine);
3961
- const frame = FRAMES[Math.floor(Date.now() / SPINNER_MS) % FRAMES.length];
4970
+ const frame = FRAMES[Math.floor(Date.now() / SPINNER_FRAME_MS) % FRAMES.length];
3962
4971
  for (const sub of this.subagents) {
3963
4972
  const color = sub.agentName === COMPACTION_AGENT ? COMPACTION_COLOR : SUBAGENT_COLORS[this.subagentColorByID.get(sub.id) ?? 0];
3964
4973
  const budget = workCols - 10;
@@ -4006,18 +5015,21 @@ var Tui = class _Tui {
4006
5015
  this.bottomDrawn = true;
4007
5016
  const totalRows = hudRows.length;
4008
5017
  const up = Math.max(0, totalRows - 1 - caretRegionRow);
4009
- let out = "\x1B[J" + hudRows.join("\r\n");
5018
+ let out = SYNC_OUTPUT_BEGIN + moveToTop + "\x1B[J" + hudRows.join("\r\n");
4010
5019
  if (totalRows > 0) {
4011
5020
  out += "\r";
4012
5021
  if (up > 0) out += `\x1B[${up}A`;
4013
5022
  if (caretScreenCol > 0) out += `\x1B[${caretScreenCol}C`;
4014
5023
  }
5024
+ out += SYNC_OUTPUT_END;
4015
5025
  process.stdout.write(out);
4016
5026
  }
4017
5027
  clearBottom() {
4018
5028
  if (!this.bottomDrawn) return;
4019
- this.moveToRegionTop();
4020
- process.stdout.write("\x1B[J");
5029
+ const moveToTop = this.regionTopSequence();
5030
+ process.stdout.write(
5031
+ SYNC_OUTPUT_BEGIN + CURSOR_HIDE + moveToTop + "\x1B[J" + CURSOR_SHOW + SYNC_OUTPUT_END
5032
+ );
4021
5033
  this.bottomDrawn = false;
4022
5034
  }
4023
5035
  // ── live streaming preview ────────────────────────────────────────────────
@@ -4026,15 +5038,15 @@ var Tui = class _Tui {
4026
5038
  // wipes it right before the finished message is committed to the transcript
4027
5039
  // (which renders full markdown), so there's no double-render.
4028
5040
  static STREAM_TAIL = 20;
4029
- // How long a preview may sit untouched before it's wiped. The model often
4030
- // reasons and then calls a tool without ever emitting an answer, so without
4031
- // this the reasoning tail would linger on screen until the *next* thought (or
4032
- // message) arrives. Re-armed on every delta → fires this long after the last.
5041
+ // How long a reasoning-only preview may sit untouched before it's wiped. A
5042
+ // visible answer must NEVER expire: it is the only copy until polling
5043
+ // promotes the durable message into terminal scrollback.
4033
5044
  static STREAM_IDLE_MS = 1e4;
4034
- /** Append a fragment of streamed answer text (rendered plain). */
5045
+ /** Append a fragment of streamed answer text (rendered as progressive Markdown). */
4035
5046
  streamResponseDelta(delta, messageId) {
4036
5047
  this.beginStreamMessage(messageId);
4037
- this.streamResponse += delta;
5048
+ this.streamDraft = appendLiveDraft(this.streamDraft, { text: delta, messageId });
5049
+ this.streamResponse = this.streamDraft.text;
4038
5050
  this.scheduleStreamRedraw();
4039
5051
  this.armStreamIdleExpiry();
4040
5052
  }
@@ -4054,8 +5066,7 @@ var Tui = class _Tui {
4054
5066
  beginStreamMessage(messageId) {
4055
5067
  if (messageId !== void 0 && messageId !== this.streamMessageId) {
4056
5068
  this.streamMessageId = messageId;
4057
- this.streamThinking = "";
4058
- this.streamResponse = "";
5069
+ if (!this.streamResponse) this.streamThinking = "";
4059
5070
  }
4060
5071
  }
4061
5072
  /** Wipe the live preview — call right before committing the final message. */
@@ -4069,6 +5080,7 @@ var Tui = class _Tui {
4069
5080
  this.streamIdleTimer = null;
4070
5081
  }
4071
5082
  this.streamMessageId = null;
5083
+ this.streamDraft = emptyLiveDraft();
4072
5084
  if (!this.streamThinking && !this.streamResponse) return;
4073
5085
  this.streamThinking = "";
4074
5086
  this.streamResponse = "";
@@ -4081,17 +5093,16 @@ var Tui = class _Tui {
4081
5093
  this.renderBottom();
4082
5094
  }, 40);
4083
5095
  }
4084
- /** Re-armed on every streamed delta: once the model goes quiet for a beat, the
4085
- * preview is stale, so wipe it instead of letting it sit until the next
4086
- * message. The committed message (if any) still renders in full via
4087
- * clearStream(), so nothing is lost. */
5096
+ /** Re-armed for reasoning-only deltas. Once answer prose exists it remains
5097
+ * visible until clearStream() runs immediately before durable promotion. */
4088
5098
  armStreamIdleExpiry() {
4089
5099
  if (this.streamIdleTimer) clearTimeout(this.streamIdleTimer);
5100
+ this.streamIdleTimer = null;
5101
+ if (this.streamResponse) return;
4090
5102
  this.streamIdleTimer = setTimeout(() => {
4091
5103
  this.streamIdleTimer = null;
4092
- if (!this.streamThinking && !this.streamResponse) return;
5104
+ if (this.streamResponse || !this.streamThinking) return;
4093
5105
  this.streamThinking = "";
4094
- this.streamResponse = "";
4095
5106
  this.renderBottom();
4096
5107
  }, _Tui.STREAM_IDLE_MS);
4097
5108
  }
@@ -4107,23 +5118,28 @@ var Tui = class _Tui {
4107
5118
  */
4108
5119
  streamPreviewLines(cols2) {
4109
5120
  const thinkStyle = "\x1B[3m\x1B[38;5;240m";
4110
- const clamp2 = (s, wrap, lead) => {
4111
- const max = cols2 - 2;
4112
- const t = s.length > max ? s.slice(0, Math.max(0, max - 1)) + "\u2026" : s;
4113
- return wrap ? `${lead}${wrap}${t}${C.reset}` : `${lead}${t}`;
4114
- };
4115
5121
  const leakedMarkup = /<\/?[||]DSML[||]|^\s*\[\/?SESSION\]\s*<?\s*$/;
4116
- const realLines = (text) => text.replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "" && !leakedMarkup.test(l));
5122
+ const renderedLines = (text) => {
5123
+ const clean2 = text.replace(/\r/g, "").split("\n").filter((line) => !leakedMarkup.test(line)).join("\n");
5124
+ const rows = [];
5125
+ for (const line of renderStreamingMarkdown(clean2, Math.max(8, cols2 - 2))) {
5126
+ const blank = line.replace(/\x1b\[[0-9;]*m/g, "").trim() === "";
5127
+ if (blank && (!rows.length || rows[rows.length - 1] === "")) continue;
5128
+ rows.push(blank ? "" : line);
5129
+ }
5130
+ while (rows.length && rows[rows.length - 1] === "") rows.pop();
5131
+ return rows;
5132
+ };
4117
5133
  if (this.streamResponse) {
4118
- const all = realLines(this.streamResponse);
5134
+ const all = renderedLines(this.streamResponse);
4119
5135
  const shown = all.slice(-20);
4120
5136
  const firstVisible = all.length <= _Tui.STREAM_TAIL;
4121
5137
  return shown.map(
4122
- (l, i) => clamp2(l, "", i === 0 && firstVisible ? `${C.gray}\u2022${C.reset} ` : " ")
5138
+ (l, i) => `${i === 0 && firstVisible ? `${C.gray}\u2022${C.reset} ` : " "}${l}`
4123
5139
  );
4124
5140
  }
4125
5141
  if (this.streamThinking) {
4126
- return realLines(this.streamThinking).slice(-20).map((l) => clamp2(l, thinkStyle, " "));
5142
+ return renderedLines(this.streamThinking).slice(-20).map((l) => ` ${thinkStyle}${l.replace(/\x1b\[0m/g, `${C.reset}${thinkStyle}`)}${C.reset}`);
4127
5143
  }
4128
5144
  return [];
4129
5145
  }
@@ -4251,6 +5267,7 @@ var Tui = class _Tui {
4251
5267
  }
4252
5268
  // ─── working indicator (turn state) ───────────────────────────────────────
4253
5269
  setWorking(on) {
5270
+ if (on === this.working) return;
4254
5271
  if (on && !this.working) {
4255
5272
  this.working = true;
4256
5273
  this.workingStart = Date.now();
@@ -4266,6 +5283,11 @@ var Tui = class _Tui {
4266
5283
  * stable, distinct colour for as long as it's active; the compaction agent
4267
5284
  * is always orange (its colour never comes from the shared pool). */
4268
5285
  setSubagents(subagents) {
5286
+ const unchanged = subagents.length === this.subagents.length && subagents.every((sub, i) => {
5287
+ const current = this.subagents[i];
5288
+ return current?.id === sub.id && current.label === sub.label && current.agentName === sub.agentName;
5289
+ });
5290
+ if (unchanged) return;
4269
5291
  this.subagents = subagents;
4270
5292
  const active = new Set(subagents.map((s) => s.id));
4271
5293
  for (const id of [...this.subagentColorByID.keys()]) {
@@ -4286,7 +5308,7 @@ var Tui = class _Tui {
4286
5308
  syncSpinner() {
4287
5309
  const spinning = this.working || this.subagents.length > 0;
4288
5310
  if (spinning && !this.spinnerTimer) {
4289
- this.spinnerTimer = setInterval(() => this.renderBottom(), SPINNER_MS);
5311
+ this.spinnerTimer = setInterval(() => this.renderBottom(), ANIMATION_TICK_MS);
4290
5312
  } else if (!spinning && this.spinnerTimer) {
4291
5313
  clearInterval(this.spinnerTimer);
4292
5314
  this.spinnerTimer = null;
@@ -4296,15 +5318,18 @@ var Tui = class _Tui {
4296
5318
  return this.working;
4297
5319
  }
4298
5320
  setBackgroundCount(n) {
5321
+ if (n === this.bgCount) return;
4299
5322
  this.bgCount = n;
4300
5323
  if (n === 0) this.bgBadgeSelected = false;
4301
5324
  this.renderBottom();
4302
5325
  }
4303
5326
  setQueuedCount(n) {
5327
+ if (n === this.queuedCount) return;
4304
5328
  this.queuedCount = n;
4305
5329
  this.renderBottom();
4306
5330
  }
4307
5331
  setConnected(connected) {
5332
+ if (connected === this.connected) return;
4308
5333
  this.connected = connected;
4309
5334
  this.renderBottom();
4310
5335
  }
@@ -4313,15 +5338,25 @@ var Tui = class _Tui {
4313
5338
  return this.inputBuffer;
4314
5339
  }
4315
5340
  /** Replace the input (and any pasted images tied to placeholders in it). */
4316
- setInput(text, images = []) {
4317
- this.inputBuffer = text;
4318
- this.cursorPos = text.length;
5341
+ setInput(text, images = [], notifyDraft = true, restoreExternalImages = false) {
5342
+ this.inputBuffer = restoreExternalImages ? ensureImagePlaceholders(text, images) : text;
5343
+ this.cursorPos = this.inputBuffer.length;
4319
5344
  this.pendingImages = images;
4320
5345
  this.historyIdx = null;
5346
+ if (!notifyDraft) this.lastDraftSeen = this.inputBuffer;
4321
5347
  this.renderBottom();
4322
5348
  }
5349
+ setExternalAttachmentNames(names) {
5350
+ if (names.length === this.externalAttachmentNames.length && names.every((name, i) => name === this.externalAttachmentNames[i])) return;
5351
+ this.externalAttachmentNames = [...names];
5352
+ this.renderBottom();
5353
+ }
5354
+ hasExternalAttachments() {
5355
+ return this.externalAttachmentNames.length > 0;
5356
+ }
4323
5357
  /** Cumulative token totals shown on the prompt line (`outTokens` includes live). */
4324
5358
  setTokens(inTokens, outTokens) {
5359
+ if (inTokens === this.tokensIn && outTokens === this.tokensOut) return;
4325
5360
  this.tokensIn = inTokens;
4326
5361
  this.tokensOut = outTokens;
4327
5362
  this.renderBottom();
@@ -4333,6 +5368,7 @@ var Tui = class _Tui {
4333
5368
  */
4334
5369
  setStep(label, outTokens) {
4335
5370
  const next = label && label.trim() ? label.replace(/\s+/g, " ").trim() : null;
5371
+ if (next === this.step && outTokens === this.stepOut) return;
4336
5372
  if (next !== this.step) {
4337
5373
  this.step = next;
4338
5374
  this.stepStart = Date.now();
@@ -4708,219 +5744,57 @@ ${C.cyan}\u2503${C.reset} ${question}
4708
5744
  process.stdout.write("\n");
4709
5745
  this.endTakeover();
4710
5746
  resolve(value);
4711
- };
4712
- this.takeoverHandler = (str, key) => {
4713
- if (key?.name === "escape") return finish(null);
4714
- if (key?.name === "return" || key?.name === "enter") return finish(buf.trim() || null);
4715
- if (key?.name === "backspace") {
4716
- buf = buf.slice(0, -1);
4717
- draw();
4718
- return;
4719
- }
4720
- if (str && !key?.ctrl && !key?.meta && str >= " ") {
4721
- buf += str;
4722
- draw();
4723
- }
4724
- };
4725
- });
4726
- }
4727
- banner(lines) {
4728
- this.clearBottom();
4729
- process.stdout.write("\n");
4730
- for (const l of lines) process.stdout.write(l + "\n");
4731
- }
4732
- };
4733
-
4734
- // src/history.ts
4735
- var HISTORY_KEY = "input_history";
4736
- var MAX_ENTRIES = 100;
4737
- function clean(value) {
4738
- if (!Array.isArray(value)) return [];
4739
- return value.filter((e) => typeof e === "string" && e.trim() !== "").slice(-MAX_ENTRIES);
4740
- }
4741
- async function loadHistory(store, threadId, seedThreadId) {
4742
- const own = clean(await store.kvGet(threadId, HISTORY_KEY));
4743
- if (own.length) return own;
4744
- if (seedThreadId && seedThreadId !== threadId) {
4745
- const seeded = clean(await store.kvGet(seedThreadId, HISTORY_KEY));
4746
- if (seeded.length) {
4747
- void store.kvSet(threadId, HISTORY_KEY, seeded);
4748
- return seeded;
4749
- }
4750
- }
4751
- return [];
4752
- }
4753
- function appendHistory(store, threadId, history, text) {
4754
- const t = text.trim();
4755
- if (!t || history[history.length - 1] === t) return history;
4756
- history.push(t);
4757
- if (history.length > MAX_ENTRIES) history.splice(0, history.length - MAX_ENTRIES);
4758
- void store.kvSet(threadId, HISTORY_KEY, [...history]);
4759
- return history;
4760
- }
4761
-
4762
- // src/markdown.ts
4763
- var ESC = "\x1B[";
4764
- var R = ESC + "0m";
4765
- var BOLD2 = ESC + "1m";
4766
- var DIM4 = ESC + "2m";
4767
- var ITAL = ESC + "3m";
4768
- var UNDER = ESC + "4m";
4769
- var TEAL = ESC + "38;5;37m";
4770
- var CYAN = ESC + "36m";
4771
- var GRAY = ESC + "90m";
4772
- var ANSI = /\x1b\[[0-9;]*m/g;
4773
- function visibleWidth(s) {
4774
- return s.replace(ANSI, "").length;
4775
- }
4776
- function padEndVisible(s, width) {
4777
- const pad = width - visibleWidth(s);
4778
- return pad > 0 ? s + " ".repeat(pad) : s;
4779
- }
4780
- function inline(s) {
4781
- const codes = [];
4782
- s = s.replace(/`([^`]+)`/g, (_, code) => {
4783
- codes.push(code);
4784
- return "\0" + (codes.length - 1) + "\0";
4785
- });
4786
- s = s.replace(
4787
- /\[([^\]]+)\]\(([^)\s]+)\)/g,
4788
- (_, text, url) => `${CYAN}${UNDER}${text}${R} ${DIM4}${url}${R}`
4789
- );
4790
- s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => `${BOLD2}${t}${R}`);
4791
- s = s.replace(/\*([^*\n]+)\*/g, (_, t) => `${ITAL}${t}${R}`);
4792
- s = s.replace(/~~([^~]+)~~/g, (_, t) => `${DIM4}${t}${R}`);
4793
- s = s.replace(/\x00(\d+)\x00/g, (_, i) => `${TEAL}${codes[+i].replace(/ /g, String.fromCharCode(160))}${R}`);
4794
- return s;
4795
- }
4796
- function wrapStyled(text, width) {
4797
- if (width < 4 || visibleWidth(text) <= width) return [text];
4798
- const words = text.split(" ");
4799
- const lines = [];
4800
- let cur = "";
4801
- let curLen = 0;
4802
- for (const w of words) {
4803
- const wLen = visibleWidth(w);
4804
- if (cur === "") {
4805
- cur = w;
4806
- curLen = wLen;
4807
- } else if (curLen + 1 + wLen <= width) {
4808
- cur += " " + w;
4809
- curLen += 1 + wLen;
4810
- } else {
4811
- lines.push(cur);
4812
- cur = w;
4813
- curLen = wLen;
4814
- }
5747
+ };
5748
+ this.takeoverHandler = (str, key) => {
5749
+ if (key?.name === "escape") return finish(null);
5750
+ if (key?.name === "return" || key?.name === "enter") return finish(buf.trim() || null);
5751
+ if (key?.name === "backspace") {
5752
+ buf = buf.slice(0, -1);
5753
+ draw();
5754
+ return;
5755
+ }
5756
+ if (str && !key?.ctrl && !key?.meta && str >= " ") {
5757
+ buf += str;
5758
+ draw();
5759
+ }
5760
+ };
5761
+ });
4815
5762
  }
4816
- if (cur !== "" || lines.length === 0) lines.push(cur);
4817
- return lines;
4818
- }
4819
- function wrapBlock(out, cols2, leadFirst, leadRest, leadWidth, text) {
4820
- const wrapped = wrapStyled(text, Math.max(8, cols2 - leadWidth));
4821
- wrapped.forEach((ln, idx) => out.push((idx === 0 ? leadFirst : leadRest) + ln));
4822
- }
4823
- function tableCells(row) {
4824
- let r = row.trim();
4825
- if (r.startsWith("|")) r = r.slice(1);
4826
- if (r.endsWith("|")) r = r.slice(0, -1);
4827
- return r.split("|").map((c4) => c4.trim());
4828
- }
4829
- var SEPARATOR = /^[\s|:-]+$/;
4830
- function isTableSeparator(line) {
4831
- return SEPARATOR.test(line) && line.includes("-") && line.includes("|");
4832
- }
4833
- function renderTable(rows) {
4834
- const cols2 = Math.max(...rows.map((r) => r.length));
4835
- const widths = [];
4836
- for (let c4 = 0; c4 < cols2; c4++) {
4837
- widths[c4] = Math.max(...rows.map((r) => visibleWidth(inline(r[c4] ?? ""))));
5763
+ banner(lines) {
5764
+ this.clearBottom();
5765
+ process.stdout.write("\n");
5766
+ for (const l of lines) process.stdout.write(l + "\n");
4838
5767
  }
4839
- const sep = `${GRAY} \u2502 ${R}`;
4840
- const out = [];
4841
- rows.forEach((r, ri) => {
4842
- const cells = [];
4843
- for (let c4 = 0; c4 < cols2; c4++) {
4844
- const raw = r[c4] ?? "";
4845
- const styled = ri === 0 ? `${BOLD2}${inline(raw)}${R}` : inline(raw);
4846
- cells.push(padEndVisible(styled, widths[c4]));
4847
- }
4848
- out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
4849
- if (ri === 0) {
4850
- const rule = widths.map((w) => `${GRAY}${"\u2500".repeat(w)}${R}`).join(`${GRAY}\u2500\u253C\u2500${R}`);
4851
- out.push(" " + rule);
4852
- }
4853
- });
4854
- return out;
5768
+ };
5769
+
5770
+ // src/history.ts
5771
+ var HISTORY_KEY = "input_history";
5772
+ var MAX_ENTRIES = 100;
5773
+ function clean(value) {
5774
+ if (!Array.isArray(value)) return [];
5775
+ return value.filter((e) => typeof e === "string" && e.trim() !== "").slice(-MAX_ENTRIES);
4855
5776
  }
4856
- function renderMarkdown(src, cols2 = 80) {
4857
- const lines = src.replace(/\r\n/g, "\n").split("\n");
4858
- const out = [];
4859
- let inFence = false;
4860
- let i = 0;
4861
- while (i < lines.length) {
4862
- const line = lines[i];
4863
- if (/^\s*```/.test(line)) {
4864
- inFence = !inFence;
4865
- i++;
4866
- continue;
4867
- }
4868
- if (inFence) {
4869
- out.push(`${GRAY}\u2502${R} ${line}`);
4870
- i++;
4871
- continue;
4872
- }
4873
- if (line.includes("|") && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
4874
- const block = [tableCells(line)];
4875
- i += 2;
4876
- while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
4877
- block.push(tableCells(lines[i]));
4878
- i++;
4879
- }
4880
- out.push(...renderTable(block));
4881
- continue;
4882
- }
4883
- const heading = line.match(/^(#{1,6})\s+(.*)$/);
4884
- if (heading) {
4885
- for (const ln of wrapStyled(heading[2].trim(), cols2)) out.push(`${BOLD2}${TEAL}${ln}${R}`);
4886
- i++;
4887
- continue;
4888
- }
4889
- if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
4890
- out.push(`${GRAY}\u2500\u2500\u2500\u2500\u2500\u2500${R}`);
4891
- i++;
4892
- continue;
4893
- }
4894
- const quote = line.match(/^\s*>\s?(.*)$/);
4895
- if (quote) {
4896
- for (const ln of wrapStyled(inline(quote[1]), Math.max(8, cols2 - 2))) {
4897
- out.push(`${GRAY}\u2502${R} ${DIM4}${ln}${R}`);
4898
- }
4899
- i++;
4900
- continue;
4901
- }
4902
- const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
4903
- if (bullet) {
4904
- const leadWidth = bullet[1].length + 2;
4905
- wrapBlock(out, cols2, `${bullet[1]}${TEAL}\u2022${R} `, " ".repeat(leadWidth), leadWidth, inline(bullet[2]));
4906
- i++;
4907
- continue;
4908
- }
4909
- const numbered = line.match(/^(\s*)(\d+)([.)])\s+(.*)$/);
4910
- if (numbered) {
4911
- const marker = `${numbered[2]}${numbered[3]}`;
4912
- const leadWidth = numbered[1].length + marker.length + 1;
4913
- wrapBlock(out, cols2, `${numbered[1]}${BOLD2}${marker}${R} `, " ".repeat(leadWidth), leadWidth, inline(numbered[4]));
4914
- i++;
4915
- continue;
5777
+ async function loadHistory(store, threadId, seedThreadId) {
5778
+ const own = clean(await store.kvGet(threadId, HISTORY_KEY));
5779
+ if (own.length) return own;
5780
+ if (seedThreadId && seedThreadId !== threadId) {
5781
+ const seeded = clean(await store.kvGet(seedThreadId, HISTORY_KEY));
5782
+ if (seeded.length) {
5783
+ void store.kvSet(threadId, HISTORY_KEY, seeded);
5784
+ return seeded;
4916
5785
  }
4917
- if (line.trim()) wrapBlock(out, cols2, "", "", 0, inline(line));
4918
- else out.push("");
4919
- i++;
4920
5786
  }
4921
- return out;
5787
+ return [];
5788
+ }
5789
+ function appendHistory(store, threadId, history, text) {
5790
+ const t = text.trim();
5791
+ if (!t || history[history.length - 1] === t) return history;
5792
+ history.push(t);
5793
+ if (history.length > MAX_ENTRIES) history.splice(0, history.length - MAX_ENTRIES);
5794
+ void store.kvSet(threadId, HISTORY_KEY, [...history]);
5795
+ return history;
4922
5796
  }
4923
- var DIR = path3.join(os10.homedir(), ".standardagents");
5797
+ var DIR = path3.join(os7.homedir(), ".standardagents");
4924
5798
  var FILE = path3.join(DIR, "credentials");
4925
5799
  function normalizeEndpoint(endpoint) {
4926
5800
  let e = endpoint.trim();
@@ -5054,7 +5928,7 @@ function relaxTlsForLocalEndpoint(endpoint) {
5054
5928
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
5055
5929
  return true;
5056
5930
  }
5057
- var dir = () => path3.join(os10.homedir(), ".standardagents");
5931
+ var dir = () => path3.join(os7.homedir(), ".standardagents");
5058
5932
  var file = () => path3.join(dir(), "machine.json");
5059
5933
  function loadMachineIdentity() {
5060
5934
  try {
@@ -5088,6 +5962,7 @@ function machineIdFromDaemonClientId(clientId) {
5088
5962
  var KEY_PREFIX = "standardcode.machine.";
5089
5963
  var CMD_SUFFIX = ".cmd";
5090
5964
  var NAME_SUFFIX = ".name";
5965
+ var ICON_SUFFIX = ".icon";
5091
5966
  var FSREQ_SUFFIX = ".fsreq";
5092
5967
  var FSRES_SUFFIX = ".fsres";
5093
5968
  var DAEMON_ONLINE_WINDOW_MS = 90 * 1e3;
@@ -5100,6 +5975,9 @@ function commandKey(machineId) {
5100
5975
  function nameKey(machineId) {
5101
5976
  return `${KEY_PREFIX}${machineId}${NAME_SUFFIX}`;
5102
5977
  }
5978
+ function iconKey(machineId) {
5979
+ return `${KEY_PREFIX}${machineId}${ICON_SUFFIX}`;
5980
+ }
5103
5981
  function fsRequestKey(machineId) {
5104
5982
  return `${KEY_PREFIX}${machineId}${FSREQ_SUFFIX}`;
5105
5983
  }
@@ -5109,23 +5987,66 @@ function fsResponseKey(machineId) {
5109
5987
  function parseFsRequest(value) {
5110
5988
  if (!value || typeof value !== "object") return null;
5111
5989
  const r = value;
5112
- if (typeof r.nonce !== "string" || r.op !== "list" || typeof r.path !== "string") return null;
5990
+ const op = r.op === "mkdir" ? "mkdir" : r.op === "list" ? "list" : null;
5991
+ if (typeof r.nonce !== "string" || !op || typeof r.path !== "string") return null;
5113
5992
  return {
5114
5993
  nonce: r.nonce,
5115
- op: "list",
5994
+ op,
5116
5995
  path: r.path,
5996
+ name: typeof r.name === "string" ? r.name : void 0,
5117
5997
  show_hidden: r.show_hidden === true,
5118
5998
  requested_at: typeof r.requested_at === "number" ? r.requested_at : Date.now()
5119
5999
  };
5120
6000
  }
6001
+ async function writeFsRequest(api, machineId, req) {
6002
+ await api.userKvSet(fsRequestKey(machineId), { ...req, requested_at: Date.now() });
6003
+ }
6004
+ async function readFsResponse(api, machineId) {
6005
+ const value = await api.userKvGet(fsResponseKey(machineId));
6006
+ if (!value || typeof value !== "object") return null;
6007
+ const r = value;
6008
+ if (typeof r.nonce !== "string" || typeof r.ok !== "boolean") return null;
6009
+ return {
6010
+ nonce: r.nonce,
6011
+ ok: r.ok,
6012
+ result: r.result,
6013
+ error: typeof r.error === "string" ? r.error : void 0,
6014
+ responded_at: typeof r.responded_at === "number" ? r.responded_at : 0
6015
+ };
6016
+ }
5121
6017
  async function readFsRequest(api, machineId) {
5122
6018
  return parseFsRequest(await api.userKvGet(fsRequestKey(machineId)));
5123
6019
  }
5124
6020
  async function writeFsResponse(api, machineId, res) {
5125
6021
  await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
5126
6022
  }
5127
- function machineDisplayName(record) {
5128
- return record.name?.trim() || "" || (record.hostname || "") || (record.id || "") || "machine";
6023
+ async function resolveRemoteProjectPath(api, machineId, typedPath, timeoutMs = 12e3) {
6024
+ const trimmed = typedPath.trim();
6025
+ if (!trimmed.startsWith("~")) return trimmed;
6026
+ const nonce = crypto.randomBytes(8).toString("hex");
6027
+ try {
6028
+ await writeFsRequest(api, machineId, { nonce, op: "list", path: trimmed });
6029
+ const deadline = Date.now() + timeoutMs;
6030
+ while (Date.now() < deadline) {
6031
+ await new Promise((r) => setTimeout(r, 700));
6032
+ const res = await readFsResponse(api, machineId).catch(() => null);
6033
+ if (res && res.nonce === nonce) {
6034
+ const resolved = res.result && typeof res.result.path === "string" ? res.result.path : "";
6035
+ return resolved || trimmed;
6036
+ }
6037
+ }
6038
+ } catch {
6039
+ }
6040
+ return trimmed;
6041
+ }
6042
+ function machineIcon(record2) {
6043
+ if (record2.icon && record2.icon.trim()) return record2.icon.trim();
6044
+ if (record2.platform === "darwin") return "\u{1F4BB}";
6045
+ if (record2.platform === "win32") return "\u{1F5A5}\uFE0F";
6046
+ return "\u{1F5B3}";
6047
+ }
6048
+ function machineDisplayName(record2) {
6049
+ return record2.name?.trim() || "" || (record2.hostname || "") || (record2.id || "") || "machine";
5129
6050
  }
5130
6051
  async function getMachineName(api, machineId) {
5131
6052
  const v = await api.userKvGet(nameKey(machineId));
@@ -5159,39 +6080,56 @@ async function loadMachines(api) {
5159
6080
  const entries = await api.userKvList(KEY_PREFIX);
5160
6081
  const records = [];
5161
6082
  const names = /* @__PURE__ */ new Map();
6083
+ const icons = /* @__PURE__ */ new Map();
5162
6084
  for (const e of entries) {
5163
6085
  const rest = e.key.slice(KEY_PREFIX.length);
5164
- if (rest.endsWith(CMD_SUFFIX)) continue;
6086
+ if (rest.endsWith(CMD_SUFFIX) || rest.endsWith(FSREQ_SUFFIX) || rest.endsWith(FSRES_SUFFIX)) continue;
5165
6087
  if (rest.endsWith(NAME_SUFFIX)) {
5166
6088
  const id = rest.slice(0, -NAME_SUFFIX.length);
5167
6089
  if (typeof e.value === "string" && e.value.trim()) names.set(id, e.value.trim());
5168
6090
  continue;
5169
6091
  }
6092
+ if (rest.endsWith(ICON_SUFFIX)) {
6093
+ const id = rest.slice(0, -ICON_SUFFIX.length);
6094
+ if (typeof e.value === "string" && e.value.trim()) icons.set(id, e.value.trim());
6095
+ continue;
6096
+ }
5170
6097
  const rec = parseMachineRecord(e.value);
5171
6098
  if (rec) records.push(rec);
5172
6099
  }
5173
6100
  for (const rec of records) {
5174
6101
  const override = names.get(rec.id);
5175
6102
  if (override) rec.name = override;
6103
+ const icon = icons.get(rec.id);
6104
+ if (icon) rec.icon = icon;
5176
6105
  }
5177
6106
  return records;
5178
6107
  }
6108
+ async function getMachineIcon(api, machineId) {
6109
+ const v = await api.userKvGet(iconKey(machineId));
6110
+ return typeof v === "string" && v.trim() ? v.trim() : null;
6111
+ }
6112
+ async function setMachineIcon(api, machineId, icon) {
6113
+ const trimmed = icon.trim();
6114
+ await api.userKvSet(iconKey(machineId), trimmed || null);
6115
+ }
5179
6116
  async function loadMachine(api, machineId) {
5180
6117
  const rec = await loadRawMachine(api, machineId);
5181
6118
  if (!rec) return null;
5182
- const override = await getMachineName(api, machineId);
6119
+ const [override, icon] = await Promise.all([getMachineName(api, machineId), getMachineIcon(api, machineId)]);
5183
6120
  if (override) rec.name = override;
6121
+ if (icon) rec.icon = icon;
5184
6122
  return rec;
5185
6123
  }
5186
- function daemonOnline(record, now = Date.now()) {
5187
- return !!record.daemon && now - record.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
6124
+ function daemonOnline(record2, now = Date.now()) {
6125
+ return !!record2.daemon && now - record2.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
5188
6126
  }
5189
6127
  function newRecord(identity) {
5190
6128
  const now = Date.now();
5191
6129
  return {
5192
6130
  id: identity.machine_id,
5193
- name: os10.hostname(),
5194
- hostname: os10.hostname(),
6131
+ name: os7.hostname(),
6132
+ hostname: os7.hostname(),
5195
6133
  platform: process.platform,
5196
6134
  arch: process.arch,
5197
6135
  version: readVersion() || void 0,
@@ -5203,15 +6141,15 @@ function newRecord(identity) {
5203
6141
  }
5204
6142
  async function updateOwnMachineRecord(api, identity, mutate) {
5205
6143
  const existing = await loadRawMachine(api, identity.machine_id);
5206
- const record = existing ?? newRecord(identity);
5207
- record.hostname = os10.hostname();
5208
- record.platform = process.platform;
5209
- record.arch = process.arch;
5210
- record.version = readVersion() || record.version;
5211
- mutate?.(record);
5212
- record.updated_at = Date.now();
5213
- await api.userKvSet(machineKey(identity.machine_id), record);
5214
- return record;
6144
+ const record2 = existing ?? newRecord(identity);
6145
+ record2.hostname = os7.hostname();
6146
+ record2.platform = process.platform;
6147
+ record2.arch = process.arch;
6148
+ record2.version = readVersion() || record2.version;
6149
+ mutate?.(record2);
6150
+ record2.updated_at = Date.now();
6151
+ await api.userKvSet(machineKey(identity.machine_id), record2);
6152
+ return record2;
5215
6153
  }
5216
6154
  function projectRepository(projectDir) {
5217
6155
  try {
@@ -5225,35 +6163,54 @@ function projectRepository(projectDir) {
5225
6163
  return null;
5226
6164
  }
5227
6165
  }
6166
+ function normalizeProjectDir(projectDir) {
6167
+ let p = projectDir.trim();
6168
+ if (!p) return p;
6169
+ if (p === "~") p = os7.homedir();
6170
+ else if (p.startsWith("~/")) p = path3.join(os7.homedir(), p.slice(2));
6171
+ return path3.resolve(p);
6172
+ }
5228
6173
  async function registerProject(api, identity, projectDir) {
5229
- const repository = projectRepository(projectDir);
5230
- await updateOwnMachineRecord(api, identity, (record) => {
5231
- record.projects[projectDir] = {
5232
- name: projectDir.split("/").filter(Boolean).pop() || projectDir,
6174
+ const dir2 = normalizeProjectDir(projectDir);
6175
+ if (!dir2) return;
6176
+ const repository = projectRepository(dir2);
6177
+ await updateOwnMachineRecord(api, identity, (record2) => {
6178
+ for (const existing of Object.keys(record2.projects)) {
6179
+ if (existing !== dir2 && normalizeProjectDir(existing) === dir2) {
6180
+ delete record2.projects[existing];
6181
+ }
6182
+ }
6183
+ record2.projects[dir2] = {
6184
+ name: dir2.split("/").filter(Boolean).pop() || dir2,
5233
6185
  last_used_at: Date.now(),
5234
6186
  repository
5235
6187
  };
5236
6188
  });
5237
6189
  }
5238
6190
  async function unregisterProject(api, identity, projectDir) {
5239
- await updateOwnMachineRecord(api, identity, (record) => {
5240
- delete record.projects[projectDir];
6191
+ const dir2 = normalizeProjectDir(projectDir);
6192
+ await updateOwnMachineRecord(api, identity, (record2) => {
6193
+ for (const existing of Object.keys(record2.projects)) {
6194
+ if (existing === projectDir || existing === dir2 || normalizeProjectDir(existing) === dir2) {
6195
+ delete record2.projects[existing];
6196
+ }
6197
+ }
5241
6198
  });
5242
6199
  }
5243
6200
  async function touchDaemon(api, identity, version) {
5244
- await updateOwnMachineRecord(api, identity, (record) => {
6201
+ await updateOwnMachineRecord(api, identity, (record2) => {
5245
6202
  const now = Date.now();
5246
- record.daemon = {
6203
+ record2.daemon = {
5247
6204
  version,
5248
- installed_at: record.daemon?.installed_at ?? now,
6205
+ installed_at: record2.daemon?.installed_at ?? now,
5249
6206
  last_seen_at: now,
5250
6207
  pid: process.pid
5251
6208
  };
5252
6209
  });
5253
6210
  }
5254
6211
  async function clearDaemon(api, identity) {
5255
- await updateOwnMachineRecord(api, identity, (record) => {
5256
- record.daemon = null;
6212
+ await updateOwnMachineRecord(api, identity, (record2) => {
6213
+ record2.daemon = null;
5257
6214
  });
5258
6215
  }
5259
6216
  function parseCommands(value) {
@@ -5289,16 +6246,16 @@ async function clearMachineCommands(api, machineId, appliedIds) {
5289
6246
  async function applyMachineCommand(api, identity, cmd) {
5290
6247
  switch (cmd.kind) {
5291
6248
  case "add_project": {
5292
- const path13 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
5293
- if (!path13) return "add_project: ignored (no path)";
5294
- await registerProject(api, identity, path13);
5295
- return `added project ${path13}`;
6249
+ const path14 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
6250
+ if (!path14) return "add_project: ignored (no path)";
6251
+ await registerProject(api, identity, path14);
6252
+ return `added project ${path14}`;
5296
6253
  }
5297
6254
  case "remove_project": {
5298
- const path13 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
5299
- if (!path13) return "remove_project: ignored (no path)";
5300
- await unregisterProject(api, identity, path13);
5301
- return `removed project ${path13}`;
6255
+ const path14 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
6256
+ if (!path14) return "remove_project: ignored (no path)";
6257
+ await unregisterProject(api, identity, path14);
6258
+ return `removed project ${path14}`;
5302
6259
  }
5303
6260
  case "update":
5304
6261
  return "update requested";
@@ -5375,7 +6332,7 @@ var PROJECT_MARKERS = [
5375
6332
  ];
5376
6333
  var MAX_ENTRIES2 = 500;
5377
6334
  function resolveBrowsePath(input3) {
5378
- const home = os10.homedir();
6335
+ const home = os7.homedir();
5379
6336
  let p = (input3 ?? "").trim();
5380
6337
  if (!p) return home;
5381
6338
  if (p === "~") return home;
@@ -5400,7 +6357,7 @@ function markers(dirPath) {
5400
6357
  return { project, repo };
5401
6358
  }
5402
6359
  function browseDirectory(input3, opts = {}) {
5403
- const home = os10.homedir();
6360
+ const home = os7.homedir();
5404
6361
  const abs = resolveBrowsePath(input3);
5405
6362
  const parent = path3.dirname(abs);
5406
6363
  const base = {
@@ -5448,6 +6405,26 @@ function browseDirectory(input3, opts = {}) {
5448
6405
  const truncated = all.length > MAX_ENTRIES2;
5449
6406
  return { ...base, entries: truncated ? all.slice(0, MAX_ENTRIES2) : all, truncated };
5450
6407
  }
6408
+ function mkdirDirectory(parentInput, name, opts = {}) {
6409
+ const parent = resolveBrowsePath(parentInput);
6410
+ const clean2 = (name ?? "").trim();
6411
+ const listParent = () => browseDirectory(parent, opts);
6412
+ if (!clean2 || clean2 === "." || clean2 === ".." || clean2.includes("/") || clean2.includes("\\") || clean2.includes("\0")) {
6413
+ return { ...listParent(), error: "Invalid folder name." };
6414
+ }
6415
+ const target = path3.join(parent, clean2);
6416
+ if (path3.dirname(target) !== parent) {
6417
+ return { ...listParent(), error: "Invalid folder name." };
6418
+ }
6419
+ try {
6420
+ fs4.mkdirSync(target, { recursive: false });
6421
+ } catch (err) {
6422
+ const code = err?.code;
6423
+ const message = code === "EEXIST" ? "A folder with that name already exists." : code === "EACCES" || code === "EPERM" ? "Permission denied." : code === "ENOENT" ? "The parent folder no longer exists." : "Could not create the folder.";
6424
+ return { ...listParent(), error: message };
6425
+ }
6426
+ return listParent();
6427
+ }
5451
6428
  var PKG_NAME = "@standardagents/code";
5452
6429
  var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
5453
6430
  var CACHE_REL_DIR = ".config/standardagents-cli";
@@ -5634,7 +6611,7 @@ var UPDATE_CHECK_MS = 6 * 60 * 6e4;
5634
6611
  var SWEEP_MS = 10 * 6e4;
5635
6612
  var MAX_WORKERS = 30;
5636
6613
  var LOG_MAX_BYTES = 1e6;
5637
- var LOG_FILE = path3.join(os10.homedir(), ".standardagents", "daemon.log");
6614
+ var LOG_FILE = path3.join(os7.homedir(), ".standardagents", "daemon.log");
5638
6615
  function daemonLog(line) {
5639
6616
  try {
5640
6617
  fs4.mkdirSync(path3.dirname(LOG_FILE), { recursive: true });
@@ -5653,13 +6630,13 @@ function pathFromTags(tags) {
5653
6630
  const tag = tags.find((t) => t.startsWith("path:"));
5654
6631
  if (!tag) return null;
5655
6632
  const raw = tag.slice("path:".length);
5656
- return raw.replace(/^~(?=\/|$)/, os10.homedir());
6633
+ return raw.replace(/^~(?=\/|$)/, os7.homedir());
5657
6634
  }
5658
6635
  var ThreadWorker = class {
5659
- constructor(api, identity, machineName, threadId, projectDir, createdAt) {
6636
+ constructor(api, identity, machineName, threadId, projectDir, createdAt2) {
5660
6637
  this.threadId = threadId;
5661
6638
  this.projectDir = projectDir;
5662
- this.createdAt = createdAt;
6639
+ this.createdAt = createdAt2;
5663
6640
  this.perm = { level: 1, alwaysAllow: /* @__PURE__ */ new Set(), allowRisk: /* @__PURE__ */ new Set() };
5664
6641
  this.session = new ExecutionSession({
5665
6642
  api,
@@ -5785,7 +6762,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5785
6762
  process.exit(1);
5786
6763
  }
5787
6764
  let displayName = machineDisplayName(await loadMachine(api, identity.machine_id).catch(() => null) ?? {
5788
- hostname: os10.hostname(),
6765
+ hostname: os7.hostname(),
5789
6766
  id: identity.machine_id
5790
6767
  });
5791
6768
  const applied = consumeAppliedUpdate(version);
@@ -5799,8 +6776,19 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5799
6776
  process.on("unhandledRejection", (err) => {
5800
6777
  daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
5801
6778
  });
6779
+ await touchDaemon(api, identity, version).catch(
6780
+ (e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
6781
+ );
6782
+ const heartbeat = setInterval(() => {
6783
+ void touchDaemon(api, identity, version).catch(() => {
6784
+ });
6785
+ void getMachineName(api, identity.machine_id).then((n) => {
6786
+ if (n) displayName = n;
6787
+ }).catch(() => {
6788
+ });
6789
+ }, HEARTBEAT_MS);
5802
6790
  const workers = /* @__PURE__ */ new Map();
5803
- const attach = async (threadId, tags, createdAt = 0) => {
6791
+ const attach = async (threadId, tags, createdAt2 = 0) => {
5804
6792
  if (workers.has(threadId)) return;
5805
6793
  const projectDir = pathFromTags(tags);
5806
6794
  if (!projectDir) return;
@@ -5817,7 +6805,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5817
6805
  daemonLog(`cannot prepare project dir ${projectDir}: ${e instanceof Error ? e.message : String(e)}`);
5818
6806
  return;
5819
6807
  }
5820
- const worker = new ThreadWorker(api, identity, displayName, threadId, projectDir, createdAt || Date.now());
6808
+ const worker = new ThreadWorker(api, identity, displayName, threadId, projectDir, createdAt2 || Date.now());
5821
6809
  worker.onEvicted = (id) => detach(id);
5822
6810
  workers.set(threadId, worker);
5823
6811
  daemonLog(`attached ${threadId.slice(0, 8)} \u2192 ${projectDir}`);
@@ -5856,17 +6844,6 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5856
6844
  });
5857
6845
  events.connect();
5858
6846
  await sweep();
5859
- await touchDaemon(api, identity, version).catch(
5860
- (e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
5861
- );
5862
- const heartbeat = setInterval(() => {
5863
- void touchDaemon(api, identity, version).catch(() => {
5864
- });
5865
- void getMachineName(api, identity.machine_id).then((n) => {
5866
- if (n) displayName = n;
5867
- }).catch(() => {
5868
- });
5869
- }, HEARTBEAT_MS);
5870
6847
  const reclaim = setInterval(() => {
5871
6848
  for (const worker of workers.values()) {
5872
6849
  if (!worker.isOwner) {
@@ -5960,7 +6937,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5960
6937
  let result;
5961
6938
  let error;
5962
6939
  try {
5963
- result = browseDirectory(req.path, { showHidden: req.show_hidden });
6940
+ result = req.op === "mkdir" ? mkdirDirectory(req.path, req.name ?? "", { showHidden: req.show_hidden }) : browseDirectory(req.path, { showHidden: req.show_hidden });
5964
6941
  } catch (err) {
5965
6942
  ok = false;
5966
6943
  error = err instanceof Error ? err.message : "browse failed";
@@ -6043,8 +7020,8 @@ function run2(cmd, args) {
6043
7020
  const output4 = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
6044
7021
  return { ok: res.status === 0, output: output4 };
6045
7022
  }
6046
- var plistPath = () => path3.join(os10.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
6047
- var unitPath = () => path3.join(os10.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
7023
+ var plistPath = () => path3.join(os7.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
7024
+ var unitPath = () => path3.join(os7.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
6048
7025
  var xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6049
7026
  function installService(command, endpoint) {
6050
7027
  if (process.platform === "darwin") return installLaunchd(command, endpoint);
@@ -6056,7 +7033,7 @@ function installService(command, endpoint) {
6056
7033
  };
6057
7034
  }
6058
7035
  function installLaunchd(command, endpoint) {
6059
- const logDir = path3.join(os10.homedir(), ".standardagents");
7036
+ const logDir = path3.join(os7.homedir(), ".standardagents");
6060
7037
  fs4.mkdirSync(logDir, { recursive: true });
6061
7038
  fs4.mkdirSync(path3.dirname(plistPath()), { recursive: true });
6062
7039
  const envEntries = [
@@ -6129,10 +7106,10 @@ WantedBy=default.target
6129
7106
  if (!enable.ok) {
6130
7107
  return { ok: false, detail: `systemctl enable failed: ${enable.output}` };
6131
7108
  }
6132
- const linger = run2("loginctl", ["enable-linger", os10.userInfo().username]);
7109
+ const linger = run2("loginctl", ["enable-linger", os7.userInfo().username]);
6133
7110
  return {
6134
7111
  ok: true,
6135
- detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${os10.userInfo().username}`)
7112
+ detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${os7.userInfo().username}`)
6136
7113
  };
6137
7114
  }
6138
7115
  function uninstallService() {
@@ -6260,7 +7237,7 @@ async function installCommand(endpointFlag) {
6260
7237
  const api = await ensureSignedIn(endpoint);
6261
7238
  const identity = loadMachineIdentity();
6262
7239
  const existing = await loadMachine(api, identity.machine_id).catch(() => null);
6263
- const suggested = machineDisplayName(existing ?? { hostname: os10.hostname(), id: identity.machine_id });
7240
+ const suggested = machineDisplayName(existing ?? { hostname: os7.hostname(), id: identity.machine_id });
6264
7241
  const rl = readline2.createInterface({ input: stdin, output: stdout });
6265
7242
  const answer = (await rl.question(
6266
7243
  `${c2.bold}Machine name${c2.reset} ${c2.dim}(shown in the session picker)${c2.reset} [${suggested}]: `
@@ -6284,12 +7261,12 @@ async function installCommand(endpointFlag) {
6284
7261
  `);
6285
7262
  stdout.write(`${c2.dim}Waiting for the daemon's first heartbeat\u2026${c2.reset}
6286
7263
  `);
6287
- const deadline = Date.now() + 3e4;
7264
+ const deadline = Date.now() + 6e4;
6288
7265
  let alive = false;
6289
7266
  while (Date.now() < deadline) {
6290
7267
  await new Promise((r) => setTimeout(r, 2e3));
6291
- const record = await loadMachine(api, identity.machine_id);
6292
- if (record && daemonOnline(record)) {
7268
+ const record2 = await loadMachine(api, identity.machine_id);
7269
+ if (record2 && daemonOnline(record2)) {
6293
7270
  alive = true;
6294
7271
  break;
6295
7272
  }
@@ -6320,7 +7297,7 @@ async function statusCommand() {
6320
7297
  const cred = getCredential(endpoint);
6321
7298
  if (!cred) {
6322
7299
  stdout.write(
6323
- `${c2.bold}Machine:${c2.reset} ${os10.hostname()} ${c2.dim}(${identity.machine_id})${c2.reset}
7300
+ `${c2.bold}Machine:${c2.reset} ${os7.hostname()} ${c2.dim}(${identity.machine_id})${c2.reset}
6324
7301
  `
6325
7302
  );
6326
7303
  stdout.write(`${c2.bold}Account:${c2.reset} ${c2.yellow}not signed in to ${endpoint}${c2.reset}
@@ -6329,21 +7306,21 @@ async function statusCommand() {
6329
7306
  }
6330
7307
  relaxTlsForLocalEndpoint(endpoint);
6331
7308
  const api = new ApiClient(endpoint, cred.access_token);
6332
- const record = await loadMachine(api, identity.machine_id).catch(() => null);
7309
+ const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
6333
7310
  stdout.write(
6334
- `${c2.bold}Machine:${c2.reset} ${machineDisplayName(record ?? { hostname: os10.hostname(), id: identity.machine_id })} ${c2.dim}(${identity.machine_id})${c2.reset}
7311
+ `${c2.bold}Machine:${c2.reset} ${machineDisplayName(record2 ?? { hostname: os7.hostname(), id: identity.machine_id })} ${c2.dim}(${identity.machine_id})${c2.reset}
6335
7312
  `
6336
7313
  );
6337
- if (!record) {
7314
+ if (!record2) {
6338
7315
  stdout.write(`${c2.bold}Registry:${c2.reset} not registered yet
6339
7316
  `);
6340
7317
  return;
6341
7318
  }
6342
- const online = daemonOnline(record);
6343
- const seen = record.daemon ? `${Math.round((Date.now() - record.daemon.last_seen_at) / 1e3)}s ago (v${record.daemon.version})` : "never";
7319
+ const online = daemonOnline(record2);
7320
+ const seen = record2.daemon ? `${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
6344
7321
  stdout.write(`${c2.bold}Registry:${c2.reset} ${online ? `${c2.green}online${c2.reset}` : `${c2.yellow}offline${c2.reset}`} \xB7 last heartbeat ${seen}
6345
7322
  `);
6346
- const projects = Object.keys(record.projects);
7323
+ const projects = Object.keys(record2.projects);
6347
7324
  stdout.write(`${c2.bold}Projects:${c2.reset} ${projects.length ? "" : c2.dim + "none registered" + c2.reset}
6348
7325
  `);
6349
7326
  for (const p of projects.sort()) stdout.write(` ${c2.dim}${p}${c2.reset}
@@ -6364,8 +7341,8 @@ async function projectCommand(action, target) {
6364
7341
  const endpoint = resolveEndpoint();
6365
7342
  const api = await ensureSignedIn(endpoint);
6366
7343
  const identity = loadMachineIdentity();
6367
- const record = await loadMachine(api, identity.machine_id).catch(() => null);
6368
- const displayName = machineDisplayName(record ?? { hostname: os10.hostname(), id: identity.machine_id });
7344
+ const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
7345
+ const displayName = machineDisplayName(record2 ?? { hostname: os7.hostname(), id: identity.machine_id });
6369
7346
  if (action === "add") {
6370
7347
  await registerProject(api, identity, dir2);
6371
7348
  stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir2} for remote sessions on ${displayName}.
@@ -6425,10 +7402,10 @@ var c3 = {
6425
7402
  reset: "\x1B[0m",
6426
7403
  dim: "\x1B[2m",
6427
7404
  bold: "\x1B[1m",
6428
- white: "\x1B[97m",
7405
+ white: themeWhite,
6429
7406
  cyan: "\x1B[36m",
6430
7407
  green: "\x1B[32m",
6431
- gray: "\x1B[90m",
7408
+ gray: themeGray,
6432
7409
  magenta: "\x1B[35m",
6433
7410
  yellow: "\x1B[33m",
6434
7411
  red: "\x1B[31m",
@@ -6523,7 +7500,7 @@ function printAssistant(tui, text) {
6523
7500
  tui.clearStream();
6524
7501
  tui.print("");
6525
7502
  let dotted = false;
6526
- for (const line of renderMarkdown(text, cols2)) {
7503
+ for (const line of renderStreamingMarkdown(text, cols2)) {
6527
7504
  if (!dotted && line.trim()) {
6528
7505
  tui.print(`${c3.gray}\u2022${c3.reset} ${line}`);
6529
7506
  dotted = true;
@@ -6565,18 +7542,21 @@ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.rese
6565
7542
  `);
6566
7543
  }
6567
7544
  function printWelcome(endpoint, projectDir) {
6568
- const home = os10.homedir();
7545
+ const home = os7.homedir();
6569
7546
  const dir2 = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
6570
7547
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
6571
7548
  const version = readVersion();
6572
7549
  const pad = " ";
7550
+ const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
7551
+ const terminalColumns = Math.max(20, process.stdout.columns || 80);
7552
+ const metaWidth = Math.max(1, terminalColumns - pad.length - markWidth - 3 - 1);
7553
+ const displayDir = truncateMiddle(dir2, metaWidth);
6573
7554
  const meta = [
6574
7555
  `${c3.bold}${gradientText("Standard Code")}${c3.reset}${version ? ` ${c3.dim}v${version}${c3.reset}` : ""}`,
6575
7556
  `${c3.dim}terminal coding agent${c3.reset}`,
6576
7557
  ...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c3.teal}${host}${c3.reset}`],
6577
- `${c3.dim}${dir2}${c3.reset}`
7558
+ `${c3.dim}${displayDir}${c3.reset}`
6578
7559
  ];
6579
- const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
6580
7560
  const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
6581
7561
  stdout.write("\n");
6582
7562
  for (let i = 0; i < LOGO_MARK.length; i++) {
@@ -6633,7 +7613,7 @@ async function main() {
6633
7613
  const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
6634
7614
  const dirArg = cliArgs.dir;
6635
7615
  const projectDir = path3.resolve(dirArg || process.cwd());
6636
- const machine = os10.hostname();
7616
+ const machine = os7.hostname();
6637
7617
  const reader = { rl: null };
6638
7618
  let handoffClosing = false;
6639
7619
  let preflightArmed = false;
@@ -6818,7 +7798,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6818
7798
  void registerProject(api, identity, projectDir).catch(() => {
6819
7799
  });
6820
7800
  const tui = new Tui(1);
6821
- const home = os10.homedir();
7801
+ const home = os7.homedir();
6822
7802
  const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
6823
7803
  const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
6824
7804
  const session = { mode: "local", identity };
@@ -6832,16 +7812,16 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6832
7812
  const where = await tui.select(
6833
7813
  `${c3.bold}${gradientText("Where should this session run?")}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
6834
7814
  [
6835
- { label: `This machine \u2014 ${shortDir}`, hint: "tools run locally", value: null },
7815
+ { label: `\u{1F5A5}\uFE0F This machine \u2014 ${shortDir}`, hint: "tools run locally", value: null },
6836
7816
  ...remoteTargets.map((m) => ({
6837
- label: m.name,
7817
+ label: `${machineIcon(m)} ${m.name}`,
6838
7818
  hint: `${m.hostname} \xB7 daemon online${m.daemon ? ` \xB7 v${m.daemon.version}` : ""}`,
6839
7819
  value: m
6840
7820
  }))
6841
7821
  ]
6842
7822
  );
6843
7823
  if (where) {
6844
- const remotePath = await pickRemoteProject(tui, where);
7824
+ const remotePath = await pickRemoteProject(tui, api, where);
6845
7825
  if (remotePath) {
6846
7826
  session.mode = "remote";
6847
7827
  session.runner = where;
@@ -6877,6 +7857,10 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6877
7857
  if (session.mode === "remote" && session.runner && session.remotePath) {
6878
7858
  await api.kvSet(id, "session_info", { cwd: session.remotePath, machine: session.runner.name }).catch(() => {
6879
7859
  });
7860
+ void enqueueMachineCommand(api, session.runner.id, "add_project", {
7861
+ path: session.remotePath
7862
+ }).catch(() => {
7863
+ });
6880
7864
  }
6881
7865
  return id;
6882
7866
  };
@@ -6916,7 +7900,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6916
7900
  function shortenPath(p, max = 38) {
6917
7901
  return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
6918
7902
  }
6919
- async function pickRemoteProject(tui, runner) {
7903
+ async function pickRemoteProject(tui, api, runner) {
6920
7904
  const ENTER_PATH = "__enter_path__";
6921
7905
  const projects = Object.entries(runner.projects).sort(
6922
7906
  (a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
@@ -6943,7 +7927,7 @@ async function pickRemoteProject(tui, runner) {
6943
7927
  tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
6944
7928
  return null;
6945
7929
  }
6946
- return trimmed;
7930
+ return await resolveRemoteProjectPath(api, runner.id, trimmed);
6947
7931
  }
6948
7932
  function isSilentMessage(m) {
6949
7933
  return m?.silent === true || m?.metadata?.silent === true;
@@ -6978,31 +7962,6 @@ function relativeTime(unixSeconds) {
6978
7962
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
6979
7963
  return `${Math.floor(diff / 86400)}d ago`;
6980
7964
  }
6981
- function hasToolCalls(m) {
6982
- const tc = m?.tool_calls;
6983
- if (Array.isArray(tc)) return tc.length > 0;
6984
- if (typeof tc === "string") {
6985
- const s = tc.trim();
6986
- return s.length > 0 && s !== "null" && s !== "[]";
6987
- }
6988
- return false;
6989
- }
6990
- function messageText(content) {
6991
- if (typeof content === "string") return content;
6992
- if (Array.isArray(content)) {
6993
- return content.map((b) => typeof b === "string" ? b : typeof b?.text === "string" ? b.text : "").join("");
6994
- }
6995
- return "";
6996
- }
6997
- function threadBusy(msgs) {
6998
- if (!msgs.length) return false;
6999
- if (msgs.some((m) => m.status === "pending")) return true;
7000
- const last = [...msgs].sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))[0];
7001
- if (!last) return false;
7002
- if (last.role === "user" || last.role === "tool") return true;
7003
- if (last.role === "assistant") return hasToolCalls(last);
7004
- return false;
7005
- }
7006
7965
  async function printHistory(api, threadId, tui) {
7007
7966
  let msgs;
7008
7967
  try {
@@ -7061,9 +8020,19 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
7061
8020
  saveApprovals(api, threadId, perm);
7062
8021
  const attaching = startLoader("Attaching to thread");
7063
8022
  let busy = false;
7064
- let interrupting = false;
7065
- const queued = [];
7066
- let editingQueued = false;
8023
+ let sharedMessaging = emptySharedMessagingSnapshot();
8024
+ let mirroredDraftRefs = [];
8025
+ let sharedMessagingReady = false;
8026
+ let sharedMessagingUnavailableShown = false;
8027
+ const messagingOrigin = {
8028
+ originClientId: `tui:${Math.random().toString(36).slice(2, 10)}`,
8029
+ originClientKind: "tui"
8030
+ };
8031
+ let editingPendingId = null;
8032
+ let reconcileSharedMessaging = async () => {
8033
+ };
8034
+ let refreshSessionProjection = async () => {
8035
+ };
7067
8036
  const shownIds = /* @__PURE__ */ new Set();
7068
8037
  const pendingSent = /* @__PURE__ */ new Map();
7069
8038
  let lastSent = null;
@@ -7161,6 +8130,10 @@ why: ${req.requestPermission}` : ""}`,
7161
8130
  bridge = exec.bridge;
7162
8131
  }
7163
8132
  const stream = new MessageStream(api, threadId, {
8133
+ onOpen: () => {
8134
+ void reconcileSharedMessaging(true);
8135
+ void refreshSessionProjection();
8136
+ },
7164
8137
  // Live streaming preview: answer text and (opt-in) internal reasoning feed
7165
8138
  // the TUI's ephemeral preview; the committed message still renders from
7166
8139
  // polling, which calls tui.clearStream() first so there's no double-render.
@@ -7168,6 +8141,12 @@ why: ${req.requestPermission}` : ""}`,
7168
8141
  onReasoningChunk: (text, mid) => tui.streamThinkingDelta(text, mid),
7169
8142
  onAssistantText: () => {
7170
8143
  },
8144
+ onMessage: (message) => {
8145
+ if (message?.role === "user" || message?.status === "pending") {
8146
+ busy = true;
8147
+ tui.setWorking(true);
8148
+ }
8149
+ },
7171
8150
  onEvent: (eventType, data) => {
7172
8151
  if (eventType === "generation" && typeof data?.outputTokens === "number") {
7173
8152
  liveOut = data.outputTokens;
@@ -7180,6 +8159,8 @@ why: ${req.requestPermission}` : ""}`,
7180
8159
  refreshStatus();
7181
8160
  } else if (eventType === "goal_updated" && data) {
7182
8161
  tui.setGoal(data);
8162
+ } else if (eventType === SHARED_MESSAGING_EVENT) {
8163
+ void reconcileSharedMessaging(true);
7183
8164
  }
7184
8165
  },
7185
8166
  // A failed turn whose message is the lease service's at-limit denial → offer
@@ -7256,7 +8237,7 @@ why: ${req.requestPermission}` : ""}`,
7256
8237
  const sessionEnded = new Promise((r) => endSession = r);
7257
8238
  const quit = async () => {
7258
8239
  tui.end();
7259
- const stopped2 = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
8240
+ const stopped2 = bridge?.isOwner ?? false ? api.requestSharedStop(threadId, messagingOrigin).catch(() => {
7260
8241
  }) : Promise.resolve();
7261
8242
  const procsStopped2 = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
7262
8243
  bridge?.close();
@@ -7331,23 +8312,89 @@ why: ${req.requestPermission}` : ""}`,
7331
8312
  };
7332
8313
  const extFor = (mime) => ({ "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" })[mime] ?? "bin";
7333
8314
  const toAttachments = (images) => images.map((img) => ({ name: `image-${img.seq}.${extFor(img.mime)}`, mimeType: img.mime, data: img.data }));
7334
- const sendNow = async (text, images = []) => {
7335
- lastSent = { text, images };
7336
- tui.printUserMessage(text);
8315
+ const toSharedAttachments = (images) => toAttachments(images);
8316
+ const fromSharedAttachments = (attachments2) => attachments2.filter(isInlineSharedAttachment).map((attachment, index) => {
8317
+ const namedSequence = /(?:image-|Image\s+)(\d+)/i.exec(attachment.name)?.[1];
8318
+ return {
8319
+ seq: namedSequence ? Number(namedSequence) : index + 1,
8320
+ data: attachment.data,
8321
+ mime: attachment.mimeType
8322
+ };
8323
+ });
8324
+ const applySharedMessaging = (snapshot, mirrorDraft) => {
8325
+ const firstSnapshot = !sharedMessagingReady;
8326
+ const previousDraftRevision = sharedMessaging.draft.revision;
8327
+ sharedMessaging = firstSnapshot ? snapshot : mergeSharedMessagingSnapshot(sharedMessaging, snapshot);
8328
+ sharedMessagingReady = true;
8329
+ sharedMessagingUnavailableShown = false;
8330
+ tui.setQueuedCount(sharedMessaging.pending.items.length);
8331
+ if (mirrorDraft && (firstSnapshot || sharedMessaging.draft.revision > previousDraftRevision) && (firstSnapshot || sharedMessaging.draft.originClientId !== messagingOrigin.originClientId)) {
8332
+ mirroredDraftRefs = sharedMessaging.draft.attachments.filter(isSharedAttachmentRef);
8333
+ tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
8334
+ tui.setInput(
8335
+ sharedMessaging.draft.content,
8336
+ fromSharedAttachments(sharedMessaging.draft.attachments),
8337
+ false,
8338
+ true
8339
+ );
8340
+ }
8341
+ };
8342
+ reconcileSharedMessaging = async (mirrorDraft = false) => {
8343
+ try {
8344
+ applySharedMessaging(await api.getSharedMessaging(threadId), mirrorDraft);
8345
+ } catch (error) {
8346
+ if (!sharedMessagingReady && !sharedMessagingUnavailableShown) {
8347
+ sharedMessagingUnavailableShown = true;
8348
+ tui.print(`${c3.dim}shared messaging unavailable: ${error instanceof Error ? error.message : String(error)}${c3.reset}`);
8349
+ }
8350
+ }
8351
+ };
8352
+ const onTerminalResume = () => void reconcileSharedMessaging(true);
8353
+ process.on("SIGCONT", onTerminalResume);
8354
+ const applySharedMutation = (promise) => promise.then((snapshot) => {
8355
+ applySharedMessaging(snapshot, false);
8356
+ return true;
8357
+ }).catch((error) => {
8358
+ tui.print(`${c3.dim}shared messaging failed: ${error instanceof Error ? error.message : String(error)}${c3.reset}`);
8359
+ return false;
8360
+ });
8361
+ const appendSharedPending = (text, images, refs = []) => applySharedMutation(api.appendPendingInput(threadId, {
8362
+ content: text,
8363
+ attachments: [...refs, ...toSharedAttachments(images)],
8364
+ ...messagingOrigin
8365
+ }));
8366
+ const steerSharedInput = (text, images, refs = []) => applySharedMutation(api.steerInput(threadId, {
8367
+ content: text,
8368
+ attachments: [...refs, ...toSharedAttachments(images)],
8369
+ ...messagingOrigin
8370
+ }));
8371
+ const editSharedPending = (item, text, images, refs) => applySharedMutation(api.updatePendingInput(threadId, item.id, {
8372
+ content: text,
8373
+ attachments: [
8374
+ ...refs,
8375
+ ...toSharedAttachments(images)
8376
+ ],
8377
+ ...messagingOrigin
8378
+ }));
8379
+ const dismissSharedPending = (item) => applySharedMutation(api.dismissPendingInput(threadId, item.id, messagingOrigin));
8380
+ const promoteSharedPending = (item) => applySharedMutation(api.steerPendingInput(threadId, item.id, messagingOrigin));
8381
+ const sendNow = async (text, images = [], refs = []) => {
8382
+ lastSent = { text, images, refs };
8383
+ tui.printUserMessage(text || `\u{1F4CE} ${refs.length + images.length} attachment(s)`);
7337
8384
  const key = text.trim();
7338
8385
  pendingSent.set(key, (pendingSent.get(key) ?? 0) + 1);
7339
8386
  try {
7340
- await api.sendMessage(threadId, text, toAttachments(images));
8387
+ await api.sendMessage(threadId, text, [...refs, ...toAttachments(images)]);
7341
8388
  } catch (e) {
7342
8389
  const n = (pendingSent.get(key) ?? 1) - 1;
7343
8390
  if (n > 0) pendingSent.set(key, n);
7344
8391
  else pendingSent.delete(key);
7345
8392
  tui.print(`${c3.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
7346
- return;
8393
+ return false;
7347
8394
  }
7348
- interrupting = false;
7349
8395
  busy = true;
7350
8396
  tui.setWorking(true);
8397
+ return true;
7351
8398
  };
7352
8399
  const whereLabel = remote ? runnerName : "this machine";
7353
8400
  let bangRunning = false;
@@ -7368,11 +8415,34 @@ why: ${req.requestPermission}` : ""}`,
7368
8415
  bangRunning = false;
7369
8416
  }
7370
8417
  };
7371
- const flushQueued = async () => {
7372
- if (!queued.length) return;
7373
- const toSend = queued.splice(0);
7374
- tui.setQueuedCount(0);
7375
- for (const q of toSend) await sendNow(q.text, q.images);
8418
+ const openPendingMenu = async () => {
8419
+ const items = sharedMessaging.pending.items;
8420
+ if (!items.length) {
8421
+ tui.print(`${c3.dim}No pending messages.${c3.reset}`);
8422
+ return;
8423
+ }
8424
+ const picked = await tui.select("Pending messages", items.map((item, index) => ({
8425
+ label: item.content.replace(/\s+/g, " "),
8426
+ hint: `${index + 1} of ${items.length}`,
8427
+ value: item
8428
+ })));
8429
+ if (!picked) return;
8430
+ const action = await tui.select("Pending message", [
8431
+ { label: "Edit", value: "edit" },
8432
+ { label: "Steer next", value: "steer" },
8433
+ { label: "Dismiss", value: "dismiss" }
8434
+ ]);
8435
+ if (!action) return;
8436
+ if (action === "edit") {
8437
+ editingPendingId = picked.id;
8438
+ mirroredDraftRefs = picked.attachments.filter(isSharedAttachmentRef);
8439
+ tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
8440
+ tui.setInput(picked.content, fromSharedAttachments(picked.attachments), true, true);
8441
+ } else if (action === "steer") {
8442
+ await promoteSharedPending(picked);
8443
+ } else {
8444
+ await dismissSharedPending(picked);
8445
+ }
7376
8446
  };
7377
8447
  const requestCompaction = async () => {
7378
8448
  try {
@@ -7476,7 +8546,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7476
8546
  tui.print(`${c3.green}\u2713${c3.reset} ${c3.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c3.reset}`);
7477
8547
  if (opts.auto && lastSent) {
7478
8548
  tui.print(`${c3.gray}Continuing\u2026${c3.reset}`);
7479
- await sendNow(lastSent.text, lastSent.images);
8549
+ await sendNow(lastSent.text, lastSent.images, lastSent.refs);
7480
8550
  }
7481
8551
  } finally {
7482
8552
  upgradeInFlight = false;
@@ -7509,6 +8579,15 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7509
8579
  hint: () => tui.contextPctLabel() || "free up context",
7510
8580
  run: requestCompaction
7511
8581
  },
8582
+ {
8583
+ name: "queue",
8584
+ label: "Pending messages",
8585
+ hint: () => {
8586
+ const count = sharedMessaging.pending.items.length;
8587
+ return count ? `${count} pending` : "none";
8588
+ },
8589
+ run: openPendingMenu
8590
+ },
7512
8591
  { name: "level", label: "Auto-accept level", hint: () => `level ${tui.level}`, run: () => runLevelMenu(tui, perm) },
7513
8592
  {
7514
8593
  name: "permissions",
@@ -7570,9 +8649,46 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7570
8649
  ]);
7571
8650
  const history = await loadHistory(api, threadId, historySeedThreadId);
7572
8651
  tui.setHistory(history);
7573
- tui.onSubmit = (text, images) => {
8652
+ await reconcileSharedMessaging(true);
8653
+ let draftTimer;
8654
+ const clearComposerDraft = () => {
8655
+ if (draftTimer) clearTimeout(draftTimer);
8656
+ draftTimer = void 0;
8657
+ mirroredDraftRefs = [];
8658
+ tui.setExternalAttachmentNames([]);
8659
+ if (sharedMessagingReady) void applySharedMutation(api.clearSharedDraft(threadId, messagingOrigin));
8660
+ };
8661
+ tui.onDraftChange = (textVal, images) => {
8662
+ if (draftTimer) clearTimeout(draftTimer);
8663
+ draftTimer = setTimeout(() => {
8664
+ draftTimer = void 0;
8665
+ if (sharedMessagingReady) {
8666
+ const hasDraft = !!textVal.trim() || images.length > 0 || mirroredDraftRefs.length > 0;
8667
+ const mutation = {
8668
+ content: textVal,
8669
+ attachments: [
8670
+ ...hasDraft ? mirroredDraftRefs : [],
8671
+ ...toSharedAttachments(images)
8672
+ ],
8673
+ ...messagingOrigin
8674
+ };
8675
+ void applySharedMutation(
8676
+ hasDraft ? api.putSharedDraft(threadId, mutation) : api.clearSharedDraft(threadId, messagingOrigin)
8677
+ );
8678
+ }
8679
+ }, 150);
8680
+ };
8681
+ const submitComposer = async (text, images, steer) => {
8682
+ const draftRefs = mirroredDraftRefs;
8683
+ if (!sharedMessagingReady && (busy || steer || editingPendingId !== null)) {
8684
+ tui.print(`${c3.dim}Restoring shared message state \u2014 try again in a moment.${c3.reset}`);
8685
+ tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
8686
+ tui.setInput(text, images);
8687
+ return;
8688
+ }
8689
+ clearComposerDraft();
7574
8690
  const trimmed = text.trimStart();
7575
- if (trimmed.startsWith("!") && !trimmed.startsWith("!!")) {
8691
+ if (trimmed.startsWith("!") && !trimmed.startsWith("!!") && images.length === 0 && draftRefs.length === 0) {
7576
8692
  const command = trimmed.slice(1).trim();
7577
8693
  if (command) {
7578
8694
  appendHistory(api, threadId, history, text);
@@ -7581,38 +8697,71 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7581
8697
  return;
7582
8698
  }
7583
8699
  const outgoing = trimmed.startsWith("!!") ? text.replace("!!", "!") : text;
7584
- appendHistory(api, threadId, history, outgoing);
8700
+ if (outgoing.trim()) appendHistory(api, threadId, history, outgoing);
7585
8701
  text = outgoing;
7586
- if (editingQueued) {
7587
- editingQueued = false;
7588
- queued.push({ text, images });
7589
- tui.setQueuedCount(queued.length);
7590
- tui.print(`${c3.gray}\u23F3 queued:${c3.reset} ${text}`);
8702
+ if (editingPendingId) {
8703
+ const pendingId = editingPendingId;
8704
+ const item = sharedMessaging.pending.items.find((candidate) => candidate.id === pendingId);
8705
+ editingPendingId = null;
8706
+ if (!item) {
8707
+ tui.print(`${c3.dim}That pending message was already dispatched or dismissed.${c3.reset}`);
8708
+ return;
8709
+ }
8710
+ const updated = await editSharedPending(item, text, images, draftRefs);
8711
+ const promoted = !steer || !updated ? updated : await applySharedMutation(api.steerPendingInput(threadId, pendingId, messagingOrigin));
8712
+ if (!promoted) {
8713
+ mirroredDraftRefs = item.attachments.filter(isSharedAttachmentRef);
8714
+ tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
8715
+ tui.setInput(text, images);
8716
+ }
8717
+ return;
8718
+ }
8719
+ if (steer) {
8720
+ tui.print(`${c3.yellow}\u21AA steering at the next safe model boundary:${c3.reset} ${text}`);
8721
+ if (!await steerSharedInput(text, images, draftRefs)) {
8722
+ mirroredDraftRefs = draftRefs;
8723
+ tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
8724
+ tui.setInput(text, images);
8725
+ }
7591
8726
  return;
7592
8727
  }
7593
8728
  if (busy) {
7594
- queued.push({ text, images });
7595
- tui.setQueuedCount(queued.length);
7596
- tui.print(`${c3.gray}\u23F3 queued:${c3.reset} ${text} ${c3.dim}(esc to steer now)${c3.reset}`);
8729
+ if (await appendSharedPending(text, images, draftRefs)) {
8730
+ tui.print(`${c3.gray}\u23F3 pending:${c3.reset} ${text} ${c3.dim}(/queue to edit, steer, or dismiss)${c3.reset}`);
8731
+ } else {
8732
+ mirroredDraftRefs = draftRefs;
8733
+ tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
8734
+ tui.setInput(text, images);
8735
+ }
7597
8736
  } else {
7598
- void sendNow(text, images);
8737
+ if (!await sendNow(text, images, draftRefs)) {
8738
+ mirroredDraftRefs = draftRefs;
8739
+ tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
8740
+ tui.setInput(text, images);
8741
+ }
7599
8742
  }
7600
8743
  };
8744
+ tui.onSubmit = (text, images) => {
8745
+ void submitComposer(text, images, false);
8746
+ };
8747
+ tui.onSteer = (text, images) => {
8748
+ void submitComposer(text, images, true);
8749
+ };
7601
8750
  tui.onInterrupt = () => {
7602
- if (queued.length > 0) {
7603
- tui.print(`${c3.yellow}\u21AA steering \u2014 stopping current work and sending your message\u2026${c3.reset}`);
7604
- void api.stop(threadId).catch(() => {
7605
- }).then(() => flushQueued());
7606
- } else if (busy) {
7607
- interrupting = true;
7608
- busy = false;
7609
- activeSteps.clear();
7610
- liveOut = 0;
7611
- tui.setWorking(false);
7612
- refreshStatus();
7613
- tui.print(`${c3.yellow}[interrupted by user]${c3.reset}`);
7614
- void api.stop(threadId).catch(() => {
7615
- });
8751
+ const firstPending = sharedMessaging.pending.items[0];
8752
+ if (!busy && firstPending) {
8753
+ tui.print(`${c3.yellow}\u21AA steering the first pending message\u2026${c3.reset}`);
8754
+ void promoteSharedPending(firstPending);
8755
+ return;
8756
+ }
8757
+ if (busy) {
8758
+ if (!sharedMessagingReady) {
8759
+ tui.print(`${c3.dim}Shared messaging is not connected; the session was not stopped.${c3.reset}`);
8760
+ return;
8761
+ }
8762
+ const advancing = sharedMessaging.pending.items.length > 0;
8763
+ tui.print(`${c3.yellow}${advancing ? "[stopping; next pending message will run]" : "[stopping at the next safe boundary]"}${c3.reset}`);
8764
+ void applySharedMutation(api.requestSharedStop(threadId, messagingOrigin));
7616
8765
  }
7617
8766
  };
7618
8767
  tui.onBgBadge = () => {
@@ -7620,11 +8769,13 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7620
8769
  });
7621
8770
  };
7622
8771
  tui.onUpArrow = () => {
7623
- if (tui.getInput().trim() || queued.length === 0) return false;
7624
- const q = queued.pop();
7625
- tui.setQueuedCount(queued.length);
7626
- editingQueued = true;
7627
- tui.setInput(q.text, q.images);
8772
+ if (tui.getInput().trim() || tui.hasExternalAttachments()) return false;
8773
+ const item = sharedMessaging.pending.items.at(-1);
8774
+ if (!item) return false;
8775
+ editingPendingId = item.id;
8776
+ mirroredDraftRefs = item.attachments.filter(isSharedAttachmentRef);
8777
+ tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
8778
+ tui.setInput(item.content, fromSharedAttachments(item.attachments), true, true);
7628
8779
  return true;
7629
8780
  };
7630
8781
  events.connect();
@@ -7700,16 +8851,26 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7700
8851
  };
7701
8852
  const poll = async () => {
7702
8853
  let msgs;
8854
+ let serverBusy = null;
8855
+ let serverTool = null;
7703
8856
  try {
7704
- msgs = await api.getMessages(threadId, 60);
8857
+ const snapshot = await api.getSessionState(threadId);
8858
+ msgs = snapshot.messages.slice(-60);
8859
+ serverBusy = snapshot.busy;
8860
+ serverTool = snapshot.current_tool;
7705
8861
  } catch {
7706
- return;
8862
+ try {
8863
+ msgs = await api.getMessages(threadId, 60);
8864
+ } catch {
8865
+ return;
8866
+ }
7707
8867
  }
7708
8868
  const sorted = [...msgs].sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
7709
8869
  for (const m of sorted) {
7710
- if (shownIds.has(m.id) || m.status === "pending") continue;
7711
- shownIds.add(m.id);
8870
+ if (shownIds.has(m.id)) continue;
7712
8871
  const text = messageText(m.content).trim();
8872
+ if (!transcriptMessageReady(m, text)) continue;
8873
+ shownIds.add(m.id);
7713
8874
  const denial = typeof m.error === "string" && m.error || text;
7714
8875
  if (denial && isSessionLimitError(denial)) {
7715
8876
  void offerUpgrade({ auto: true });
@@ -7736,19 +8897,20 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7736
8897
  }
7737
8898
  }
7738
8899
  }
7739
- const polledBusy = threadBusy(msgs);
7740
- if (interrupting) {
7741
- if (!polledBusy) interrupting = false;
7742
- busy = false;
7743
- } else {
7744
- busy = polledBusy;
8900
+ const polledBusy = (serverBusy ?? false) || threadBusy(msgs);
8901
+ if (serverTool && !activeSteps.has(serverTool.id)) {
8902
+ activeSteps.set(serverTool.id, serverTool.name || "working");
8903
+ refreshStatus();
8904
+ } else if (serverBusy !== null && !serverTool && activeSteps.size) {
8905
+ activeSteps.clear();
8906
+ refreshStatus();
7745
8907
  }
8908
+ busy = polledBusy;
7746
8909
  tui.setWorking(busy);
7747
8910
  if (!busy) {
7748
8911
  if (activeSteps.size) activeSteps.clear();
7749
8912
  liveOut = 0;
7750
8913
  refreshStatus();
7751
- if (queued.length > 0 && !editingQueued) await flushQueued();
7752
8914
  }
7753
8915
  refreshBgCount();
7754
8916
  void relayApprovals().catch(() => {
@@ -7784,12 +8946,17 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7784
8946
  } catch {
7785
8947
  }
7786
8948
  };
7787
- const pollTimer = setInterval(() => void poll().catch(() => {
7788
- }), 1200);
8949
+ refreshSessionProjection = () => poll().catch(() => {
8950
+ });
8951
+ await refreshSessionProjection();
8952
+ const pollTimer = setInterval(() => {
8953
+ if (busy || approvalPromptOpen) void refreshSessionProjection();
8954
+ }, 1200);
7789
8955
  await sessionEnded;
7790
8956
  clearInterval(pollTimer);
7791
8957
  clearInterval(heartbeatPoll);
7792
- const stopped = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
8958
+ process.off("SIGCONT", onTerminalResume);
8959
+ const stopped = bridge?.isOwner ?? false ? api.requestSharedStop(threadId, messagingOrigin).catch(() => {
7793
8960
  }) : Promise.resolve();
7794
8961
  const procsStopped = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
7795
8962
  bridge?.close();
@@ -7898,7 +9065,7 @@ async function runMachinesMenu(tui, api, self) {
7898
9065
  const daemonBit = m.daemon ? online ? "daemon online" : "daemon offline" : "no daemon";
7899
9066
  const nproj = Object.keys(m.projects).length;
7900
9067
  return {
7901
- label: `${m.name}${isSelf ? " (this machine)" : ""}`,
9068
+ label: `${machineIcon(m)} ${m.name}${isSelf ? " (this machine)" : ""}`,
7902
9069
  hint: `${m.hostname} \xB7 ${m.platform}/${m.arch} \xB7 v${m.version ?? "?"} \xB7 ${daemonBit} \xB7 ${nproj} project${nproj === 1 ? "" : "s"}`,
7903
9070
  value: m.id
7904
9071
  };
@@ -7913,7 +9080,8 @@ async function manageMachine(tui, api, self, machine) {
7913
9080
  const online = daemonOnline(machine);
7914
9081
  const canRunCommands = isSelf || !!machine.daemon;
7915
9082
  const options = [
7916
- { label: "Rename", value: "rename" }
9083
+ { label: "Rename", value: "rename" },
9084
+ { label: "Set icon", hint: machine.icon || "default", value: "icon" }
7917
9085
  ];
7918
9086
  if (canRunCommands) {
7919
9087
  options.push(
@@ -7931,8 +9099,22 @@ async function manageMachine(tui, api, self, machine) {
7931
9099
  `${c3.dim}${machine.name}'s daemon is offline \u2014 queued changes apply when it next comes online.${c3.reset}`
7932
9100
  );
7933
9101
  }
7934
- const action = await tui.select(`${c3.bold}${machine.name}${c3.reset}`, options);
9102
+ const action = await tui.select(`${c3.bold}${machineIcon(machine)} ${machine.name}${c3.reset}`, options);
7935
9103
  if (!action || action === "back") return;
9104
+ if (action === "icon") {
9105
+ const current = machine.icon ?? "";
9106
+ const emoji = await tui.prompt(
9107
+ `${c3.bold}Icon for ${machine.name}${c3.reset} ${c3.dim}(paste an emoji, blank to reset)${c3.reset}`,
9108
+ current
9109
+ );
9110
+ if (emoji !== null) {
9111
+ const trimmed = emoji.trim();
9112
+ await setMachineIcon(api, machine.id, trimmed);
9113
+ machine.icon = trimmed || void 0;
9114
+ tui.print(`${c3.green}\u2713${c3.reset} icon ${trimmed ? `set to ${trimmed}` : "reset"} for ${machine.name}`);
9115
+ }
9116
+ return manageMachine(tui, api, self, machine);
9117
+ }
7936
9118
  const dispatch = async (kind, args) => {
7937
9119
  if (isSelf) {
7938
9120
  await applyMachineCommand(api, self, { kind, args});
@@ -7974,12 +9156,12 @@ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNot
7974
9156
  );
7975
9157
  if (!picked) return;
7976
9158
  if (picked === ADD) {
7977
- const path13 = await tui.prompt(
9159
+ const path14 = await tui.prompt(
7978
9160
  `Absolute project path on ${machine.name}`,
7979
9161
  machine.id === self.machine_id ? process.cwd() : "/home/you/project"
7980
9162
  );
7981
- if (!path13 || !path13.trim()) return;
7982
- const trimmed = path13.trim();
9163
+ if (!path14 || !path14.trim()) return;
9164
+ const trimmed = path14.trim();
7983
9165
  if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
7984
9166
  tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
7985
9167
  return;