@standardagents/code 0.9.5 → 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("..")) {
@@ -2418,6 +2711,7 @@ var MessageStream = class {
2418
2711
  ws.addEventListener("open", () => {
2419
2712
  this.reconnectAttempt = 0;
2420
2713
  this.startHeartbeat(ws);
2714
+ this.hooks.onOpen?.();
2421
2715
  this.resolveConnected?.();
2422
2716
  });
2423
2717
  ws.addEventListener("message", (ev) => {
@@ -2435,7 +2729,7 @@ var MessageStream = class {
2435
2729
  }
2436
2730
  startHeartbeat(ws) {
2437
2731
  this.stopHeartbeat();
2438
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
2732
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
2439
2733
  this.heartbeat.start();
2440
2734
  }
2441
2735
  stopHeartbeat() {
@@ -2484,10 +2778,11 @@ var MessageStream = class {
2484
2778
  }
2485
2779
  if (msg.type === "message_data" && (msg.depth ?? 0) === 0) {
2486
2780
  const data = msg.data || {};
2781
+ this.hooks.onMessage?.(data);
2487
2782
  if (data.role === "assistant" && typeof data.content === "string" && data.content.trim()) {
2488
2783
  const tc = data.tool_calls;
2489
- const hasToolCalls2 = Array.isArray(tc) ? tc.length > 0 : typeof tc === "string" && tc.trim() !== "" && tc.trim() !== "null" && tc.trim() !== "[]";
2490
- 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);
2491
2786
  }
2492
2787
  if (data.role === "assistant" && data.status === "failed" && data.error) {
2493
2788
  this.hooks.onError(String(data.error));
@@ -2537,7 +2832,7 @@ var SystemEvents = class {
2537
2832
  }
2538
2833
  startHeartbeat(ws) {
2539
2834
  this.stopHeartbeat();
2540
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
2835
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
2541
2836
  this.heartbeat.start();
2542
2837
  }
2543
2838
  stopHeartbeat() {
@@ -2670,6 +2965,664 @@ var SubagentActivity = class {
2670
2965
  }
2671
2966
  };
2672
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
+
2673
3626
  // src/wordmill.ts
2674
3627
  var MILL_WORDS = [
2675
3628
  "Working",
@@ -2711,8 +3664,8 @@ var HOLD_MS = 2600;
2711
3664
  var LAZY_MS = 320;
2712
3665
  var LAZY_PERIOD = 200;
2713
3666
  var FAST_PERIOD = 70;
2714
- var BOLD = "\x1B[1m";
2715
- var DIM3 = "\x1B[2m";
3667
+ var BOLD2 = "\x1B[1m";
3668
+ var DIM4 = "\x1B[2m";
2716
3669
  var OFF = "\x1B[22m";
2717
3670
  function glyphAt(slot, bucket) {
2718
3671
  let h = (slot + 1) * 2654435761 ^ (bucket + 1) * 40503;
@@ -2747,7 +3700,7 @@ var WordMill = class {
2747
3700
  if (this.phaseAt === 0) this.phaseAt = now;
2748
3701
  if (!this.target) {
2749
3702
  if (now - this.phaseAt >= HOLD_MS) this.beginMorph(now);
2750
- else return `${BOLD}${this.word}${OFF}`;
3703
+ else return `${BOLD2}${this.word}${OFF}`;
2751
3704
  }
2752
3705
  return this.morphFrame(now);
2753
3706
  }
@@ -2773,21 +3726,21 @@ var WordMill = class {
2773
3726
  this.word = target;
2774
3727
  this.target = null;
2775
3728
  this.phaseAt = now;
2776
- return `${BOLD}${this.word}${OFF}`;
3729
+ return `${BOLD2}${this.word}${OFF}`;
2777
3730
  }
2778
3731
  let out = "";
2779
3732
  for (let i = 0; i < this.slots.length; i++) {
2780
3733
  const s = this.slots[i];
2781
3734
  if (t < s.start) {
2782
3735
  const ch = this.word[i];
2783
- if (ch) out += `${BOLD}${ch}${OFF}`;
3736
+ if (ch) out += `${BOLD2}${ch}${OFF}`;
2784
3737
  } else if (t < s.land) {
2785
3738
  const age = t - s.start;
2786
3739
  const period = age < LAZY_MS ? LAZY_PERIOD : FAST_PERIOD;
2787
- out += `${DIM3}${glyphAt(i, Math.floor(t / period))}${OFF}`;
3740
+ out += `${DIM4}${glyphAt(i, Math.floor(t / period))}${OFF}`;
2788
3741
  } else {
2789
3742
  const ch = target[i];
2790
- if (ch) out += `${BOLD}${ch}${OFF}`;
3743
+ if (ch) out += `${BOLD2}${ch}${OFF}`;
2791
3744
  }
2792
3745
  }
2793
3746
  return out;
@@ -2833,7 +3786,7 @@ function fromFile(filePath) {
2833
3786
  }
2834
3787
  }
2835
3788
  async function readDarwin() {
2836
- 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`);
2837
3790
  const script = [
2838
3791
  `set d to the clipboard as \xABclass PNGf\xBB`,
2839
3792
  `set f to open for access POSIX file "${tmp}" with write permission`,
@@ -2870,7 +3823,7 @@ async function readLinux() {
2870
3823
  return null;
2871
3824
  }
2872
3825
  async function readWindows() {
2873
- 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`);
2874
3827
  const ps = [
2875
3828
  "Add-Type -AssemblyName System.Windows.Forms;",
2876
3829
  "$img = [System.Windows.Forms.Clipboard]::GetImage();",
@@ -2898,6 +3851,11 @@ async function readClipboardImage() {
2898
3851
  function imagePlaceholder(seq) {
2899
3852
  return `[#Image ${seq}]`;
2900
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
+ }
2901
3859
  var INPUT_BOX_MARGIN = 1;
2902
3860
  function inputBoxBorderColor() {
2903
3861
  return "\x1B[38;5;240m";
@@ -3091,8 +4049,13 @@ var C = {
3091
4049
  gray: themeGray,
3092
4050
  teal: "\x1B[38;5;37m"
3093
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";
3094
4056
  var FRAMES = ["\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
3095
- var SPINNER_MS = 70;
4057
+ var SPINNER_FRAME_MS = 70;
4058
+ var ANIMATION_TICK_MS = 1e3 / 60;
3096
4059
  var SUBAGENT_COLORS = [
3097
4060
  "\x1B[35m",
3098
4061
  // magenta
@@ -3165,6 +4128,7 @@ var Tui = class _Tui {
3165
4128
  // answer text (plain). Bounded to a tail; cleared when the message commits.
3166
4129
  streamThinking = "";
3167
4130
  streamResponse = "";
4131
+ streamDraft = emptyLiveDraft();
3168
4132
  streamMessageId = null;
3169
4133
  // the message currently previewing
3170
4134
  streamRedrawTimer = null;
@@ -3194,7 +4158,7 @@ var Tui = class _Tui {
3194
4158
  // rows re-wrap (a full-width ruler becomes 2+ physical rows when narrowed),
3195
4159
  // so the caret-relative move-up from the last paint is stale. We store the
3196
4160
  // visible width of EVERY region row (not just above the body) plus the caret
3197
- // row index so moveToRegionTop can recompute physical height under the new
4161
+ // row index so regionTopSequence can recompute physical height under the new
3198
4162
  // wrap. Resize events are debounced — drag-resizing fires dozens of events
3199
4163
  // and redrawing each one desyncs and leaves ghost chrome.
3200
4164
  lastDrawnCols = 0;
@@ -3221,6 +4185,9 @@ var Tui = class _Tui {
3221
4185
  // placeholder at the caret; on submit only images whose placeholder is still
3222
4186
  // present in the text are handed to onSubmit. Cleared with the input.
3223
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 = [];
3224
4191
  imagePasteBusy = false;
3225
4192
  // one clipboard read at a time
3226
4193
  // Sent-message history for ↑/↓ recall (oldest → newest). `historyIdx` is the
@@ -3234,6 +4201,9 @@ var Tui = class _Tui {
3234
4201
  // event hooks (wired by index.ts)
3235
4202
  onSubmit = () => {
3236
4203
  };
4204
+ /** Shift+Return sends a steering input through the portable messaging endpoint. */
4205
+ onSteer = () => {
4206
+ };
3237
4207
  onInterrupt = () => {
3238
4208
  };
3239
4209
  /** Up on the top row: return true to consume it (e.g. pull a queued message)
@@ -3242,8 +4212,8 @@ var Tui = class _Tui {
3242
4212
  /** Enter while the `[⚙ n bg]` badge is selected — opens the bg process panel. */
3243
4213
  onBgBadge = () => {
3244
4214
  };
3245
- /** Fired (deduped) when the input draft text changes index.ts debounce-writes
3246
- * it into the shared cross-surface presence buffer. */
4215
+ /** Fired (deduped) when the composer changes so index.ts can persist the
4216
+ * shared packed-endpoint draft, including attachments. */
3247
4217
  onDraftChange = () => {
3248
4218
  };
3249
4219
  lastDraftSeen = "";
@@ -3329,7 +4299,7 @@ var Tui = class _Tui {
3329
4299
  dispatch(str, key) {
3330
4300
  const seq = key && key.sequence || str || "";
3331
4301
  if (seq === "\n" || seq === "\x1B[13;2u" || seq === "\x1B[27;2;13~") {
3332
- this.insertAtCursor("\n");
4302
+ if (!this.takeoverHandler && !this.pasting && !this.paletteOpen()) this.submitInput(true);
3333
4303
  return;
3334
4304
  }
3335
4305
  if (key && key.ctrl && key.name === "c") {
@@ -3398,10 +4368,7 @@ var Tui = class _Tui {
3398
4368
  return;
3399
4369
  }
3400
4370
  if (key.name === "return" || key.name === "enter") {
3401
- if (key.shift) {
3402
- this.insertAtCursor("\n");
3403
- return;
3404
- }
4371
+ if (key.shift) return;
3405
4372
  if (matches.length) this.runCommand(matches[cur]);
3406
4373
  return;
3407
4374
  }
@@ -3464,23 +4431,15 @@ var Tui = class _Tui {
3464
4431
  return;
3465
4432
  }
3466
4433
  if (key.name === "return" || key.name === "enter") {
3467
- if (key.shift) {
4434
+ if (key.meta) {
3468
4435
  this.insertAtCursor("\n");
3469
4436
  return;
3470
4437
  }
3471
- const text = this.inputBuffer;
3472
- const images = this.pendingImages.filter((img) => text.includes(imagePlaceholder(img.seq)));
3473
- this.inputBuffer = "";
3474
- this.cursorPos = 0;
3475
- this.pendingImages = [];
3476
- this.historyIdx = null;
3477
- this.historyDraft = "";
3478
- this.historyDraftImages = [];
3479
- this.renderBottom();
3480
- if (text.trim()) {
3481
- this.addHistoryEntry(text.trim());
3482
- this.onSubmit(text.trim(), images);
4438
+ if (key.shift) {
4439
+ this.submitInput(true);
4440
+ return;
3483
4441
  }
4442
+ this.submitInput(false);
3484
4443
  return;
3485
4444
  }
3486
4445
  if (key.ctrl && key.name === "v") {
@@ -3511,6 +4470,23 @@ var Tui = class _Tui {
3511
4470
  this.insertAtCursor(str);
3512
4471
  }
3513
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
+ }
3514
4490
  insertAtCursor(text) {
3515
4491
  this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos) + text + this.inputBuffer.slice(this.cursorPos);
3516
4492
  this.cursorPos += text.length;
@@ -3713,7 +4689,7 @@ var Tui = class _Tui {
3713
4689
  }
3714
4690
  spinnerFrame() {
3715
4691
  const now = Date.now();
3716
- 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}`;
3717
4693
  }
3718
4694
  /** "↑X ↓Y" cumulative token totals (greyed — low-priority). */
3719
4695
  tokensText() {
@@ -3753,7 +4729,7 @@ var Tui = class _Tui {
3753
4729
  }
3754
4730
  /** Is the `/` command palette currently showing? (input starts with "/".) */
3755
4731
  paletteOpen() {
3756
- 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("/");
3757
4733
  }
3758
4734
  /** Commands matching the text typed after "/", in declared order. */
3759
4735
  filteredCommands() {
@@ -3812,9 +4788,10 @@ var Tui = class _Tui {
3812
4788
  /** The prompt line prefix (with ANSI colour) that precedes the typed text. */
3813
4789
  promptPrefix() {
3814
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} ` : "";
3815
4792
  const bgText = `[\u2699 ${this.bgCount} bg]`;
3816
4793
  const bg = this.bgCount > 0 ? this.bgBadgeSelected ? `${C.cyan}\x1B[7m${bgText}\x1B[27m${C.reset} ` : `${C.cyan}${bgText}${C.reset} ` : "";
3817
- return `${q}${bg}${this.levelColor()}\u276F${C.reset} `;
4794
+ return `${q}${attachments2}${bg}${this.levelColor()}\u276F${C.reset} `;
3818
4795
  }
3819
4796
  visibleWidth(s) {
3820
4797
  let w = 0;
@@ -3889,20 +4866,21 @@ var Tui = class _Tui {
3889
4866
  if (matches.length === 0) return [` ${C.gray}no matching command${C.reset}`];
3890
4867
  const cur = Math.min(this.slashIdx, matches.length - 1);
3891
4868
  const pointerW = 2;
4869
+ const rowCols = Math.max(1, cols2 - 1);
3892
4870
  return matches.map((cmd, i) => {
3893
4871
  const sel = i === cur;
3894
4872
  const hint = (typeof cmd.hint === "function" ? cmd.hint() : cmd.hint) ?? "";
3895
4873
  const hintW = hint.length;
3896
4874
  const name = `/${cmd.name}`;
3897
4875
  let visible = `${name} ${cmd.label}`;
3898
- const labelMax = Math.max(6, cols2 - pointerW - (hintW ? hintW + 2 : 0));
4876
+ const labelMax = Math.max(6, rowCols - pointerW - (hintW ? hintW + 2 : 0));
3899
4877
  if (visible.length > labelMax) visible = visible.slice(0, labelMax - 1) + "\u2026";
3900
4878
  const desc = visible.slice(name.length);
3901
4879
  const pointer = sel ? `${C.magenta}\u276F${C.reset} ` : " ";
3902
4880
  const nameStyled = sel ? `${C.bold}${C.cyan}${name}${C.reset}` : `${C.cyan}${name}${C.reset}`;
3903
4881
  let line = `${pointer}${nameStyled}${C.gray}${desc}${C.reset}`;
3904
4882
  if (hintW) {
3905
- const gap = Math.max(2, cols2 - pointerW - visible.length - hintW);
4883
+ const gap = Math.max(2, rowCols - pointerW - visible.length - hintW);
3906
4884
  line += `${" ".repeat(gap)}${C.gray}${hint}${C.reset}`;
3907
4885
  }
3908
4886
  return line;
@@ -3924,9 +4902,9 @@ var Tui = class _Tui {
3924
4902
  * the NEW wrap: rows above the caret row + (caret row's rewrap − 1) so we
3925
4903
  * prefer a slight over-move (clean wipe) over under-move (ghost chrome).
3926
4904
  */
3927
- moveToRegionTop() {
3928
- process.stdout.write("\r");
3929
- if (!this.bottomDrawn) return;
4905
+ regionTopSequence() {
4906
+ let sequence = "\r";
4907
+ if (!this.bottomDrawn) return sequence;
3930
4908
  const cols2 = process.stdout.columns || 80;
3931
4909
  let up;
3932
4910
  if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0 && this.drawnRegionWidths.length) {
@@ -3943,7 +4921,8 @@ var Tui = class _Tui {
3943
4921
  } else {
3944
4922
  up = this.lastCursorRow;
3945
4923
  }
3946
- if (up > 0) process.stdout.write(`\x1B[${up}A`);
4924
+ if (up > 0) sequence += `\x1B[${up}A`;
4925
+ return sequence;
3947
4926
  }
3948
4927
  /**
3949
4928
  * Render the bottom region: status + subagents above a side-margined expanding
@@ -3959,14 +4938,15 @@ var Tui = class _Tui {
3959
4938
  if (!this.started || this.takeoverHandler) return;
3960
4939
  if (this.inputBuffer !== this.lastDraftSeen) {
3961
4940
  this.lastDraftSeen = this.inputBuffer;
4941
+ const images = this.pendingImages.filter((img) => this.inputBuffer.includes(imagePlaceholder(img.seq)));
3962
4942
  try {
3963
- this.onDraftChange(this.inputBuffer);
4943
+ this.onDraftChange(this.inputBuffer, images);
3964
4944
  } catch {
3965
4945
  }
3966
4946
  }
3967
4947
  if (this.resizePending) return;
3968
4948
  const cols2 = process.stdout.columns || 80;
3969
- this.moveToRegionTop();
4949
+ const moveToTop = this.regionTopSequence();
3970
4950
  const hudWidths = [];
3971
4951
  const hudRows = [];
3972
4952
  const rowCap = Math.max(1, cols2 - 1);
@@ -3987,7 +4967,7 @@ var Tui = class _Tui {
3987
4967
  if (quitLine) writeHudRow(workPad + quitLine);
3988
4968
  const statusLine = this.statusLineText(workCols);
3989
4969
  if (statusLine) writeHudRow(workPad + statusLine);
3990
- const frame = FRAMES[Math.floor(Date.now() / SPINNER_MS) % FRAMES.length];
4970
+ const frame = FRAMES[Math.floor(Date.now() / SPINNER_FRAME_MS) % FRAMES.length];
3991
4971
  for (const sub of this.subagents) {
3992
4972
  const color = sub.agentName === COMPACTION_AGENT ? COMPACTION_COLOR : SUBAGENT_COLORS[this.subagentColorByID.get(sub.id) ?? 0];
3993
4973
  const budget = workCols - 10;
@@ -4035,18 +5015,21 @@ var Tui = class _Tui {
4035
5015
  this.bottomDrawn = true;
4036
5016
  const totalRows = hudRows.length;
4037
5017
  const up = Math.max(0, totalRows - 1 - caretRegionRow);
4038
- let out = "\x1B[J" + hudRows.join("\r\n");
5018
+ let out = SYNC_OUTPUT_BEGIN + moveToTop + "\x1B[J" + hudRows.join("\r\n");
4039
5019
  if (totalRows > 0) {
4040
5020
  out += "\r";
4041
5021
  if (up > 0) out += `\x1B[${up}A`;
4042
5022
  if (caretScreenCol > 0) out += `\x1B[${caretScreenCol}C`;
4043
5023
  }
5024
+ out += SYNC_OUTPUT_END;
4044
5025
  process.stdout.write(out);
4045
5026
  }
4046
5027
  clearBottom() {
4047
5028
  if (!this.bottomDrawn) return;
4048
- this.moveToRegionTop();
4049
- 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
+ );
4050
5033
  this.bottomDrawn = false;
4051
5034
  }
4052
5035
  // ── live streaming preview ────────────────────────────────────────────────
@@ -4055,15 +5038,15 @@ var Tui = class _Tui {
4055
5038
  // wipes it right before the finished message is committed to the transcript
4056
5039
  // (which renders full markdown), so there's no double-render.
4057
5040
  static STREAM_TAIL = 20;
4058
- // How long a preview may sit untouched before it's wiped. The model often
4059
- // reasons and then calls a tool without ever emitting an answer, so without
4060
- // this the reasoning tail would linger on screen until the *next* thought (or
4061
- // 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.
4062
5044
  static STREAM_IDLE_MS = 1e4;
4063
- /** Append a fragment of streamed answer text (rendered plain). */
5045
+ /** Append a fragment of streamed answer text (rendered as progressive Markdown). */
4064
5046
  streamResponseDelta(delta, messageId) {
4065
5047
  this.beginStreamMessage(messageId);
4066
- this.streamResponse += delta;
5048
+ this.streamDraft = appendLiveDraft(this.streamDraft, { text: delta, messageId });
5049
+ this.streamResponse = this.streamDraft.text;
4067
5050
  this.scheduleStreamRedraw();
4068
5051
  this.armStreamIdleExpiry();
4069
5052
  }
@@ -4083,8 +5066,7 @@ var Tui = class _Tui {
4083
5066
  beginStreamMessage(messageId) {
4084
5067
  if (messageId !== void 0 && messageId !== this.streamMessageId) {
4085
5068
  this.streamMessageId = messageId;
4086
- this.streamThinking = "";
4087
- this.streamResponse = "";
5069
+ if (!this.streamResponse) this.streamThinking = "";
4088
5070
  }
4089
5071
  }
4090
5072
  /** Wipe the live preview — call right before committing the final message. */
@@ -4098,6 +5080,7 @@ var Tui = class _Tui {
4098
5080
  this.streamIdleTimer = null;
4099
5081
  }
4100
5082
  this.streamMessageId = null;
5083
+ this.streamDraft = emptyLiveDraft();
4101
5084
  if (!this.streamThinking && !this.streamResponse) return;
4102
5085
  this.streamThinking = "";
4103
5086
  this.streamResponse = "";
@@ -4110,17 +5093,16 @@ var Tui = class _Tui {
4110
5093
  this.renderBottom();
4111
5094
  }, 40);
4112
5095
  }
4113
- /** Re-armed on every streamed delta: once the model goes quiet for a beat, the
4114
- * preview is stale, so wipe it instead of letting it sit until the next
4115
- * message. The committed message (if any) still renders in full via
4116
- * 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. */
4117
5098
  armStreamIdleExpiry() {
4118
5099
  if (this.streamIdleTimer) clearTimeout(this.streamIdleTimer);
5100
+ this.streamIdleTimer = null;
5101
+ if (this.streamResponse) return;
4119
5102
  this.streamIdleTimer = setTimeout(() => {
4120
5103
  this.streamIdleTimer = null;
4121
- if (!this.streamThinking && !this.streamResponse) return;
5104
+ if (this.streamResponse || !this.streamThinking) return;
4122
5105
  this.streamThinking = "";
4123
- this.streamResponse = "";
4124
5106
  this.renderBottom();
4125
5107
  }, _Tui.STREAM_IDLE_MS);
4126
5108
  }
@@ -4136,23 +5118,28 @@ var Tui = class _Tui {
4136
5118
  */
4137
5119
  streamPreviewLines(cols2) {
4138
5120
  const thinkStyle = "\x1B[3m\x1B[38;5;240m";
4139
- const clamp2 = (s, wrap, lead) => {
4140
- const max = cols2 - 2;
4141
- const t = s.length > max ? s.slice(0, Math.max(0, max - 1)) + "\u2026" : s;
4142
- return wrap ? `${lead}${wrap}${t}${C.reset}` : `${lead}${t}`;
5121
+ const leakedMarkup = /<\/?[||]DSML[||]|^\s*\[\/?SESSION\]\s*<?\s*$/;
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;
4143
5132
  };
4144
- const leakedMarkup = /<\/?[||]DSML[||]|^\s*\[\/?SESSION\]\s*<?\s*$/;
4145
- const realLines = (text) => text.replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "" && !leakedMarkup.test(l));
4146
5133
  if (this.streamResponse) {
4147
- const all = realLines(this.streamResponse);
5134
+ const all = renderedLines(this.streamResponse);
4148
5135
  const shown = all.slice(-20);
4149
5136
  const firstVisible = all.length <= _Tui.STREAM_TAIL;
4150
5137
  return shown.map(
4151
- (l, i) => clamp2(l, "", i === 0 && firstVisible ? `${C.gray}\u2022${C.reset} ` : " ")
5138
+ (l, i) => `${i === 0 && firstVisible ? `${C.gray}\u2022${C.reset} ` : " "}${l}`
4152
5139
  );
4153
5140
  }
4154
5141
  if (this.streamThinking) {
4155
- 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}`);
4156
5143
  }
4157
5144
  return [];
4158
5145
  }
@@ -4280,6 +5267,7 @@ var Tui = class _Tui {
4280
5267
  }
4281
5268
  // ─── working indicator (turn state) ───────────────────────────────────────
4282
5269
  setWorking(on) {
5270
+ if (on === this.working) return;
4283
5271
  if (on && !this.working) {
4284
5272
  this.working = true;
4285
5273
  this.workingStart = Date.now();
@@ -4295,6 +5283,11 @@ var Tui = class _Tui {
4295
5283
  * stable, distinct colour for as long as it's active; the compaction agent
4296
5284
  * is always orange (its colour never comes from the shared pool). */
4297
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;
4298
5291
  this.subagents = subagents;
4299
5292
  const active = new Set(subagents.map((s) => s.id));
4300
5293
  for (const id of [...this.subagentColorByID.keys()]) {
@@ -4315,7 +5308,7 @@ var Tui = class _Tui {
4315
5308
  syncSpinner() {
4316
5309
  const spinning = this.working || this.subagents.length > 0;
4317
5310
  if (spinning && !this.spinnerTimer) {
4318
- this.spinnerTimer = setInterval(() => this.renderBottom(), SPINNER_MS);
5311
+ this.spinnerTimer = setInterval(() => this.renderBottom(), ANIMATION_TICK_MS);
4319
5312
  } else if (!spinning && this.spinnerTimer) {
4320
5313
  clearInterval(this.spinnerTimer);
4321
5314
  this.spinnerTimer = null;
@@ -4325,15 +5318,18 @@ var Tui = class _Tui {
4325
5318
  return this.working;
4326
5319
  }
4327
5320
  setBackgroundCount(n) {
5321
+ if (n === this.bgCount) return;
4328
5322
  this.bgCount = n;
4329
5323
  if (n === 0) this.bgBadgeSelected = false;
4330
5324
  this.renderBottom();
4331
5325
  }
4332
5326
  setQueuedCount(n) {
5327
+ if (n === this.queuedCount) return;
4333
5328
  this.queuedCount = n;
4334
5329
  this.renderBottom();
4335
5330
  }
4336
5331
  setConnected(connected) {
5332
+ if (connected === this.connected) return;
4337
5333
  this.connected = connected;
4338
5334
  this.renderBottom();
4339
5335
  }
@@ -4342,15 +5338,25 @@ var Tui = class _Tui {
4342
5338
  return this.inputBuffer;
4343
5339
  }
4344
5340
  /** Replace the input (and any pasted images tied to placeholders in it). */
4345
- setInput(text, images = []) {
4346
- this.inputBuffer = text;
4347
- 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;
4348
5344
  this.pendingImages = images;
4349
5345
  this.historyIdx = null;
5346
+ if (!notifyDraft) this.lastDraftSeen = this.inputBuffer;
5347
+ this.renderBottom();
5348
+ }
5349
+ setExternalAttachmentNames(names) {
5350
+ if (names.length === this.externalAttachmentNames.length && names.every((name, i) => name === this.externalAttachmentNames[i])) return;
5351
+ this.externalAttachmentNames = [...names];
4350
5352
  this.renderBottom();
4351
5353
  }
5354
+ hasExternalAttachments() {
5355
+ return this.externalAttachmentNames.length > 0;
5356
+ }
4352
5357
  /** Cumulative token totals shown on the prompt line (`outTokens` includes live). */
4353
5358
  setTokens(inTokens, outTokens) {
5359
+ if (inTokens === this.tokensIn && outTokens === this.tokensOut) return;
4354
5360
  this.tokensIn = inTokens;
4355
5361
  this.tokensOut = outTokens;
4356
5362
  this.renderBottom();
@@ -4362,6 +5368,7 @@ var Tui = class _Tui {
4362
5368
  */
4363
5369
  setStep(label, outTokens) {
4364
5370
  const next = label && label.trim() ? label.replace(/\s+/g, " ").trim() : null;
5371
+ if (next === this.step && outTokens === this.stepOut) return;
4365
5372
  if (next !== this.step) {
4366
5373
  this.step = next;
4367
5374
  this.stepStart = Date.now();
@@ -4787,169 +5794,7 @@ function appendHistory(store, threadId, history, text) {
4787
5794
  void store.kvSet(threadId, HISTORY_KEY, [...history]);
4788
5795
  return history;
4789
5796
  }
4790
-
4791
- // src/markdown.ts
4792
- var ESC = "\x1B[";
4793
- var R = ESC + "0m";
4794
- var BOLD2 = ESC + "1m";
4795
- var DIM4 = ESC + "2m";
4796
- var ITAL = ESC + "3m";
4797
- var UNDER = ESC + "4m";
4798
- var TEAL = ESC + "38;5;37m";
4799
- var CYAN = ESC + "36m";
4800
- var GRAY = ESC + "90m";
4801
- var ANSI = /\x1b\[[0-9;]*m/g;
4802
- function visibleWidth(s) {
4803
- return s.replace(ANSI, "").length;
4804
- }
4805
- function padEndVisible(s, width) {
4806
- const pad = width - visibleWidth(s);
4807
- return pad > 0 ? s + " ".repeat(pad) : s;
4808
- }
4809
- function inline(s) {
4810
- const codes = [];
4811
- s = s.replace(/`([^`]+)`/g, (_, code) => {
4812
- codes.push(code);
4813
- return "\0" + (codes.length - 1) + "\0";
4814
- });
4815
- s = s.replace(
4816
- /\[([^\]]+)\]\(([^)\s]+)\)/g,
4817
- (_, text, url) => `${CYAN}${UNDER}${text}${R} ${DIM4}${url}${R}`
4818
- );
4819
- s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => `${BOLD2}${t}${R}`);
4820
- s = s.replace(/\*([^*\n]+)\*/g, (_, t) => `${ITAL}${t}${R}`);
4821
- s = s.replace(/~~([^~]+)~~/g, (_, t) => `${DIM4}${t}${R}`);
4822
- s = s.replace(/\x00(\d+)\x00/g, (_, i) => `${TEAL}${codes[+i].replace(/ /g, String.fromCharCode(160))}${R}`);
4823
- return s;
4824
- }
4825
- function wrapStyled(text, width) {
4826
- if (width < 4 || visibleWidth(text) <= width) return [text];
4827
- const words = text.split(" ");
4828
- const lines = [];
4829
- let cur = "";
4830
- let curLen = 0;
4831
- for (const w of words) {
4832
- const wLen = visibleWidth(w);
4833
- if (cur === "") {
4834
- cur = w;
4835
- curLen = wLen;
4836
- } else if (curLen + 1 + wLen <= width) {
4837
- cur += " " + w;
4838
- curLen += 1 + wLen;
4839
- } else {
4840
- lines.push(cur);
4841
- cur = w;
4842
- curLen = wLen;
4843
- }
4844
- }
4845
- if (cur !== "" || lines.length === 0) lines.push(cur);
4846
- return lines;
4847
- }
4848
- function wrapBlock(out, cols2, leadFirst, leadRest, leadWidth, text) {
4849
- const wrapped = wrapStyled(text, Math.max(8, cols2 - leadWidth));
4850
- wrapped.forEach((ln, idx) => out.push((idx === 0 ? leadFirst : leadRest) + ln));
4851
- }
4852
- function tableCells(row) {
4853
- let r = row.trim();
4854
- if (r.startsWith("|")) r = r.slice(1);
4855
- if (r.endsWith("|")) r = r.slice(0, -1);
4856
- return r.split("|").map((c4) => c4.trim());
4857
- }
4858
- var SEPARATOR = /^[\s|:-]+$/;
4859
- function isTableSeparator(line) {
4860
- return SEPARATOR.test(line) && line.includes("-") && line.includes("|");
4861
- }
4862
- function renderTable(rows) {
4863
- const cols2 = Math.max(...rows.map((r) => r.length));
4864
- const widths = [];
4865
- for (let c4 = 0; c4 < cols2; c4++) {
4866
- widths[c4] = Math.max(...rows.map((r) => visibleWidth(inline(r[c4] ?? ""))));
4867
- }
4868
- const sep = `${GRAY} \u2502 ${R}`;
4869
- const out = [];
4870
- rows.forEach((r, ri) => {
4871
- const cells = [];
4872
- for (let c4 = 0; c4 < cols2; c4++) {
4873
- const raw = r[c4] ?? "";
4874
- const styled = ri === 0 ? `${BOLD2}${inline(raw)}${R}` : inline(raw);
4875
- cells.push(padEndVisible(styled, widths[c4]));
4876
- }
4877
- out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
4878
- if (ri === 0) {
4879
- const rule = widths.map((w) => `${GRAY}${"\u2500".repeat(w)}${R}`).join(`${GRAY}\u2500\u253C\u2500${R}`);
4880
- out.push(" " + rule);
4881
- }
4882
- });
4883
- return out;
4884
- }
4885
- function renderMarkdown(src, cols2 = 80) {
4886
- const lines = src.replace(/\r\n/g, "\n").split("\n");
4887
- const out = [];
4888
- let inFence = false;
4889
- let i = 0;
4890
- while (i < lines.length) {
4891
- const line = lines[i];
4892
- if (/^\s*```/.test(line)) {
4893
- inFence = !inFence;
4894
- i++;
4895
- continue;
4896
- }
4897
- if (inFence) {
4898
- out.push(`${GRAY}\u2502${R} ${line}`);
4899
- i++;
4900
- continue;
4901
- }
4902
- if (line.includes("|") && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
4903
- const block = [tableCells(line)];
4904
- i += 2;
4905
- while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
4906
- block.push(tableCells(lines[i]));
4907
- i++;
4908
- }
4909
- out.push(...renderTable(block));
4910
- continue;
4911
- }
4912
- const heading = line.match(/^(#{1,6})\s+(.*)$/);
4913
- if (heading) {
4914
- for (const ln of wrapStyled(heading[2].trim(), cols2)) out.push(`${BOLD2}${TEAL}${ln}${R}`);
4915
- i++;
4916
- continue;
4917
- }
4918
- if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
4919
- out.push(`${GRAY}\u2500\u2500\u2500\u2500\u2500\u2500${R}`);
4920
- i++;
4921
- continue;
4922
- }
4923
- const quote = line.match(/^\s*>\s?(.*)$/);
4924
- if (quote) {
4925
- for (const ln of wrapStyled(inline(quote[1]), Math.max(8, cols2 - 2))) {
4926
- out.push(`${GRAY}\u2502${R} ${DIM4}${ln}${R}`);
4927
- }
4928
- i++;
4929
- continue;
4930
- }
4931
- const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
4932
- if (bullet) {
4933
- const leadWidth = bullet[1].length + 2;
4934
- wrapBlock(out, cols2, `${bullet[1]}${TEAL}\u2022${R} `, " ".repeat(leadWidth), leadWidth, inline(bullet[2]));
4935
- i++;
4936
- continue;
4937
- }
4938
- const numbered = line.match(/^(\s*)(\d+)([.)])\s+(.*)$/);
4939
- if (numbered) {
4940
- const marker = `${numbered[2]}${numbered[3]}`;
4941
- const leadWidth = numbered[1].length + marker.length + 1;
4942
- wrapBlock(out, cols2, `${numbered[1]}${BOLD2}${marker}${R} `, " ".repeat(leadWidth), leadWidth, inline(numbered[4]));
4943
- i++;
4944
- continue;
4945
- }
4946
- if (line.trim()) wrapBlock(out, cols2, "", "", 0, inline(line));
4947
- else out.push("");
4948
- i++;
4949
- }
4950
- return out;
4951
- }
4952
- var DIR = path3.join(os10.homedir(), ".standardagents");
5797
+ var DIR = path3.join(os7.homedir(), ".standardagents");
4953
5798
  var FILE = path3.join(DIR, "credentials");
4954
5799
  function normalizeEndpoint(endpoint) {
4955
5800
  let e = endpoint.trim();
@@ -5083,7 +5928,7 @@ function relaxTlsForLocalEndpoint(endpoint) {
5083
5928
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
5084
5929
  return true;
5085
5930
  }
5086
- var dir = () => path3.join(os10.homedir(), ".standardagents");
5931
+ var dir = () => path3.join(os7.homedir(), ".standardagents");
5087
5932
  var file = () => path3.join(dir(), "machine.json");
5088
5933
  function loadMachineIdentity() {
5089
5934
  try {
@@ -5153,20 +5998,55 @@ function parseFsRequest(value) {
5153
5998
  requested_at: typeof r.requested_at === "number" ? r.requested_at : Date.now()
5154
5999
  };
5155
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
+ }
5156
6017
  async function readFsRequest(api, machineId) {
5157
6018
  return parseFsRequest(await api.userKvGet(fsRequestKey(machineId)));
5158
6019
  }
5159
6020
  async function writeFsResponse(api, machineId, res) {
5160
6021
  await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
5161
6022
  }
5162
- function machineIcon(record) {
5163
- if (record.icon && record.icon.trim()) return record.icon.trim();
5164
- if (record.platform === "darwin") return "\u{1F4BB}";
5165
- if (record.platform === "win32") return "\u{1F5A5}\uFE0F";
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";
5166
6046
  return "\u{1F5B3}";
5167
6047
  }
5168
- function machineDisplayName(record) {
5169
- return record.name?.trim() || "" || (record.hostname || "") || (record.id || "") || "machine";
6048
+ function machineDisplayName(record2) {
6049
+ return record2.name?.trim() || "" || (record2.hostname || "") || (record2.id || "") || "machine";
5170
6050
  }
5171
6051
  async function getMachineName(api, machineId) {
5172
6052
  const v = await api.userKvGet(nameKey(machineId));
@@ -5241,15 +6121,15 @@ async function loadMachine(api, machineId) {
5241
6121
  if (icon) rec.icon = icon;
5242
6122
  return rec;
5243
6123
  }
5244
- function daemonOnline(record, now = Date.now()) {
5245
- 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;
5246
6126
  }
5247
6127
  function newRecord(identity) {
5248
6128
  const now = Date.now();
5249
6129
  return {
5250
6130
  id: identity.machine_id,
5251
- name: os10.hostname(),
5252
- hostname: os10.hostname(),
6131
+ name: os7.hostname(),
6132
+ hostname: os7.hostname(),
5253
6133
  platform: process.platform,
5254
6134
  arch: process.arch,
5255
6135
  version: readVersion() || void 0,
@@ -5261,15 +6141,15 @@ function newRecord(identity) {
5261
6141
  }
5262
6142
  async function updateOwnMachineRecord(api, identity, mutate) {
5263
6143
  const existing = await loadRawMachine(api, identity.machine_id);
5264
- const record = existing ?? newRecord(identity);
5265
- record.hostname = os10.hostname();
5266
- record.platform = process.platform;
5267
- record.arch = process.arch;
5268
- record.version = readVersion() || record.version;
5269
- mutate?.(record);
5270
- record.updated_at = Date.now();
5271
- await api.userKvSet(machineKey(identity.machine_id), record);
5272
- 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;
5273
6153
  }
5274
6154
  function projectRepository(projectDir) {
5275
6155
  try {
@@ -5283,35 +6163,54 @@ function projectRepository(projectDir) {
5283
6163
  return null;
5284
6164
  }
5285
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
+ }
5286
6173
  async function registerProject(api, identity, projectDir) {
5287
- const repository = projectRepository(projectDir);
5288
- await updateOwnMachineRecord(api, identity, (record) => {
5289
- record.projects[projectDir] = {
5290
- 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,
5291
6185
  last_used_at: Date.now(),
5292
6186
  repository
5293
6187
  };
5294
6188
  });
5295
6189
  }
5296
6190
  async function unregisterProject(api, identity, projectDir) {
5297
- await updateOwnMachineRecord(api, identity, (record) => {
5298
- 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
+ }
5299
6198
  });
5300
6199
  }
5301
6200
  async function touchDaemon(api, identity, version) {
5302
- await updateOwnMachineRecord(api, identity, (record) => {
6201
+ await updateOwnMachineRecord(api, identity, (record2) => {
5303
6202
  const now = Date.now();
5304
- record.daemon = {
6203
+ record2.daemon = {
5305
6204
  version,
5306
- installed_at: record.daemon?.installed_at ?? now,
6205
+ installed_at: record2.daemon?.installed_at ?? now,
5307
6206
  last_seen_at: now,
5308
6207
  pid: process.pid
5309
6208
  };
5310
6209
  });
5311
6210
  }
5312
6211
  async function clearDaemon(api, identity) {
5313
- await updateOwnMachineRecord(api, identity, (record) => {
5314
- record.daemon = null;
6212
+ await updateOwnMachineRecord(api, identity, (record2) => {
6213
+ record2.daemon = null;
5315
6214
  });
5316
6215
  }
5317
6216
  function parseCommands(value) {
@@ -5347,16 +6246,16 @@ async function clearMachineCommands(api, machineId, appliedIds) {
5347
6246
  async function applyMachineCommand(api, identity, cmd) {
5348
6247
  switch (cmd.kind) {
5349
6248
  case "add_project": {
5350
- const path13 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
5351
- if (!path13) return "add_project: ignored (no path)";
5352
- await registerProject(api, identity, path13);
5353
- 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}`;
5354
6253
  }
5355
6254
  case "remove_project": {
5356
- const path13 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
5357
- if (!path13) return "remove_project: ignored (no path)";
5358
- await unregisterProject(api, identity, path13);
5359
- 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}`;
5360
6259
  }
5361
6260
  case "update":
5362
6261
  return "update requested";
@@ -5433,7 +6332,7 @@ var PROJECT_MARKERS = [
5433
6332
  ];
5434
6333
  var MAX_ENTRIES2 = 500;
5435
6334
  function resolveBrowsePath(input3) {
5436
- const home = os10.homedir();
6335
+ const home = os7.homedir();
5437
6336
  let p = (input3 ?? "").trim();
5438
6337
  if (!p) return home;
5439
6338
  if (p === "~") return home;
@@ -5458,7 +6357,7 @@ function markers(dirPath) {
5458
6357
  return { project, repo };
5459
6358
  }
5460
6359
  function browseDirectory(input3, opts = {}) {
5461
- const home = os10.homedir();
6360
+ const home = os7.homedir();
5462
6361
  const abs = resolveBrowsePath(input3);
5463
6362
  const parent = path3.dirname(abs);
5464
6363
  const base = {
@@ -5712,7 +6611,7 @@ var UPDATE_CHECK_MS = 6 * 60 * 6e4;
5712
6611
  var SWEEP_MS = 10 * 6e4;
5713
6612
  var MAX_WORKERS = 30;
5714
6613
  var LOG_MAX_BYTES = 1e6;
5715
- var LOG_FILE = path3.join(os10.homedir(), ".standardagents", "daemon.log");
6614
+ var LOG_FILE = path3.join(os7.homedir(), ".standardagents", "daemon.log");
5716
6615
  function daemonLog(line) {
5717
6616
  try {
5718
6617
  fs4.mkdirSync(path3.dirname(LOG_FILE), { recursive: true });
@@ -5731,13 +6630,13 @@ function pathFromTags(tags) {
5731
6630
  const tag = tags.find((t) => t.startsWith("path:"));
5732
6631
  if (!tag) return null;
5733
6632
  const raw = tag.slice("path:".length);
5734
- return raw.replace(/^~(?=\/|$)/, os10.homedir());
6633
+ return raw.replace(/^~(?=\/|$)/, os7.homedir());
5735
6634
  }
5736
6635
  var ThreadWorker = class {
5737
- constructor(api, identity, machineName, threadId, projectDir, createdAt) {
6636
+ constructor(api, identity, machineName, threadId, projectDir, createdAt2) {
5738
6637
  this.threadId = threadId;
5739
6638
  this.projectDir = projectDir;
5740
- this.createdAt = createdAt;
6639
+ this.createdAt = createdAt2;
5741
6640
  this.perm = { level: 1, alwaysAllow: /* @__PURE__ */ new Set(), allowRisk: /* @__PURE__ */ new Set() };
5742
6641
  this.session = new ExecutionSession({
5743
6642
  api,
@@ -5863,7 +6762,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5863
6762
  process.exit(1);
5864
6763
  }
5865
6764
  let displayName = machineDisplayName(await loadMachine(api, identity.machine_id).catch(() => null) ?? {
5866
- hostname: os10.hostname(),
6765
+ hostname: os7.hostname(),
5867
6766
  id: identity.machine_id
5868
6767
  });
5869
6768
  const applied = consumeAppliedUpdate(version);
@@ -5877,8 +6776,19 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5877
6776
  process.on("unhandledRejection", (err) => {
5878
6777
  daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
5879
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);
5880
6790
  const workers = /* @__PURE__ */ new Map();
5881
- const attach = async (threadId, tags, createdAt = 0) => {
6791
+ const attach = async (threadId, tags, createdAt2 = 0) => {
5882
6792
  if (workers.has(threadId)) return;
5883
6793
  const projectDir = pathFromTags(tags);
5884
6794
  if (!projectDir) return;
@@ -5895,7 +6805,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5895
6805
  daemonLog(`cannot prepare project dir ${projectDir}: ${e instanceof Error ? e.message : String(e)}`);
5896
6806
  return;
5897
6807
  }
5898
- 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());
5899
6809
  worker.onEvicted = (id) => detach(id);
5900
6810
  workers.set(threadId, worker);
5901
6811
  daemonLog(`attached ${threadId.slice(0, 8)} \u2192 ${projectDir}`);
@@ -5934,17 +6844,6 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5934
6844
  });
5935
6845
  events.connect();
5936
6846
  await sweep();
5937
- await touchDaemon(api, identity, version).catch(
5938
- (e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
5939
- );
5940
- const heartbeat = setInterval(() => {
5941
- void touchDaemon(api, identity, version).catch(() => {
5942
- });
5943
- void getMachineName(api, identity.machine_id).then((n) => {
5944
- if (n) displayName = n;
5945
- }).catch(() => {
5946
- });
5947
- }, HEARTBEAT_MS);
5948
6847
  const reclaim = setInterval(() => {
5949
6848
  for (const worker of workers.values()) {
5950
6849
  if (!worker.isOwner) {
@@ -6121,8 +7020,8 @@ function run2(cmd, args) {
6121
7020
  const output4 = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
6122
7021
  return { ok: res.status === 0, output: output4 };
6123
7022
  }
6124
- var plistPath = () => path3.join(os10.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
6125
- 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);
6126
7025
  var xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6127
7026
  function installService(command, endpoint) {
6128
7027
  if (process.platform === "darwin") return installLaunchd(command, endpoint);
@@ -6134,7 +7033,7 @@ function installService(command, endpoint) {
6134
7033
  };
6135
7034
  }
6136
7035
  function installLaunchd(command, endpoint) {
6137
- const logDir = path3.join(os10.homedir(), ".standardagents");
7036
+ const logDir = path3.join(os7.homedir(), ".standardagents");
6138
7037
  fs4.mkdirSync(logDir, { recursive: true });
6139
7038
  fs4.mkdirSync(path3.dirname(plistPath()), { recursive: true });
6140
7039
  const envEntries = [
@@ -6207,10 +7106,10 @@ WantedBy=default.target
6207
7106
  if (!enable.ok) {
6208
7107
  return { ok: false, detail: `systemctl enable failed: ${enable.output}` };
6209
7108
  }
6210
- const linger = run2("loginctl", ["enable-linger", os10.userInfo().username]);
7109
+ const linger = run2("loginctl", ["enable-linger", os7.userInfo().username]);
6211
7110
  return {
6212
7111
  ok: true,
6213
- 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}`)
6214
7113
  };
6215
7114
  }
6216
7115
  function uninstallService() {
@@ -6338,7 +7237,7 @@ async function installCommand(endpointFlag) {
6338
7237
  const api = await ensureSignedIn(endpoint);
6339
7238
  const identity = loadMachineIdentity();
6340
7239
  const existing = await loadMachine(api, identity.machine_id).catch(() => null);
6341
- const suggested = machineDisplayName(existing ?? { hostname: os10.hostname(), id: identity.machine_id });
7240
+ const suggested = machineDisplayName(existing ?? { hostname: os7.hostname(), id: identity.machine_id });
6342
7241
  const rl = readline2.createInterface({ input: stdin, output: stdout });
6343
7242
  const answer = (await rl.question(
6344
7243
  `${c2.bold}Machine name${c2.reset} ${c2.dim}(shown in the session picker)${c2.reset} [${suggested}]: `
@@ -6362,12 +7261,12 @@ async function installCommand(endpointFlag) {
6362
7261
  `);
6363
7262
  stdout.write(`${c2.dim}Waiting for the daemon's first heartbeat\u2026${c2.reset}
6364
7263
  `);
6365
- const deadline = Date.now() + 3e4;
7264
+ const deadline = Date.now() + 6e4;
6366
7265
  let alive = false;
6367
7266
  while (Date.now() < deadline) {
6368
7267
  await new Promise((r) => setTimeout(r, 2e3));
6369
- const record = await loadMachine(api, identity.machine_id);
6370
- if (record && daemonOnline(record)) {
7268
+ const record2 = await loadMachine(api, identity.machine_id);
7269
+ if (record2 && daemonOnline(record2)) {
6371
7270
  alive = true;
6372
7271
  break;
6373
7272
  }
@@ -6398,7 +7297,7 @@ async function statusCommand() {
6398
7297
  const cred = getCredential(endpoint);
6399
7298
  if (!cred) {
6400
7299
  stdout.write(
6401
- `${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}
6402
7301
  `
6403
7302
  );
6404
7303
  stdout.write(`${c2.bold}Account:${c2.reset} ${c2.yellow}not signed in to ${endpoint}${c2.reset}
@@ -6407,21 +7306,21 @@ async function statusCommand() {
6407
7306
  }
6408
7307
  relaxTlsForLocalEndpoint(endpoint);
6409
7308
  const api = new ApiClient(endpoint, cred.access_token);
6410
- const record = await loadMachine(api, identity.machine_id).catch(() => null);
7309
+ const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
6411
7310
  stdout.write(
6412
- `${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}
6413
7312
  `
6414
7313
  );
6415
- if (!record) {
7314
+ if (!record2) {
6416
7315
  stdout.write(`${c2.bold}Registry:${c2.reset} not registered yet
6417
7316
  `);
6418
7317
  return;
6419
7318
  }
6420
- const online = daemonOnline(record);
6421
- 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";
6422
7321
  stdout.write(`${c2.bold}Registry:${c2.reset} ${online ? `${c2.green}online${c2.reset}` : `${c2.yellow}offline${c2.reset}`} \xB7 last heartbeat ${seen}
6423
7322
  `);
6424
- const projects = Object.keys(record.projects);
7323
+ const projects = Object.keys(record2.projects);
6425
7324
  stdout.write(`${c2.bold}Projects:${c2.reset} ${projects.length ? "" : c2.dim + "none registered" + c2.reset}
6426
7325
  `);
6427
7326
  for (const p of projects.sort()) stdout.write(` ${c2.dim}${p}${c2.reset}
@@ -6442,8 +7341,8 @@ async function projectCommand(action, target) {
6442
7341
  const endpoint = resolveEndpoint();
6443
7342
  const api = await ensureSignedIn(endpoint);
6444
7343
  const identity = loadMachineIdentity();
6445
- const record = await loadMachine(api, identity.machine_id).catch(() => null);
6446
- 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 });
6447
7346
  if (action === "add") {
6448
7347
  await registerProject(api, identity, dir2);
6449
7348
  stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir2} for remote sessions on ${displayName}.
@@ -6601,7 +7500,7 @@ function printAssistant(tui, text) {
6601
7500
  tui.clearStream();
6602
7501
  tui.print("");
6603
7502
  let dotted = false;
6604
- for (const line of renderMarkdown(text, cols2)) {
7503
+ for (const line of renderStreamingMarkdown(text, cols2)) {
6605
7504
  if (!dotted && line.trim()) {
6606
7505
  tui.print(`${c3.gray}\u2022${c3.reset} ${line}`);
6607
7506
  dotted = true;
@@ -6643,18 +7542,21 @@ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.rese
6643
7542
  `);
6644
7543
  }
6645
7544
  function printWelcome(endpoint, projectDir) {
6646
- const home = os10.homedir();
7545
+ const home = os7.homedir();
6647
7546
  const dir2 = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
6648
7547
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
6649
7548
  const version = readVersion();
6650
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);
6651
7554
  const meta = [
6652
7555
  `${c3.bold}${gradientText("Standard Code")}${c3.reset}${version ? ` ${c3.dim}v${version}${c3.reset}` : ""}`,
6653
7556
  `${c3.dim}terminal coding agent${c3.reset}`,
6654
7557
  ...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c3.teal}${host}${c3.reset}`],
6655
- `${c3.dim}${dir2}${c3.reset}`
7558
+ `${c3.dim}${displayDir}${c3.reset}`
6656
7559
  ];
6657
- const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
6658
7560
  const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
6659
7561
  stdout.write("\n");
6660
7562
  for (let i = 0; i < LOGO_MARK.length; i++) {
@@ -6711,7 +7613,7 @@ async function main() {
6711
7613
  const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
6712
7614
  const dirArg = cliArgs.dir;
6713
7615
  const projectDir = path3.resolve(dirArg || process.cwd());
6714
- const machine = os10.hostname();
7616
+ const machine = os7.hostname();
6715
7617
  const reader = { rl: null };
6716
7618
  let handoffClosing = false;
6717
7619
  let preflightArmed = false;
@@ -6896,7 +7798,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6896
7798
  void registerProject(api, identity, projectDir).catch(() => {
6897
7799
  });
6898
7800
  const tui = new Tui(1);
6899
- const home = os10.homedir();
7801
+ const home = os7.homedir();
6900
7802
  const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
6901
7803
  const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
6902
7804
  const session = { mode: "local", identity };
@@ -6919,7 +7821,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6919
7821
  ]
6920
7822
  );
6921
7823
  if (where) {
6922
- const remotePath = await pickRemoteProject(tui, where);
7824
+ const remotePath = await pickRemoteProject(tui, api, where);
6923
7825
  if (remotePath) {
6924
7826
  session.mode = "remote";
6925
7827
  session.runner = where;
@@ -6955,6 +7857,10 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6955
7857
  if (session.mode === "remote" && session.runner && session.remotePath) {
6956
7858
  await api.kvSet(id, "session_info", { cwd: session.remotePath, machine: session.runner.name }).catch(() => {
6957
7859
  });
7860
+ void enqueueMachineCommand(api, session.runner.id, "add_project", {
7861
+ path: session.remotePath
7862
+ }).catch(() => {
7863
+ });
6958
7864
  }
6959
7865
  return id;
6960
7866
  };
@@ -6994,7 +7900,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6994
7900
  function shortenPath(p, max = 38) {
6995
7901
  return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
6996
7902
  }
6997
- async function pickRemoteProject(tui, runner) {
7903
+ async function pickRemoteProject(tui, api, runner) {
6998
7904
  const ENTER_PATH = "__enter_path__";
6999
7905
  const projects = Object.entries(runner.projects).sort(
7000
7906
  (a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
@@ -7021,7 +7927,7 @@ async function pickRemoteProject(tui, runner) {
7021
7927
  tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
7022
7928
  return null;
7023
7929
  }
7024
- return trimmed;
7930
+ return await resolveRemoteProjectPath(api, runner.id, trimmed);
7025
7931
  }
7026
7932
  function isSilentMessage(m) {
7027
7933
  return m?.silent === true || m?.metadata?.silent === true;
@@ -7056,31 +7962,6 @@ function relativeTime(unixSeconds) {
7056
7962
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
7057
7963
  return `${Math.floor(diff / 86400)}d ago`;
7058
7964
  }
7059
- function hasToolCalls(m) {
7060
- const tc = m?.tool_calls;
7061
- if (Array.isArray(tc)) return tc.length > 0;
7062
- if (typeof tc === "string") {
7063
- const s = tc.trim();
7064
- return s.length > 0 && s !== "null" && s !== "[]";
7065
- }
7066
- return false;
7067
- }
7068
- function messageText(content) {
7069
- if (typeof content === "string") return content;
7070
- if (Array.isArray(content)) {
7071
- return content.map((b) => typeof b === "string" ? b : typeof b?.text === "string" ? b.text : "").join("");
7072
- }
7073
- return "";
7074
- }
7075
- function threadBusy(msgs) {
7076
- if (!msgs.length) return false;
7077
- if (msgs.some((m) => m.status === "pending")) return true;
7078
- const last = [...msgs].sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))[0];
7079
- if (!last) return false;
7080
- if (last.role === "user" || last.role === "tool") return true;
7081
- if (last.role === "assistant") return hasToolCalls(last);
7082
- return false;
7083
- }
7084
7965
  async function printHistory(api, threadId, tui) {
7085
7966
  let msgs;
7086
7967
  try {
@@ -7139,9 +8020,19 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
7139
8020
  saveApprovals(api, threadId, perm);
7140
8021
  const attaching = startLoader("Attaching to thread");
7141
8022
  let busy = false;
7142
- let interrupting = false;
7143
- const queued = [];
7144
- 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
+ };
7145
8036
  const shownIds = /* @__PURE__ */ new Set();
7146
8037
  const pendingSent = /* @__PURE__ */ new Map();
7147
8038
  let lastSent = null;
@@ -7239,6 +8130,10 @@ why: ${req.requestPermission}` : ""}`,
7239
8130
  bridge = exec.bridge;
7240
8131
  }
7241
8132
  const stream = new MessageStream(api, threadId, {
8133
+ onOpen: () => {
8134
+ void reconcileSharedMessaging(true);
8135
+ void refreshSessionProjection();
8136
+ },
7242
8137
  // Live streaming preview: answer text and (opt-in) internal reasoning feed
7243
8138
  // the TUI's ephemeral preview; the committed message still renders from
7244
8139
  // polling, which calls tui.clearStream() first so there's no double-render.
@@ -7246,6 +8141,12 @@ why: ${req.requestPermission}` : ""}`,
7246
8141
  onReasoningChunk: (text, mid) => tui.streamThinkingDelta(text, mid),
7247
8142
  onAssistantText: () => {
7248
8143
  },
8144
+ onMessage: (message) => {
8145
+ if (message?.role === "user" || message?.status === "pending") {
8146
+ busy = true;
8147
+ tui.setWorking(true);
8148
+ }
8149
+ },
7249
8150
  onEvent: (eventType, data) => {
7250
8151
  if (eventType === "generation" && typeof data?.outputTokens === "number") {
7251
8152
  liveOut = data.outputTokens;
@@ -7258,6 +8159,8 @@ why: ${req.requestPermission}` : ""}`,
7258
8159
  refreshStatus();
7259
8160
  } else if (eventType === "goal_updated" && data) {
7260
8161
  tui.setGoal(data);
8162
+ } else if (eventType === SHARED_MESSAGING_EVENT) {
8163
+ void reconcileSharedMessaging(true);
7261
8164
  }
7262
8165
  },
7263
8166
  // A failed turn whose message is the lease service's at-limit denial → offer
@@ -7334,7 +8237,7 @@ why: ${req.requestPermission}` : ""}`,
7334
8237
  const sessionEnded = new Promise((r) => endSession = r);
7335
8238
  const quit = async () => {
7336
8239
  tui.end();
7337
- const stopped2 = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
8240
+ const stopped2 = bridge?.isOwner ?? false ? api.requestSharedStop(threadId, messagingOrigin).catch(() => {
7338
8241
  }) : Promise.resolve();
7339
8242
  const procsStopped2 = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
7340
8243
  bridge?.close();
@@ -7409,23 +8312,89 @@ why: ${req.requestPermission}` : ""}`,
7409
8312
  };
7410
8313
  const extFor = (mime) => ({ "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" })[mime] ?? "bin";
7411
8314
  const toAttachments = (images) => images.map((img) => ({ name: `image-${img.seq}.${extFor(img.mime)}`, mimeType: img.mime, data: img.data }));
7412
- const sendNow = async (text, images = []) => {
7413
- lastSent = { text, images };
7414
- 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)`);
7415
8384
  const key = text.trim();
7416
8385
  pendingSent.set(key, (pendingSent.get(key) ?? 0) + 1);
7417
8386
  try {
7418
- await api.sendMessage(threadId, text, toAttachments(images));
8387
+ await api.sendMessage(threadId, text, [...refs, ...toAttachments(images)]);
7419
8388
  } catch (e) {
7420
8389
  const n = (pendingSent.get(key) ?? 1) - 1;
7421
8390
  if (n > 0) pendingSent.set(key, n);
7422
8391
  else pendingSent.delete(key);
7423
8392
  tui.print(`${c3.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
7424
- return;
8393
+ return false;
7425
8394
  }
7426
- interrupting = false;
7427
8395
  busy = true;
7428
8396
  tui.setWorking(true);
8397
+ return true;
7429
8398
  };
7430
8399
  const whereLabel = remote ? runnerName : "this machine";
7431
8400
  let bangRunning = false;
@@ -7446,11 +8415,34 @@ why: ${req.requestPermission}` : ""}`,
7446
8415
  bangRunning = false;
7447
8416
  }
7448
8417
  };
7449
- const flushQueued = async () => {
7450
- if (!queued.length) return;
7451
- const toSend = queued.splice(0);
7452
- tui.setQueuedCount(0);
7453
- 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
+ }
7454
8446
  };
7455
8447
  const requestCompaction = async () => {
7456
8448
  try {
@@ -7554,7 +8546,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7554
8546
  tui.print(`${c3.green}\u2713${c3.reset} ${c3.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c3.reset}`);
7555
8547
  if (opts.auto && lastSent) {
7556
8548
  tui.print(`${c3.gray}Continuing\u2026${c3.reset}`);
7557
- await sendNow(lastSent.text, lastSent.images);
8549
+ await sendNow(lastSent.text, lastSent.images, lastSent.refs);
7558
8550
  }
7559
8551
  } finally {
7560
8552
  upgradeInFlight = false;
@@ -7587,6 +8579,15 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7587
8579
  hint: () => tui.contextPctLabel() || "free up context",
7588
8580
  run: requestCompaction
7589
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
+ },
7590
8591
  { name: "level", label: "Auto-accept level", hint: () => `level ${tui.level}`, run: () => runLevelMenu(tui, perm) },
7591
8592
  {
7592
8593
  name: "permissions",
@@ -7648,26 +8649,46 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7648
8649
  ]);
7649
8650
  const history = await loadHistory(api, threadId, historySeedThreadId);
7650
8651
  tui.setHistory(history);
7651
- const PRESENCE_KEY = "presence.latest_draft";
7652
- const presenceSurfaceId = `cli:${Math.random().toString(36).slice(2, 10)}`;
7653
- let presenceTimer;
7654
- const clearPresence = () => {
7655
- if (presenceTimer) clearTimeout(presenceTimer);
7656
- void api.kvSet(threadId, PRESENCE_KEY, { text: "", images: [], surface: "cli", surface_id: presenceSurfaceId, updated_at: Date.now() }).catch(() => {
7657
- });
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));
7658
8660
  };
7659
- tui.onDraftChange = (textVal) => {
7660
- if (presenceTimer) clearTimeout(presenceTimer);
7661
- presenceTimer = setTimeout(() => {
7662
- const t = textVal.trim();
7663
- void api.kvSet(threadId, PRESENCE_KEY, { text: t ? textVal : "", images: [], surface: "cli", surface_id: presenceSurfaceId, updated_at: Date.now() }).catch(() => {
7664
- });
7665
- }, 400);
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);
7666
8680
  };
7667
- tui.onSubmit = (text, images) => {
7668
- clearPresence();
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();
7669
8690
  const trimmed = text.trimStart();
7670
- if (trimmed.startsWith("!") && !trimmed.startsWith("!!")) {
8691
+ if (trimmed.startsWith("!") && !trimmed.startsWith("!!") && images.length === 0 && draftRefs.length === 0) {
7671
8692
  const command = trimmed.slice(1).trim();
7672
8693
  if (command) {
7673
8694
  appendHistory(api, threadId, history, text);
@@ -7676,38 +8697,71 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7676
8697
  return;
7677
8698
  }
7678
8699
  const outgoing = trimmed.startsWith("!!") ? text.replace("!!", "!") : text;
7679
- appendHistory(api, threadId, history, outgoing);
8700
+ if (outgoing.trim()) appendHistory(api, threadId, history, outgoing);
7680
8701
  text = outgoing;
7681
- if (editingQueued) {
7682
- editingQueued = false;
7683
- queued.push({ text, images });
7684
- tui.setQueuedCount(queued.length);
7685
- 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
+ }
7686
8726
  return;
7687
8727
  }
7688
8728
  if (busy) {
7689
- queued.push({ text, images });
7690
- tui.setQueuedCount(queued.length);
7691
- 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
+ }
7692
8736
  } else {
7693
- 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
+ }
7694
8742
  }
7695
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
+ };
7696
8750
  tui.onInterrupt = () => {
7697
- if (queued.length > 0) {
7698
- tui.print(`${c3.yellow}\u21AA steering \u2014 stopping current work and sending your message\u2026${c3.reset}`);
7699
- void api.stop(threadId).catch(() => {
7700
- }).then(() => flushQueued());
7701
- } else if (busy) {
7702
- interrupting = true;
7703
- busy = false;
7704
- activeSteps.clear();
7705
- liveOut = 0;
7706
- tui.setWorking(false);
7707
- refreshStatus();
7708
- tui.print(`${c3.yellow}[interrupted by user]${c3.reset}`);
7709
- void api.stop(threadId).catch(() => {
7710
- });
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));
7711
8765
  }
7712
8766
  };
7713
8767
  tui.onBgBadge = () => {
@@ -7715,11 +8769,13 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7715
8769
  });
7716
8770
  };
7717
8771
  tui.onUpArrow = () => {
7718
- if (tui.getInput().trim() || queued.length === 0) return false;
7719
- const q = queued.pop();
7720
- tui.setQueuedCount(queued.length);
7721
- editingQueued = true;
7722
- 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);
7723
8779
  return true;
7724
8780
  };
7725
8781
  events.connect();
@@ -7795,16 +8851,26 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7795
8851
  };
7796
8852
  const poll = async () => {
7797
8853
  let msgs;
8854
+ let serverBusy = null;
8855
+ let serverTool = null;
7798
8856
  try {
7799
- 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;
7800
8861
  } catch {
7801
- return;
8862
+ try {
8863
+ msgs = await api.getMessages(threadId, 60);
8864
+ } catch {
8865
+ return;
8866
+ }
7802
8867
  }
7803
8868
  const sorted = [...msgs].sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
7804
8869
  for (const m of sorted) {
7805
- if (shownIds.has(m.id) || m.status === "pending") continue;
7806
- shownIds.add(m.id);
8870
+ if (shownIds.has(m.id)) continue;
7807
8871
  const text = messageText(m.content).trim();
8872
+ if (!transcriptMessageReady(m, text)) continue;
8873
+ shownIds.add(m.id);
7808
8874
  const denial = typeof m.error === "string" && m.error || text;
7809
8875
  if (denial && isSessionLimitError(denial)) {
7810
8876
  void offerUpgrade({ auto: true });
@@ -7831,19 +8897,20 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7831
8897
  }
7832
8898
  }
7833
8899
  }
7834
- const polledBusy = threadBusy(msgs);
7835
- if (interrupting) {
7836
- if (!polledBusy) interrupting = false;
7837
- busy = false;
7838
- } else {
7839
- 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();
7840
8907
  }
8908
+ busy = polledBusy;
7841
8909
  tui.setWorking(busy);
7842
8910
  if (!busy) {
7843
8911
  if (activeSteps.size) activeSteps.clear();
7844
8912
  liveOut = 0;
7845
8913
  refreshStatus();
7846
- if (queued.length > 0 && !editingQueued) await flushQueued();
7847
8914
  }
7848
8915
  refreshBgCount();
7849
8916
  void relayApprovals().catch(() => {
@@ -7879,12 +8946,17 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7879
8946
  } catch {
7880
8947
  }
7881
8948
  };
7882
- const pollTimer = setInterval(() => void poll().catch(() => {
7883
- }), 1200);
8949
+ refreshSessionProjection = () => poll().catch(() => {
8950
+ });
8951
+ await refreshSessionProjection();
8952
+ const pollTimer = setInterval(() => {
8953
+ if (busy || approvalPromptOpen) void refreshSessionProjection();
8954
+ }, 1200);
7884
8955
  await sessionEnded;
7885
8956
  clearInterval(pollTimer);
7886
8957
  clearInterval(heartbeatPoll);
7887
- 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(() => {
7888
8960
  }) : Promise.resolve();
7889
8961
  const procsStopped = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
7890
8962
  bridge?.close();
@@ -8084,12 +9156,12 @@ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNot
8084
9156
  );
8085
9157
  if (!picked) return;
8086
9158
  if (picked === ADD) {
8087
- const path13 = await tui.prompt(
9159
+ const path14 = await tui.prompt(
8088
9160
  `Absolute project path on ${machine.name}`,
8089
9161
  machine.id === self.machine_id ? process.cwd() : "/home/you/project"
8090
9162
  );
8091
- if (!path13 || !path13.trim()) return;
8092
- const trimmed = path13.trim();
9163
+ if (!path14 || !path14.trim()) return;
9164
+ const trimmed = path14.trim();
8093
9165
  if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
8094
9166
  tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
8095
9167
  return;