@standardagents/code 0.9.5 → 0.9.7

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);
@@ -113,6 +249,27 @@ var ApiClient = class {
113
249
  async verify() {
114
250
  return (await this.verifyDetailed()).ok;
115
251
  }
252
+ /**
253
+ * Re-point a thread's ROOT agent (mid-session agent switch). The runtime
254
+ * treats this as an explicit handoff: the full conversation is preserved
255
+ * and the next turn plays on the new agent. Published spec surface
256
+ * (`PATCH /api/threads/:id { agent_id }`), same as every head uses.
257
+ */
258
+ async setThreadAgent(threadId, agentId) {
259
+ await this.json(`/api/threads/${threadId}`, {
260
+ method: "PATCH",
261
+ body: JSON.stringify({ agent_id: agentId })
262
+ });
263
+ }
264
+ /**
265
+ * Whether the account has completed the Sama One (OpenSama) ChatGPT
266
+ * connection. Secret env variables come back as metadata only — the
267
+ * instance never returns the OpenAI key value to a client.
268
+ */
269
+ async openSamaStatus() {
270
+ const body = await this.json("/api/users/me/env");
271
+ return (body.variables ?? []).some((entry) => entry.name === "OPENSAMA_OPENAI_API_KEY");
272
+ }
116
273
  /**
117
274
  * Check the endpoint + token and say PRECISELY what's wrong when they
118
275
  * fail. "That token didn't work" is a lie when the real problem is a typo'd
@@ -183,21 +340,36 @@ var ApiClient = class {
183
340
  */
184
341
  async listThreads(agentId, requireTags) {
185
342
  const ids = Array.isArray(agentId) ? agentId : [agentId];
186
- const pages = await Promise.all(
187
- ids.map(
343
+ const [me, ...pages] = await Promise.all([
344
+ this.currentUserId(),
345
+ ...ids.map(
188
346
  (id) => this.json(
189
347
  `/api/threads?agent_id=${encodeURIComponent(id)}&limit=100`
190
348
  ).catch(() => [])
191
349
  )
192
- );
350
+ ]);
193
351
  const arr = pages.flatMap((res) => Array.isArray(res) ? res : res.threads || []);
194
352
  return arr.map((t) => ({
195
353
  id: t.id,
196
354
  tags: Array.isArray(t.tags) ? t.tags : [],
197
355
  created_at: t.created_at,
198
356
  title: t.title,
199
- preview: t.preview || t.last_message
200
- })).filter((t) => requireTags.every((tag) => t.tags.includes(tag)));
357
+ preview: t.preview || t.last_message,
358
+ user_id: t.user_id ?? null
359
+ })).filter((t) => !me || !t.user_id || t.user_id === me).filter((t) => requireTags.every((tag) => t.tags.includes(tag)));
360
+ }
361
+ /** The authenticated user's id (cached). Null when the instance doesn't
362
+ * report one (super-admin sessions, very old instances). */
363
+ meUserId;
364
+ async currentUserId() {
365
+ if (this.meUserId !== void 0) return this.meUserId;
366
+ try {
367
+ const res = await this.json(`/api/auth/me`);
368
+ this.meUserId = typeof res?.user?.id === "string" && res.user.id ? res.user.id : null;
369
+ } catch {
370
+ this.meUserId = null;
371
+ }
372
+ return this.meUserId;
201
373
  }
202
374
  /**
203
375
  * Subagent child threads of a thread, each with its current lifecycle status
@@ -225,14 +397,85 @@ var ApiClient = class {
225
397
  * `mimeType` — which the server stores in the thread filesystem and injects
226
398
  * into the LLM's vision context as real image content blocks.
227
399
  */
228
- async sendMessage(threadId, content, attachments) {
400
+ async sendMessage(threadId, content, attachments2) {
229
401
  const body = { role: "user", content };
230
- if (attachments && attachments.length > 0) body.attachments = attachments;
402
+ if (attachments2 && attachments2.length > 0) body.attachments = attachments2;
231
403
  await this.json(`/api/threads/${threadId}/messages`, {
232
404
  method: "POST",
233
405
  body: JSON.stringify(body)
234
406
  });
235
407
  }
408
+ // ── portable Standard Code shared messaging endpoints ────────────────────
409
+ messagingPath(threadId, suffix = "") {
410
+ return `/api/threads/${threadId}${SHARED_MESSAGING_ROUTE}${suffix}`;
411
+ }
412
+ async getSharedMessaging(threadId) {
413
+ return parseSharedMessagingSnapshot(await this.json(this.messagingPath(threadId)));
414
+ }
415
+ async appendPendingInput(threadId, mutation) {
416
+ return parseSharedMessagingSnapshot(
417
+ await this.json(this.messagingPath(threadId, "/pending"), {
418
+ method: "POST",
419
+ body: JSON.stringify(mutation)
420
+ })
421
+ );
422
+ }
423
+ async steerInput(threadId, mutation) {
424
+ return parseSharedMessagingSnapshot(
425
+ await this.json(this.messagingPath(threadId, "/steer"), {
426
+ method: "POST",
427
+ body: JSON.stringify(mutation)
428
+ })
429
+ );
430
+ }
431
+ async updatePendingInput(threadId, pendingId, mutation) {
432
+ return parseSharedMessagingSnapshot(
433
+ await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}`), {
434
+ method: "PATCH",
435
+ body: JSON.stringify(mutation)
436
+ })
437
+ );
438
+ }
439
+ async dismissPendingInput(threadId, pendingId, origin2) {
440
+ return parseSharedMessagingSnapshot(
441
+ await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}`), {
442
+ method: "DELETE",
443
+ body: JSON.stringify(origin2)
444
+ })
445
+ );
446
+ }
447
+ async steerPendingInput(threadId, pendingId, origin2) {
448
+ return parseSharedMessagingSnapshot(
449
+ await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}/steer`), {
450
+ method: "POST",
451
+ body: JSON.stringify(origin2)
452
+ })
453
+ );
454
+ }
455
+ async putSharedDraft(threadId, mutation) {
456
+ return parseSharedMessagingSnapshot(
457
+ await this.json(this.messagingPath(threadId, "/draft"), {
458
+ method: "PUT",
459
+ body: JSON.stringify(mutation)
460
+ })
461
+ );
462
+ }
463
+ async clearSharedDraft(threadId, origin2) {
464
+ return parseSharedMessagingSnapshot(
465
+ await this.json(this.messagingPath(threadId, "/draft"), {
466
+ method: "DELETE",
467
+ body: JSON.stringify(origin2)
468
+ })
469
+ );
470
+ }
471
+ async requestSharedStop(threadId, origin2) {
472
+ return parseSharedMessagingSnapshot(
473
+ await this.json(this.messagingPath(threadId, "/stop"), {
474
+ method: "POST",
475
+ body: JSON.stringify(origin2)
476
+ })
477
+ );
478
+ }
236
479
  async getMessages(threadId, limit = 50, order) {
237
480
  const orderParam = order ? `&order=${order}` : "";
238
481
  const res = await this.json(
@@ -240,6 +483,14 @@ var ApiClient = class {
240
483
  );
241
484
  return Array.isArray(res) ? res : res.messages || [];
242
485
  }
486
+ /**
487
+ * One server-derived projection for busy/idle, current tool, conversation,
488
+ * goal, and live children. Streams provide immediacy; this snapshot settles
489
+ * state after reconnects and prevents each UI from inventing lifecycle rules.
490
+ */
491
+ async getSessionState(threadId, limit = 500) {
492
+ return this.json(`/api/threads/${threadId}/session_state?limit=${limit}`);
493
+ }
243
494
  async getLogs(threadId, limit = 100) {
244
495
  const res = await this.json(
245
496
  `/api/threads/${threadId}/logs?limit=${limit}&order=desc`
@@ -403,12 +654,6 @@ var ApiClient = class {
403
654
  async compact(threadId) {
404
655
  await this.json(`/api/threads/${threadId}/compact`, { method: "POST" });
405
656
  }
406
- async stop(threadId) {
407
- try {
408
- await this.json(`/api/threads/${threadId}/stop`, { method: "POST" });
409
- } catch {
410
- }
411
- }
412
657
  /**
413
658
  * Run a user-typed `!command` on the thread's execution owner (wherever the
414
659
  * session runs — e.g. a remote VPS daemon). The instance forwards it over
@@ -504,6 +749,7 @@ var Heartbeat = class {
504
749
  this.onDead = onDead;
505
750
  this.intervalMs = options.intervalMs ?? HEARTBEAT_INTERVAL_MS;
506
751
  this.silenceMs = options.silenceMs ?? CONNECTION_SILENCE_TIMEOUT_MS;
752
+ this.request = options.request ?? "ping";
507
753
  }
508
754
  ws;
509
755
  onDead;
@@ -511,6 +757,7 @@ var Heartbeat = class {
511
757
  lastRecvAt = 0;
512
758
  intervalMs;
513
759
  silenceMs;
760
+ request;
514
761
  start() {
515
762
  this.stop();
516
763
  this.lastRecvAt = Date.now();
@@ -520,6 +767,10 @@ var Heartbeat = class {
520
767
  markAlive() {
521
768
  this.lastRecvAt = Date.now();
522
769
  }
770
+ /** Change the heartbeat frame without restarting the connection timer. */
771
+ setRequest(request) {
772
+ this.request = request;
773
+ }
523
774
  stop() {
524
775
  if (this.timer) {
525
776
  clearInterval(this.timer);
@@ -532,7 +783,7 @@ var Heartbeat = class {
532
783
  return;
533
784
  }
534
785
  try {
535
- if (this.ws.readyState === WebSocket.OPEN) this.ws.send("ping");
786
+ if (this.ws.readyState === WebSocket.OPEN) this.ws.send(this.request);
536
787
  else this.fail();
537
788
  } catch {
538
789
  this.fail();
@@ -554,6 +805,56 @@ var DIM = "\x1B[2m";
554
805
  var ADD_BG = "\x1B[48;5;22m\x1B[38;5;254m";
555
806
  var DEL_BG = "\x1B[48;5;52m\x1B[38;5;254m";
556
807
  var MAX_SIDE_LINES = 4;
808
+ function terminalGlyphWidth(ch) {
809
+ const cp = ch.codePointAt(0);
810
+ 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;
811
+ }
812
+ function truncateMiddle(text, maxColumns) {
813
+ const limit = Math.max(0, Math.floor(maxColumns));
814
+ if (limit === 0) return "";
815
+ const chars = [...text];
816
+ const totalWidth = chars.reduce((width, ch) => width + terminalGlyphWidth(ch), 0);
817
+ if (totalWidth <= limit) return text;
818
+ if (limit === 1) return "\u2026";
819
+ const available = limit - 1;
820
+ const headBudget = Math.ceil(available / 2);
821
+ const tailBudget = Math.floor(available / 2);
822
+ const head = [];
823
+ const tail = [];
824
+ let headWidth = 0;
825
+ let tailWidth = 0;
826
+ for (let i = 0; i < chars.length; i++) {
827
+ const width = terminalGlyphWidth(chars[i]);
828
+ if (headWidth + width > headBudget) break;
829
+ head.push(chars[i]);
830
+ headWidth += width;
831
+ }
832
+ for (let i = chars.length - 1; i >= head.length; i--) {
833
+ const width = terminalGlyphWidth(chars[i]);
834
+ if (tailWidth + width > tailBudget) break;
835
+ tail.unshift(chars[i]);
836
+ tailWidth += width;
837
+ }
838
+ const isSeparator = (ch) => ch === "/" || ch === "\\";
839
+ let headEnd = head.length;
840
+ let tailStart = chars.length - tail.length;
841
+ for (let i = headEnd - 1; i >= 0; i--) {
842
+ if (isSeparator(chars[i])) {
843
+ headEnd = i + 1;
844
+ break;
845
+ }
846
+ }
847
+ for (let i = tailStart; i < chars.length; i++) {
848
+ if (isSeparator(chars[i])) {
849
+ tailStart = i;
850
+ break;
851
+ }
852
+ }
853
+ if (headEnd < tailStart && headEnd > 0 && tailStart < chars.length) {
854
+ return `${chars.slice(0, headEnd).join("")}\u2026${chars.slice(tailStart).join("")}`;
855
+ }
856
+ return `${head.join("")}\u2026${tail.join("")}`;
857
+ }
557
858
  function clamp(s, max) {
558
859
  return s.length > max ? s.slice(0, Math.max(0, max - 1)) + "\u2026" : s;
559
860
  }
@@ -727,6 +1028,7 @@ var Bridge = class {
727
1028
  ws = null;
728
1029
  closed = false;
729
1030
  heartbeat = null;
1031
+ activeToolRequests = 0;
730
1032
  reconnectAttempt = 0;
731
1033
  reconnectTimer = null;
732
1034
  resolveConnected = null;
@@ -850,9 +1152,14 @@ var Bridge = class {
850
1152
  }
851
1153
  startHeartbeat(ws) {
852
1154
  this.stopHeartbeat();
853
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
1155
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), {
1156
+ request: this.activeToolRequests > 0 ? "ping" : "stream_ping"
1157
+ });
854
1158
  this.heartbeat.start();
855
1159
  }
1160
+ updateHeartbeatMode() {
1161
+ this.heartbeat?.setRequest(this.activeToolRequests > 0 ? "ping" : "stream_ping");
1162
+ }
856
1163
  stopHeartbeat() {
857
1164
  if (this.heartbeat) {
858
1165
  this.heartbeat.stop();
@@ -938,7 +1245,14 @@ var Bridge = class {
938
1245
  if (msg.type !== "tool_request") return;
939
1246
  if (!this.owner) return;
940
1247
  const req = msg;
941
- await this.handleToolRequest(req);
1248
+ this.activeToolRequests++;
1249
+ this.updateHeartbeatMode();
1250
+ try {
1251
+ await this.handleToolRequest(req);
1252
+ } finally {
1253
+ this.activeToolRequests = Math.max(0, this.activeToolRequests - 1);
1254
+ this.updateHeartbeatMode();
1255
+ }
942
1256
  }
943
1257
  /**
944
1258
  * Reply to a tool request. Durable calls (the agent parked them) deliver the
@@ -1112,7 +1426,7 @@ function detailSuffix(tool, result) {
1112
1426
  const lines = result.split("\n").length;
1113
1427
  return ` (${lines} line${lines === 1 ? "" : "s"})`;
1114
1428
  }
1115
- var LOG_DIR = path3.join(os10.homedir(), ".standardagents", "process-logs");
1429
+ var LOG_DIR = path3.join(os7.homedir(), ".standardagents", "process-logs");
1116
1430
  var KEY2 = "bg_processes";
1117
1431
  function isAlive(pid) {
1118
1432
  try {
@@ -1189,7 +1503,7 @@ var ProcessRegistry = class {
1189
1503
  }
1190
1504
  };
1191
1505
  function configFile() {
1192
- return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(os10.homedir(), ".standardagents", "mcp.json");
1506
+ return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(os7.homedir(), ".standardagents", "mcp.json");
1193
1507
  }
1194
1508
  function loadMcpConfig() {
1195
1509
  try {
@@ -1491,7 +1805,7 @@ var HostTools = class {
1491
1805
  }
1492
1806
  }
1493
1807
  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}`);
1808
+ const skillDir = path3.join(os7.tmpdir(), "standardcode-skills", `${skill}-${hash}`);
1495
1809
  for (const f of files) {
1496
1810
  const dest = path3.resolve(skillDir, f.path);
1497
1811
  if (path3.relative(skillDir, dest).startsWith("..")) {
@@ -2418,6 +2732,7 @@ var MessageStream = class {
2418
2732
  ws.addEventListener("open", () => {
2419
2733
  this.reconnectAttempt = 0;
2420
2734
  this.startHeartbeat(ws);
2735
+ this.hooks.onOpen?.();
2421
2736
  this.resolveConnected?.();
2422
2737
  });
2423
2738
  ws.addEventListener("message", (ev) => {
@@ -2435,7 +2750,7 @@ var MessageStream = class {
2435
2750
  }
2436
2751
  startHeartbeat(ws) {
2437
2752
  this.stopHeartbeat();
2438
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
2753
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
2439
2754
  this.heartbeat.start();
2440
2755
  }
2441
2756
  stopHeartbeat() {
@@ -2484,10 +2799,11 @@ var MessageStream = class {
2484
2799
  }
2485
2800
  if (msg.type === "message_data" && (msg.depth ?? 0) === 0) {
2486
2801
  const data = msg.data || {};
2802
+ this.hooks.onMessage?.(data);
2487
2803
  if (data.role === "assistant" && typeof data.content === "string" && data.content.trim()) {
2488
2804
  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);
2805
+ const hasToolCalls = Array.isArray(tc) ? tc.length > 0 : typeof tc === "string" && tc.trim() !== "" && tc.trim() !== "null" && tc.trim() !== "[]";
2806
+ this.hooks.onAssistantText(data.content, hasToolCalls);
2491
2807
  }
2492
2808
  if (data.role === "assistant" && data.status === "failed" && data.error) {
2493
2809
  this.hooks.onError(String(data.error));
@@ -2537,7 +2853,7 @@ var SystemEvents = class {
2537
2853
  }
2538
2854
  startHeartbeat(ws) {
2539
2855
  this.stopHeartbeat();
2540
- this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
2856
+ this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
2541
2857
  this.heartbeat.start();
2542
2858
  }
2543
2859
  stopHeartbeat() {
@@ -2670,6 +2986,664 @@ var SubagentActivity = class {
2670
2986
  }
2671
2987
  };
2672
2988
 
2989
+ // src/session-state.ts
2990
+ var emptyLiveDraft = () => ({ version: 1, text: "", messageIds: [] });
2991
+ function normalizeLiveDraft(state) {
2992
+ return {
2993
+ version: 1,
2994
+ text: typeof state?.text === "string" ? state.text : "",
2995
+ messageIds: Array.isArray(state?.messageIds) ? [...new Set(state.messageIds.filter((id) => typeof id === "string" && !!id))] : []
2996
+ };
2997
+ }
2998
+ function appendLiveDraft(state, chunks) {
2999
+ const current = normalizeLiveDraft(state);
3000
+ const nextIds = [...current.messageIds];
3001
+ let text = current.text;
3002
+ for (const chunk of Array.isArray(chunks) ? chunks : [chunks]) {
3003
+ if (typeof chunk?.text !== "string" || !chunk.text) continue;
3004
+ text += chunk.text;
3005
+ if (typeof chunk.messageId === "string" && chunk.messageId && !nextIds.includes(chunk.messageId)) {
3006
+ nextIds.push(chunk.messageId);
3007
+ }
3008
+ }
3009
+ return { version: 1, text, messageIds: nextIds.slice(-64) };
3010
+ }
3011
+ var createdAt = (message) => Number(message.created_at ?? message.createdAt ?? 0);
3012
+ function messageText(content) {
3013
+ if (typeof content === "string") return content;
3014
+ if (Array.isArray(content)) {
3015
+ return content.map((block) => typeof block === "string" ? block : typeof block?.text === "string" ? block.text : "").join("");
3016
+ }
3017
+ return "";
3018
+ }
3019
+ function parseObject(value) {
3020
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
3021
+ if (typeof value !== "string" || !value.trim()) return {};
3022
+ try {
3023
+ const parsed = JSON.parse(value);
3024
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
3025
+ } catch {
3026
+ return {};
3027
+ }
3028
+ }
3029
+ function parseToolCalls(message) {
3030
+ if (Array.isArray(message.toolCalls)) return message.toolCalls;
3031
+ let raw = message.tool_calls;
3032
+ if (typeof raw === "string") {
3033
+ try {
3034
+ raw = JSON.parse(raw);
3035
+ } catch {
3036
+ return [];
3037
+ }
3038
+ }
3039
+ if (!Array.isArray(raw)) return [];
3040
+ return raw.flatMap((item) => {
3041
+ const id = typeof item?.id === "string" ? item.id : "";
3042
+ const nameValue = item?.function?.name ?? item?.name;
3043
+ const name = typeof nameValue === "string" ? nameValue : "";
3044
+ if (!id || !name) return [];
3045
+ return [{ id, name, arguments: parseObject(item?.function?.arguments ?? item?.arguments ?? item?.args) }];
3046
+ });
3047
+ }
3048
+ function deriveSessionActivity(messages) {
3049
+ const visible = messages.filter((message) => message.silent !== true && message.metadata?.silent !== true).sort((a, b) => createdAt(a) - createdAt(b));
3050
+ if (visible.some((message) => message.status === "pending")) return { busy: true, currentTool: unresolvedTool(visible) };
3051
+ const last = visible.at(-1);
3052
+ if (!last) return { busy: false, currentTool: null };
3053
+ const currentTool = unresolvedTool(visible);
3054
+ if (currentTool) return { busy: true, currentTool };
3055
+ if (last.role === "user" || last.role === "tool") return { busy: true, currentTool: null };
3056
+ if (last.role === "assistant" && last.status !== "failed" && !messageText(last.content).trim()) {
3057
+ return { busy: true, currentTool: null };
3058
+ }
3059
+ return { busy: false, currentTool: null };
3060
+ }
3061
+ function threadBusy(messages) {
3062
+ return deriveSessionActivity(messages).busy;
3063
+ }
3064
+ function unresolvedTool(messages) {
3065
+ const resultIds = new Set(messages.flatMap((message) => {
3066
+ const id = message.tool_call_id ?? message.toolCallId;
3067
+ return message.role === "tool" && typeof id === "string" ? [id] : [];
3068
+ }));
3069
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
3070
+ if (messages[i].role === "user") return null;
3071
+ if (messages[i].role !== "assistant") continue;
3072
+ const calls = parseToolCalls(messages[i]);
3073
+ if (!calls.length) return null;
3074
+ return calls.find((tool) => !resultIds.has(tool.id)) ?? null;
3075
+ }
3076
+ return null;
3077
+ }
3078
+
3079
+ // src/transcript-delivery.ts
3080
+ function transcriptMessageReady(message, text) {
3081
+ if (message.status === "pending") return false;
3082
+ if (message.role === "assistant" && message.status !== "failed" && !text.trim()) return false;
3083
+ return true;
3084
+ }
3085
+
3086
+ // src/progressive-markdown.ts
3087
+ var punctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
3088
+ var isWhitespace = (value) => value === void 0 || /\s/.test(value);
3089
+ var isPunctuation = (value) => value !== void 0 && punctuation.test(value);
3090
+ function runLength(source, start, marker) {
3091
+ let end = start;
3092
+ while (source[end] === marker) end++;
3093
+ return end - start;
3094
+ }
3095
+ function delimiterFlanking(source, at, length, marker) {
3096
+ const previous = at > 0 && source[at - 1] !== "\n" ? source[at - 1] : void 0;
3097
+ const next = at + length < source.length && source[at + length] !== "\n" ? source[at + length] : void 0;
3098
+ const previousWhitespace = isWhitespace(previous);
3099
+ const nextWhitespace = isWhitespace(next);
3100
+ const previousPunctuation = isPunctuation(previous);
3101
+ const nextPunctuation = isPunctuation(next);
3102
+ const leftFlanking = !nextWhitespace && (!nextPunctuation || previousWhitespace || previousPunctuation);
3103
+ const rightFlanking = !previousWhitespace && (!previousPunctuation || nextWhitespace || nextPunctuation);
3104
+ if (marker === "_") {
3105
+ return {
3106
+ canOpen: leftFlanking && (!rightFlanking || previousPunctuation),
3107
+ canClose: rightFlanking && (!leftFlanking || nextPunctuation)
3108
+ };
3109
+ }
3110
+ return { canOpen: leftFlanking, canClose: rightFlanking };
3111
+ }
3112
+ function sameStyles(a, b) {
3113
+ return a.length === b.length && a.every((style, index) => style === b[index]);
3114
+ }
3115
+ function parseProgressiveInline(source, baseOffset = 0) {
3116
+ const runs = [];
3117
+ const stack = [];
3118
+ let inlineTicks = 0;
3119
+ let i = 0;
3120
+ const styles = () => [
3121
+ ...stack.map((frame) => frame.style),
3122
+ ...inlineTicks > 0 ? ["code"] : []
3123
+ ];
3124
+ const append = (text, at, href) => {
3125
+ if (!text) return;
3126
+ const active = styles();
3127
+ const last = runs.at(-1);
3128
+ if (last && last._end === at && last.href === href && sameStyles(last.styles, active)) {
3129
+ last.text += text;
3130
+ last._end = at + text.length;
3131
+ return;
3132
+ }
3133
+ runs.push({ id: `i:${baseOffset + at}`, text, styles: active, ...{}, _end: at + text.length });
3134
+ };
3135
+ while (i < source.length) {
3136
+ const character = source[i];
3137
+ if (character === "\\") {
3138
+ if (i + 1 < source.length && isPunctuation(source[i + 1])) {
3139
+ append(source[i + 1], i + 1);
3140
+ i += 2;
3141
+ continue;
3142
+ }
3143
+ if (i + 1 === source.length) {
3144
+ i++;
3145
+ continue;
3146
+ }
3147
+ }
3148
+ if (inlineTicks > 0) {
3149
+ if (character === "`") {
3150
+ const length = runLength(source, i, "`");
3151
+ if (length === inlineTicks) {
3152
+ inlineTicks = 0;
3153
+ i += length;
3154
+ continue;
3155
+ }
3156
+ if (i + length === source.length) {
3157
+ i += length;
3158
+ continue;
3159
+ }
3160
+ append("`".repeat(length), i);
3161
+ i += length;
3162
+ continue;
3163
+ }
3164
+ append(character, i);
3165
+ i++;
3166
+ continue;
3167
+ }
3168
+ if (character === "`") {
3169
+ const length = runLength(source, i, "`");
3170
+ if (i + length < source.length) inlineTicks = length;
3171
+ i += length;
3172
+ continue;
3173
+ }
3174
+ if (character === "[") {
3175
+ const destinationAt = source.indexOf("](", i + 1);
3176
+ if (destinationAt >= 0) {
3177
+ const destinationEnd = source.indexOf(")", destinationAt + 2);
3178
+ const end = destinationEnd >= 0 ? destinationEnd : source.length;
3179
+ const href = source.slice(destinationAt + 2, end);
3180
+ const labelStart = i + 1;
3181
+ const labelRuns = parseProgressiveInline(source.slice(labelStart, destinationAt), baseOffset + labelStart);
3182
+ for (const run3 of labelRuns) {
3183
+ runs.push({
3184
+ ...run3,
3185
+ styles: run3.styles.includes("link") ? run3.styles : [...run3.styles, "link"],
3186
+ ...destinationEnd >= 0 && href ? { href } : {},
3187
+ _end: run3.id.startsWith("i:") ? Number(run3.id.slice(2)) - baseOffset + run3.text.length : end
3188
+ });
3189
+ }
3190
+ i = destinationEnd >= 0 ? destinationEnd + 1 : source.length;
3191
+ continue;
3192
+ }
3193
+ }
3194
+ if (character === "*" || character === "_" || character === "~") {
3195
+ const run3 = runLength(source, i, character);
3196
+ const usable = character === "~" ? run3 - run3 % 2 : run3;
3197
+ if (usable > 0) {
3198
+ const { canOpen, canClose } = delimiterFlanking(source, i, run3, character);
3199
+ const frames = [];
3200
+ if (character === "~") {
3201
+ for (let n = 0; n < usable; n += 2) frames.push({ marker: "~", length: 2, style: "strike" });
3202
+ } else {
3203
+ let remaining = usable;
3204
+ while (remaining >= 2) {
3205
+ frames.push({ marker: character, length: 2, style: "strong" });
3206
+ remaining -= 2;
3207
+ }
3208
+ if (remaining) frames.push({ marker: character, length: 1, style: "emphasis" });
3209
+ }
3210
+ let consumed = 0;
3211
+ let closed = 0;
3212
+ if (canClose) {
3213
+ for (const frame of [...frames].reverse()) {
3214
+ const top = stack.at(-1);
3215
+ if (top && top.marker === frame.marker && top.length === frame.length) {
3216
+ stack.pop();
3217
+ consumed += frame.length;
3218
+ closed += frame.length;
3219
+ }
3220
+ }
3221
+ }
3222
+ if (canOpen) {
3223
+ for (const frame of frames) {
3224
+ if (consumed >= frame.length) consumed -= frame.length;
3225
+ else stack.push(frame);
3226
+ }
3227
+ }
3228
+ if (canOpen || closed > 0) {
3229
+ i += usable;
3230
+ if (run3 > usable) append(character.repeat(run3 - usable), i);
3231
+ i += run3 - usable;
3232
+ continue;
3233
+ }
3234
+ if (i + run3 === source.length) {
3235
+ i += run3;
3236
+ continue;
3237
+ }
3238
+ }
3239
+ if (usable === 0 && i + run3 === source.length) {
3240
+ i += run3;
3241
+ continue;
3242
+ }
3243
+ }
3244
+ append(character, i);
3245
+ i++;
3246
+ }
3247
+ return runs.map(({ _end: _, ...run3 }) => run3);
3248
+ }
3249
+ function sourceLines(source) {
3250
+ if (!source) return [];
3251
+ const lines = [];
3252
+ let start = 0;
3253
+ while (start <= source.length) {
3254
+ const newline = source.indexOf("\n", start);
3255
+ if (newline < 0) {
3256
+ lines.push({ text: source.slice(start).replace(/\r$/, ""), start, end: source.length, next: source.length, terminated: false });
3257
+ break;
3258
+ }
3259
+ lines.push({ text: source.slice(start, newline).replace(/\r$/, ""), start, end: newline, next: newline + 1, terminated: true });
3260
+ start = newline + 1;
3261
+ if (start === source.length) {
3262
+ lines.push({ text: "", start, end: start, next: start, terminated: false });
3263
+ break;
3264
+ }
3265
+ }
3266
+ return lines;
3267
+ }
3268
+ function blockFence(line) {
3269
+ const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
3270
+ if (!match) return null;
3271
+ const marker = match[1][0];
3272
+ const info = match[2];
3273
+ if (marker === "`" && info.includes("`")) return null;
3274
+ return { marker, length: match[1].length, info: info.trim() };
3275
+ }
3276
+ var tableSeparator = (line) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line) && line.includes("-");
3277
+ var partialTableSeparator = (line) => /^\s*\|?\s*:?-*:?\s*(\|\s*:?-*:?\s*)*\|?\s*$/.test(line) && /[-|:]/.test(line);
3278
+ function tableCells(line) {
3279
+ let start = 0;
3280
+ let end = line.length;
3281
+ while (start < end && /\s/.test(line[start])) start++;
3282
+ while (end > start && /\s/.test(line[end - 1])) end--;
3283
+ if (line[start] === "|") start++;
3284
+ if (line[end - 1] === "|") end--;
3285
+ const cells = [];
3286
+ let cellStart = start;
3287
+ for (let i = start; i <= end; i++) {
3288
+ if (i === end || line[i] === "|" && (i === 0 || line[i - 1] !== "\\")) {
3289
+ const raw = line.slice(cellStart, i);
3290
+ const leading = raw.match(/^\s*/)?.[0].length ?? 0;
3291
+ cells.push({ text: raw.trim().replace(/\\\|/g, "|"), offset: cellStart + leading });
3292
+ cellStart = i + 1;
3293
+ }
3294
+ }
3295
+ return cells;
3296
+ }
3297
+ function cell(text, offset) {
3298
+ return { id: `cell:${offset}`, runs: parseProgressiveInline(text, offset) };
3299
+ }
3300
+ function startsSpecialBlock(lines, index) {
3301
+ const line = lines[index]?.text ?? "";
3302
+ const next = lines[index + 1]?.text;
3303
+ 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);
3304
+ }
3305
+ function parseStreamingMarkdown(source) {
3306
+ const lines = sourceLines(source);
3307
+ const blocks = [];
3308
+ let i = 0;
3309
+ while (i < lines.length) {
3310
+ const line = lines[i];
3311
+ if (!line.text.trim()) {
3312
+ i++;
3313
+ continue;
3314
+ }
3315
+ const id = `b:${line.start}`;
3316
+ const opening = blockFence(line.text);
3317
+ if (opening) {
3318
+ const codeStart = line.next;
3319
+ let j2 = i + 1;
3320
+ let closing;
3321
+ while (j2 < lines.length) {
3322
+ const candidate = blockFence(lines[j2].text);
3323
+ if (candidate && candidate.marker === opening.marker && candidate.length >= opening.length && !candidate.info) {
3324
+ closing = lines[j2];
3325
+ break;
3326
+ }
3327
+ j2++;
3328
+ }
3329
+ let codeEnd = closing?.start ?? source.length;
3330
+ if (!closing) {
3331
+ const tailStart = Math.max(codeStart, source.lastIndexOf("\n") + 1);
3332
+ const tail = source.slice(tailStart);
3333
+ const pending = /^ {0,3}(`+|~+)[ \t]*$/.exec(tail);
3334
+ if (pending && pending[1][0] === opening.marker && pending[1].length < opening.length) codeEnd = tailStart;
3335
+ }
3336
+ if (codeEnd > codeStart && source[codeEnd - 1] === "\n") codeEnd--;
3337
+ blocks.push({
3338
+ id,
3339
+ kind: "code",
3340
+ start: line.start,
3341
+ end: closing?.next ?? source.length,
3342
+ complete: !!closing && closing.terminated,
3343
+ language: opening.info || "code",
3344
+ text: source.slice(codeStart, codeEnd)
3345
+ });
3346
+ i = closing ? j2 + 1 : lines.length;
3347
+ continue;
3348
+ }
3349
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line.text);
3350
+ if (heading) {
3351
+ const contentAt = line.start + heading[1].length + 1;
3352
+ blocks.push({
3353
+ id,
3354
+ kind: "heading",
3355
+ start: line.start,
3356
+ end: line.end,
3357
+ complete: line.terminated,
3358
+ level: heading[1].length,
3359
+ runs: parseProgressiveInline(heading[2], contentAt)
3360
+ });
3361
+ i++;
3362
+ continue;
3363
+ }
3364
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line.text)) {
3365
+ blocks.push({ id, kind: "rule", start: line.start, end: line.end, complete: line.terminated });
3366
+ i++;
3367
+ continue;
3368
+ }
3369
+ const separatorNext = line.text.includes("|") && i + 1 < lines.length && tableSeparator(lines[i + 1].text);
3370
+ if (separatorNext) {
3371
+ const separator = tableCells(lines[i + 1].text);
3372
+ const align = separator.map(({ text: text2 }) => text2.startsWith(":") && text2.endsWith(":") ? "center" : text2.endsWith(":") ? "right" : text2.startsWith(":") ? "left" : "");
3373
+ const header = tableCells(line.text).map((entry) => cell(entry.text, line.start + entry.offset));
3374
+ const rows = [];
3375
+ let j2 = i + 2;
3376
+ while (j2 < lines.length && lines[j2].text.trim() && lines[j2].text.includes("|")) {
3377
+ rows.push(tableCells(lines[j2].text).map((entry) => cell(entry.text, lines[j2].start + entry.offset)));
3378
+ j2++;
3379
+ }
3380
+ blocks.push({
3381
+ id,
3382
+ kind: "table",
3383
+ start: line.start,
3384
+ end: lines[Math.max(i + 1, j2 - 1)].end,
3385
+ complete: j2 < lines.length && lines[j2].terminated,
3386
+ header,
3387
+ rows,
3388
+ align
3389
+ });
3390
+ i = j2;
3391
+ continue;
3392
+ }
3393
+ if (/^ {0,3}\|/.test(line.text)) {
3394
+ const streamingSeparator = i + 1 >= lines.length || !lines[i + 1].terminated && (!lines[i + 1].text || partialTableSeparator(lines[i + 1].text));
3395
+ if (streamingSeparator) {
3396
+ const header = tableCells(line.text).map((entry) => cell(entry.text, line.start + entry.offset));
3397
+ if (header.some((entry) => entry.runs.length)) {
3398
+ blocks.push({
3399
+ id,
3400
+ kind: "table",
3401
+ start: line.start,
3402
+ end: lines[Math.min(i + 1, lines.length - 1)].end,
3403
+ complete: false,
3404
+ header,
3405
+ rows: []
3406
+ });
3407
+ } else {
3408
+ blocks.push({ id, kind: "paragraph", start: line.start, end: line.end, complete: false, runs: [] });
3409
+ }
3410
+ i = lines.length;
3411
+ continue;
3412
+ }
3413
+ }
3414
+ const quote = /^\s*>\s?(.*)$/.exec(line.text);
3415
+ if (quote) {
3416
+ const parts = [];
3417
+ let j2 = i;
3418
+ let firstContentAt = line.start + line.text.indexOf(quote[1]);
3419
+ while (j2 < lines.length) {
3420
+ const match = /^\s*>\s?(.*)$/.exec(lines[j2].text);
3421
+ if (!match) break;
3422
+ if (!parts.length) firstContentAt = lines[j2].start + lines[j2].text.indexOf(match[1]);
3423
+ parts.push(match[1]);
3424
+ j2++;
3425
+ }
3426
+ blocks.push({
3427
+ id,
3428
+ kind: "quote",
3429
+ start: line.start,
3430
+ end: lines[j2 - 1].end,
3431
+ complete: j2 < lines.length && lines[j2].terminated,
3432
+ runs: parseProgressiveInline(parts.join("\n"), firstContentAt)
3433
+ });
3434
+ i = j2;
3435
+ continue;
3436
+ }
3437
+ const list = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line.text);
3438
+ if (list) {
3439
+ const ordered = /^\d/.test(list[2]);
3440
+ const items = [];
3441
+ let j2 = i;
3442
+ while (j2 < lines.length) {
3443
+ const match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(lines[j2].text);
3444
+ if (!match || /^\d/.test(match[2]) !== ordered) break;
3445
+ const contentAt = lines[j2].start + match[1].length + match[2].length + 1;
3446
+ items.push(cell(match[3], contentAt));
3447
+ j2++;
3448
+ }
3449
+ blocks.push({
3450
+ id,
3451
+ kind: "list",
3452
+ start: line.start,
3453
+ end: lines[j2 - 1].end,
3454
+ complete: j2 < lines.length && lines[j2].terminated,
3455
+ ordered,
3456
+ items
3457
+ });
3458
+ i = j2;
3459
+ continue;
3460
+ }
3461
+ if (!line.terminated && (/^ {0,3}#{1,6}\s*$/.test(line.text) || /^\s*[-*_]{1,2}\s*$/.test(line.text))) {
3462
+ blocks.push({ id, kind: "paragraph", start: line.start, end: line.end, complete: false, runs: [] });
3463
+ i++;
3464
+ continue;
3465
+ }
3466
+ const paragraph = [line];
3467
+ let j = i + 1;
3468
+ while (j < lines.length && lines[j].text.trim() && !startsSpecialBlock(lines, j)) {
3469
+ paragraph.push(lines[j]);
3470
+ j++;
3471
+ }
3472
+ const text = paragraph.map((part) => part.text).join("\n");
3473
+ blocks.push({
3474
+ id,
3475
+ kind: "paragraph",
3476
+ start: line.start,
3477
+ end: paragraph.at(-1).end,
3478
+ complete: j < lines.length && lines[j].terminated,
3479
+ runs: parseProgressiveInline(text, line.start)
3480
+ });
3481
+ i = j;
3482
+ }
3483
+ return { version: 3, sourceLength: source.length, blocks };
3484
+ }
3485
+
3486
+ // src/markdown.ts
3487
+ var ESC = "\x1B[";
3488
+ var R = ESC + "0m";
3489
+ var BOLD = ESC + "1m";
3490
+ var DIM3 = ESC + "2m";
3491
+ var ITAL = ESC + "3m";
3492
+ var UNDER = ESC + "4m";
3493
+ var TEAL = ESC + "38;5;37m";
3494
+ var CYAN = ESC + "36m";
3495
+ var GRAY = ESC + "90m";
3496
+ var ANSI = /\x1b\[[0-9;]*m/g;
3497
+ function visibleWidth(s) {
3498
+ return s.replace(ANSI, "").length;
3499
+ }
3500
+ function padEndVisible(s, width) {
3501
+ const pad = width - visibleWidth(s);
3502
+ return pad > 0 ? s + " ".repeat(pad) : s;
3503
+ }
3504
+ function inline(s) {
3505
+ const codes = [];
3506
+ s = s.replace(/`([^`]+)`/g, (_, code) => {
3507
+ codes.push(code);
3508
+ return "\0" + (codes.length - 1) + "\0";
3509
+ });
3510
+ s = s.replace(
3511
+ /\[([^\]]+)\]\(([^)\s]+)\)/g,
3512
+ (_, text, url) => `${CYAN}${UNDER}${text}${R} ${DIM3}${url}${R}`
3513
+ );
3514
+ s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => `${BOLD}${t}${R}`);
3515
+ s = s.replace(/\*([^*\n]+)\*/g, (_, t) => `${ITAL}${t}${R}`);
3516
+ s = s.replace(/__([^_]+)__/g, (_, t) => `${BOLD}${t}${R}`);
3517
+ s = s.replace(/(^|[^\w])_([^_\n]+)_($|[^\w])/g, (_, a, t, b) => `${a}${ITAL}${t}${R}${b}`);
3518
+ s = s.replace(/~~([^~]+)~~/g, (_, t) => `${DIM3}${t}${R}`);
3519
+ s = s.replace(/\x00(\d+)\x00/g, (_, i) => `${TEAL}${codes[+i].replace(/ /g, String.fromCharCode(160))}${R}`);
3520
+ return s;
3521
+ }
3522
+ function wrapStyled(text, width) {
3523
+ if (width < 4 || visibleWidth(text) <= width) return [text];
3524
+ const words = text.split(" ");
3525
+ const lines = [];
3526
+ let cur = "";
3527
+ let curLen = 0;
3528
+ for (const w of words) {
3529
+ const wLen = visibleWidth(w);
3530
+ if (cur === "") {
3531
+ cur = w;
3532
+ curLen = wLen;
3533
+ } else if (curLen + 1 + wLen <= width) {
3534
+ cur += " " + w;
3535
+ curLen += 1 + wLen;
3536
+ } else {
3537
+ lines.push(cur);
3538
+ cur = w;
3539
+ curLen = wLen;
3540
+ }
3541
+ }
3542
+ if (cur !== "" || lines.length === 0) lines.push(cur);
3543
+ return lines;
3544
+ }
3545
+ function wrapBlock(out, cols2, leadFirst, leadRest, leadWidth, text) {
3546
+ const wrapped = wrapStyled(text, Math.max(8, cols2 - leadWidth));
3547
+ wrapped.forEach((ln, idx) => out.push((idx === 0 ? leadFirst : leadRest) + ln));
3548
+ }
3549
+ function renderTable(rows) {
3550
+ const cols2 = Math.max(...rows.map((r) => r.length));
3551
+ const widths = [];
3552
+ for (let c4 = 0; c4 < cols2; c4++) {
3553
+ widths[c4] = Math.max(...rows.map((r) => visibleWidth(inline(r[c4] ?? ""))));
3554
+ }
3555
+ const sep = `${GRAY} \u2502 ${R}`;
3556
+ const out = [];
3557
+ rows.forEach((r, ri) => {
3558
+ const cells = [];
3559
+ for (let c4 = 0; c4 < cols2; c4++) {
3560
+ const raw = r[c4] ?? "";
3561
+ const styled = ri === 0 ? `${BOLD}${inline(raw)}${R}` : inline(raw);
3562
+ cells.push(padEndVisible(styled, widths[c4]));
3563
+ }
3564
+ out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
3565
+ if (ri === 0) {
3566
+ const rule = widths.map((w) => `${GRAY}${"\u2500".repeat(w)}${R}`).join(`${GRAY}\u2500\u253C\u2500${R}`);
3567
+ out.push(" " + rule);
3568
+ }
3569
+ });
3570
+ return out;
3571
+ }
3572
+ function streamingRun(run3) {
3573
+ let text = run3.styles.includes("code") ? run3.text.replace(/ /g, String.fromCharCode(160)) : run3.text;
3574
+ if (run3.styles.includes("code")) text = `${TEAL}${text}${R}`;
3575
+ if (run3.styles.includes("strike")) text = `${DIM3}${text}${R}`;
3576
+ if (run3.styles.includes("strong")) text = `${BOLD}${text}${R}`;
3577
+ if (run3.styles.includes("emphasis")) text = `${ITAL}${text}${R}`;
3578
+ if (run3.href || run3.styles.includes("link")) text = `${CYAN}${UNDER}${text}${R}`;
3579
+ return text;
3580
+ }
3581
+ function streamingInline(runs) {
3582
+ return runs.map(streamingRun).join("");
3583
+ }
3584
+ function plainCell(cell2) {
3585
+ return cell2.runs.map((run3) => run3.text).join("");
3586
+ }
3587
+ function renderStreamingMarkdown(src, cols2 = 80) {
3588
+ const document = parseStreamingMarkdown(src);
3589
+ const out = [];
3590
+ for (const block of document.blocks) {
3591
+ if (out.length && out.at(-1) !== "") out.push("");
3592
+ switch (block.kind) {
3593
+ case "code": {
3594
+ const lines = (block.text ?? "").split("\n");
3595
+ if (lines.length === 1 && !lines[0]) out.push(`${GRAY}\u2502${R} `);
3596
+ else for (const line of lines) out.push(`${GRAY}\u2502${R} ${line}`);
3597
+ break;
3598
+ }
3599
+ case "heading": {
3600
+ const text = streamingInline(block.runs ?? []);
3601
+ for (const line of wrapStyled(text, cols2)) out.push(`${BOLD}${TEAL}${line}${R}`);
3602
+ break;
3603
+ }
3604
+ case "rule":
3605
+ out.push(`${GRAY}\u2500\u2500\u2500\u2500\u2500\u2500${R}`);
3606
+ break;
3607
+ case "quote": {
3608
+ const text = streamingInline(block.runs ?? []);
3609
+ for (const logical of text.split("\n")) {
3610
+ for (const line of wrapStyled(logical, Math.max(8, cols2 - 2))) out.push(`${GRAY}\u2502${R} ${DIM3}${line}${R}`);
3611
+ }
3612
+ break;
3613
+ }
3614
+ case "list": {
3615
+ for (let index = 0; index < (block.items ?? []).length; index++) {
3616
+ const marker = block.ordered ? `${index + 1}.` : "\u2022";
3617
+ const lead = block.ordered ? `${BOLD}${marker}${R} ` : `${TEAL}${marker}${R} `;
3618
+ wrapBlock(
3619
+ out,
3620
+ cols2,
3621
+ lead,
3622
+ " ".repeat(marker.length + 1),
3623
+ marker.length + 1,
3624
+ streamingInline(block.items[index].runs)
3625
+ );
3626
+ }
3627
+ break;
3628
+ }
3629
+ case "table": {
3630
+ const rows = [
3631
+ (block.header ?? []).map(plainCell),
3632
+ ...(block.rows ?? []).map((row) => row.map(plainCell))
3633
+ ];
3634
+ if (rows[0].length) out.push(...renderTable(rows));
3635
+ break;
3636
+ }
3637
+ case "paragraph": {
3638
+ const text = streamingInline(block.runs ?? []);
3639
+ for (const logical of text.split("\n")) wrapBlock(out, cols2, "", "", 0, logical);
3640
+ break;
3641
+ }
3642
+ }
3643
+ }
3644
+ return out;
3645
+ }
3646
+
2673
3647
  // src/wordmill.ts
2674
3648
  var MILL_WORDS = [
2675
3649
  "Working",
@@ -2711,8 +3685,8 @@ var HOLD_MS = 2600;
2711
3685
  var LAZY_MS = 320;
2712
3686
  var LAZY_PERIOD = 200;
2713
3687
  var FAST_PERIOD = 70;
2714
- var BOLD = "\x1B[1m";
2715
- var DIM3 = "\x1B[2m";
3688
+ var BOLD2 = "\x1B[1m";
3689
+ var DIM4 = "\x1B[2m";
2716
3690
  var OFF = "\x1B[22m";
2717
3691
  function glyphAt(slot, bucket) {
2718
3692
  let h = (slot + 1) * 2654435761 ^ (bucket + 1) * 40503;
@@ -2747,7 +3721,7 @@ var WordMill = class {
2747
3721
  if (this.phaseAt === 0) this.phaseAt = now;
2748
3722
  if (!this.target) {
2749
3723
  if (now - this.phaseAt >= HOLD_MS) this.beginMorph(now);
2750
- else return `${BOLD}${this.word}${OFF}`;
3724
+ else return `${BOLD2}${this.word}${OFF}`;
2751
3725
  }
2752
3726
  return this.morphFrame(now);
2753
3727
  }
@@ -2773,21 +3747,21 @@ var WordMill = class {
2773
3747
  this.word = target;
2774
3748
  this.target = null;
2775
3749
  this.phaseAt = now;
2776
- return `${BOLD}${this.word}${OFF}`;
3750
+ return `${BOLD2}${this.word}${OFF}`;
2777
3751
  }
2778
3752
  let out = "";
2779
3753
  for (let i = 0; i < this.slots.length; i++) {
2780
3754
  const s = this.slots[i];
2781
3755
  if (t < s.start) {
2782
3756
  const ch = this.word[i];
2783
- if (ch) out += `${BOLD}${ch}${OFF}`;
3757
+ if (ch) out += `${BOLD2}${ch}${OFF}`;
2784
3758
  } else if (t < s.land) {
2785
3759
  const age = t - s.start;
2786
3760
  const period = age < LAZY_MS ? LAZY_PERIOD : FAST_PERIOD;
2787
- out += `${DIM3}${glyphAt(i, Math.floor(t / period))}${OFF}`;
3761
+ out += `${DIM4}${glyphAt(i, Math.floor(t / period))}${OFF}`;
2788
3762
  } else {
2789
3763
  const ch = target[i];
2790
- if (ch) out += `${BOLD}${ch}${OFF}`;
3764
+ if (ch) out += `${BOLD2}${ch}${OFF}`;
2791
3765
  }
2792
3766
  }
2793
3767
  return out;
@@ -2833,7 +3807,7 @@ function fromFile(filePath) {
2833
3807
  }
2834
3808
  }
2835
3809
  async function readDarwin() {
2836
- const tmp = path3.join(os10.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
3810
+ const tmp = path3.join(os7.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
2837
3811
  const script = [
2838
3812
  `set d to the clipboard as \xABclass PNGf\xBB`,
2839
3813
  `set f to open for access POSIX file "${tmp}" with write permission`,
@@ -2870,7 +3844,7 @@ async function readLinux() {
2870
3844
  return null;
2871
3845
  }
2872
3846
  async function readWindows() {
2873
- const tmp = path3.join(os10.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
3847
+ const tmp = path3.join(os7.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
2874
3848
  const ps = [
2875
3849
  "Add-Type -AssemblyName System.Windows.Forms;",
2876
3850
  "$img = [System.Windows.Forms.Clipboard]::GetImage();",
@@ -2898,6 +3872,11 @@ async function readClipboardImage() {
2898
3872
  function imagePlaceholder(seq) {
2899
3873
  return `[#Image ${seq}]`;
2900
3874
  }
3875
+ function ensureImagePlaceholders(text, images) {
3876
+ const missing = images.map((image) => imagePlaceholder(image.seq)).filter((placeholder) => !text.includes(placeholder));
3877
+ if (!missing.length) return text;
3878
+ return [text.trimEnd(), ...missing].filter(Boolean).join(" ");
3879
+ }
2901
3880
  var INPUT_BOX_MARGIN = 1;
2902
3881
  function inputBoxBorderColor() {
2903
3882
  return "\x1B[38;5;240m";
@@ -3091,8 +4070,13 @@ var C = {
3091
4070
  gray: themeGray,
3092
4071
  teal: "\x1B[38;5;37m"
3093
4072
  };
4073
+ var SYNC_OUTPUT_BEGIN = "\x1B[?2026h";
4074
+ var SYNC_OUTPUT_END = "\x1B[?2026l";
4075
+ var CURSOR_HIDE = "\x1B[?25l";
4076
+ var CURSOR_SHOW = "\x1B[?25h";
3094
4077
  var FRAMES = ["\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
3095
- var SPINNER_MS = 70;
4078
+ var SPINNER_FRAME_MS = 70;
4079
+ var ANIMATION_TICK_MS = 1e3 / 60;
3096
4080
  var SUBAGENT_COLORS = [
3097
4081
  "\x1B[35m",
3098
4082
  // magenta
@@ -3165,6 +4149,7 @@ var Tui = class _Tui {
3165
4149
  // answer text (plain). Bounded to a tail; cleared when the message commits.
3166
4150
  streamThinking = "";
3167
4151
  streamResponse = "";
4152
+ streamDraft = emptyLiveDraft();
3168
4153
  streamMessageId = null;
3169
4154
  // the message currently previewing
3170
4155
  streamRedrawTimer = null;
@@ -3194,7 +4179,7 @@ var Tui = class _Tui {
3194
4179
  // rows re-wrap (a full-width ruler becomes 2+ physical rows when narrowed),
3195
4180
  // so the caret-relative move-up from the last paint is stale. We store the
3196
4181
  // 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
4182
+ // row index so regionTopSequence can recompute physical height under the new
3198
4183
  // wrap. Resize events are debounced — drag-resizing fires dozens of events
3199
4184
  // and redrawing each one desyncs and leaves ghost chrome.
3200
4185
  lastDrawnCols = 0;
@@ -3221,6 +4206,9 @@ var Tui = class _Tui {
3221
4206
  // placeholder at the caret; on submit only images whose placeholder is still
3222
4207
  // present in the text are handed to onSubmit. Cleared with the input.
3223
4208
  pendingImages = [];
4209
+ /** Portable file refs mirrored from another client. Their bytes remain in
4210
+ * the thread filesystem, so the TUI shows names without decoding them. */
4211
+ externalAttachmentNames = [];
3224
4212
  imagePasteBusy = false;
3225
4213
  // one clipboard read at a time
3226
4214
  // Sent-message history for ↑/↓ recall (oldest → newest). `historyIdx` is the
@@ -3234,6 +4222,9 @@ var Tui = class _Tui {
3234
4222
  // event hooks (wired by index.ts)
3235
4223
  onSubmit = () => {
3236
4224
  };
4225
+ /** Shift+Return sends a steering input through the portable messaging endpoint. */
4226
+ onSteer = () => {
4227
+ };
3237
4228
  onInterrupt = () => {
3238
4229
  };
3239
4230
  /** Up on the top row: return true to consume it (e.g. pull a queued message)
@@ -3242,8 +4233,8 @@ var Tui = class _Tui {
3242
4233
  /** Enter while the `[⚙ n bg]` badge is selected — opens the bg process panel. */
3243
4234
  onBgBadge = () => {
3244
4235
  };
3245
- /** Fired (deduped) when the input draft text changes index.ts debounce-writes
3246
- * it into the shared cross-surface presence buffer. */
4236
+ /** Fired (deduped) when the composer changes so index.ts can persist the
4237
+ * shared packed-endpoint draft, including attachments. */
3247
4238
  onDraftChange = () => {
3248
4239
  };
3249
4240
  lastDraftSeen = "";
@@ -3329,7 +4320,7 @@ var Tui = class _Tui {
3329
4320
  dispatch(str, key) {
3330
4321
  const seq = key && key.sequence || str || "";
3331
4322
  if (seq === "\n" || seq === "\x1B[13;2u" || seq === "\x1B[27;2;13~") {
3332
- this.insertAtCursor("\n");
4323
+ if (!this.takeoverHandler && !this.pasting && !this.paletteOpen()) this.submitInput(true);
3333
4324
  return;
3334
4325
  }
3335
4326
  if (key && key.ctrl && key.name === "c") {
@@ -3398,10 +4389,7 @@ var Tui = class _Tui {
3398
4389
  return;
3399
4390
  }
3400
4391
  if (key.name === "return" || key.name === "enter") {
3401
- if (key.shift) {
3402
- this.insertAtCursor("\n");
3403
- return;
3404
- }
4392
+ if (key.shift) return;
3405
4393
  if (matches.length) this.runCommand(matches[cur]);
3406
4394
  return;
3407
4395
  }
@@ -3464,23 +4452,15 @@ var Tui = class _Tui {
3464
4452
  return;
3465
4453
  }
3466
4454
  if (key.name === "return" || key.name === "enter") {
3467
- if (key.shift) {
4455
+ if (key.meta) {
3468
4456
  this.insertAtCursor("\n");
3469
4457
  return;
3470
4458
  }
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);
4459
+ if (key.shift) {
4460
+ this.submitInput(true);
4461
+ return;
3483
4462
  }
4463
+ this.submitInput(false);
3484
4464
  return;
3485
4465
  }
3486
4466
  if (key.ctrl && key.name === "v") {
@@ -3511,6 +4491,23 @@ var Tui = class _Tui {
3511
4491
  this.insertAtCursor(str);
3512
4492
  }
3513
4493
  }
4494
+ submitInput(steer) {
4495
+ const text = this.inputBuffer;
4496
+ const images = this.pendingImages.filter((img) => text.includes(imagePlaceholder(img.seq)));
4497
+ const hasExternalAttachments = this.externalAttachmentNames.length > 0;
4498
+ this.inputBuffer = "";
4499
+ this.cursorPos = 0;
4500
+ this.pendingImages = [];
4501
+ this.externalAttachmentNames = [];
4502
+ this.historyIdx = null;
4503
+ this.historyDraft = "";
4504
+ this.historyDraftImages = [];
4505
+ this.renderBottom();
4506
+ if (!text.trim() && !hasExternalAttachments && images.length === 0) return;
4507
+ if (text.trim()) this.addHistoryEntry(text.trim());
4508
+ if (steer) this.onSteer(text.trim(), images);
4509
+ else this.onSubmit(text.trim(), images);
4510
+ }
3514
4511
  insertAtCursor(text) {
3515
4512
  this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos) + text + this.inputBuffer.slice(this.cursorPos);
3516
4513
  this.cursorPos += text.length;
@@ -3713,7 +4710,7 @@ var Tui = class _Tui {
3713
4710
  }
3714
4711
  spinnerFrame() {
3715
4712
  const now = Date.now();
3716
- return `${C.bold}${brandCycleColor(now)}${FRAMES[Math.floor(now / SPINNER_MS) % FRAMES.length]}${C.reset}`;
4713
+ return `${C.bold}${brandCycleColor(now)}${FRAMES[Math.floor(now / SPINNER_FRAME_MS) % FRAMES.length]}${C.reset}`;
3717
4714
  }
3718
4715
  /** "↑X ↓Y" cumulative token totals (greyed — low-priority). */
3719
4716
  tokensText() {
@@ -3753,7 +4750,7 @@ var Tui = class _Tui {
3753
4750
  }
3754
4751
  /** Is the `/` command palette currently showing? (input starts with "/".) */
3755
4752
  paletteOpen() {
3756
- return this.started && !this.takeoverHandler && this.commands.length > 0 && this.inputBuffer.startsWith("/");
4753
+ return this.started && !this.takeoverHandler && this.commands.length > 0 && this.externalAttachmentNames.length === 0 && this.inputBuffer.startsWith("/");
3757
4754
  }
3758
4755
  /** Commands matching the text typed after "/", in declared order. */
3759
4756
  filteredCommands() {
@@ -3812,9 +4809,10 @@ var Tui = class _Tui {
3812
4809
  /** The prompt line prefix (with ANSI colour) that precedes the typed text. */
3813
4810
  promptPrefix() {
3814
4811
  const q = this.queuedCount > 0 ? `${C.yellow}[\u23F3 ${this.queuedCount} queued]${C.reset} ` : "";
4812
+ const attachments2 = this.externalAttachmentNames.length > 0 ? `${C.gray}[\u{1F4CE} ${this.externalAttachmentNames.length}]${C.reset} ` : "";
3815
4813
  const bgText = `[\u2699 ${this.bgCount} bg]`;
3816
4814
  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} `;
4815
+ return `${q}${attachments2}${bg}${this.levelColor()}\u276F${C.reset} `;
3818
4816
  }
3819
4817
  visibleWidth(s) {
3820
4818
  let w = 0;
@@ -3889,20 +4887,21 @@ var Tui = class _Tui {
3889
4887
  if (matches.length === 0) return [` ${C.gray}no matching command${C.reset}`];
3890
4888
  const cur = Math.min(this.slashIdx, matches.length - 1);
3891
4889
  const pointerW = 2;
4890
+ const rowCols = Math.max(1, cols2 - 1);
3892
4891
  return matches.map((cmd, i) => {
3893
4892
  const sel = i === cur;
3894
4893
  const hint = (typeof cmd.hint === "function" ? cmd.hint() : cmd.hint) ?? "";
3895
4894
  const hintW = hint.length;
3896
4895
  const name = `/${cmd.name}`;
3897
4896
  let visible = `${name} ${cmd.label}`;
3898
- const labelMax = Math.max(6, cols2 - pointerW - (hintW ? hintW + 2 : 0));
4897
+ const labelMax = Math.max(6, rowCols - pointerW - (hintW ? hintW + 2 : 0));
3899
4898
  if (visible.length > labelMax) visible = visible.slice(0, labelMax - 1) + "\u2026";
3900
4899
  const desc = visible.slice(name.length);
3901
4900
  const pointer = sel ? `${C.magenta}\u276F${C.reset} ` : " ";
3902
4901
  const nameStyled = sel ? `${C.bold}${C.cyan}${name}${C.reset}` : `${C.cyan}${name}${C.reset}`;
3903
4902
  let line = `${pointer}${nameStyled}${C.gray}${desc}${C.reset}`;
3904
4903
  if (hintW) {
3905
- const gap = Math.max(2, cols2 - pointerW - visible.length - hintW);
4904
+ const gap = Math.max(2, rowCols - pointerW - visible.length - hintW);
3906
4905
  line += `${" ".repeat(gap)}${C.gray}${hint}${C.reset}`;
3907
4906
  }
3908
4907
  return line;
@@ -3924,9 +4923,9 @@ var Tui = class _Tui {
3924
4923
  * the NEW wrap: rows above the caret row + (caret row's rewrap − 1) so we
3925
4924
  * prefer a slight over-move (clean wipe) over under-move (ghost chrome).
3926
4925
  */
3927
- moveToRegionTop() {
3928
- process.stdout.write("\r");
3929
- if (!this.bottomDrawn) return;
4926
+ regionTopSequence() {
4927
+ let sequence = "\r";
4928
+ if (!this.bottomDrawn) return sequence;
3930
4929
  const cols2 = process.stdout.columns || 80;
3931
4930
  let up;
3932
4931
  if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0 && this.drawnRegionWidths.length) {
@@ -3943,7 +4942,8 @@ var Tui = class _Tui {
3943
4942
  } else {
3944
4943
  up = this.lastCursorRow;
3945
4944
  }
3946
- if (up > 0) process.stdout.write(`\x1B[${up}A`);
4945
+ if (up > 0) sequence += `\x1B[${up}A`;
4946
+ return sequence;
3947
4947
  }
3948
4948
  /**
3949
4949
  * Render the bottom region: status + subagents above a side-margined expanding
@@ -3959,14 +4959,15 @@ var Tui = class _Tui {
3959
4959
  if (!this.started || this.takeoverHandler) return;
3960
4960
  if (this.inputBuffer !== this.lastDraftSeen) {
3961
4961
  this.lastDraftSeen = this.inputBuffer;
4962
+ const images = this.pendingImages.filter((img) => this.inputBuffer.includes(imagePlaceholder(img.seq)));
3962
4963
  try {
3963
- this.onDraftChange(this.inputBuffer);
4964
+ this.onDraftChange(this.inputBuffer, images);
3964
4965
  } catch {
3965
4966
  }
3966
4967
  }
3967
4968
  if (this.resizePending) return;
3968
4969
  const cols2 = process.stdout.columns || 80;
3969
- this.moveToRegionTop();
4970
+ const moveToTop = this.regionTopSequence();
3970
4971
  const hudWidths = [];
3971
4972
  const hudRows = [];
3972
4973
  const rowCap = Math.max(1, cols2 - 1);
@@ -3987,7 +4988,7 @@ var Tui = class _Tui {
3987
4988
  if (quitLine) writeHudRow(workPad + quitLine);
3988
4989
  const statusLine = this.statusLineText(workCols);
3989
4990
  if (statusLine) writeHudRow(workPad + statusLine);
3990
- const frame = FRAMES[Math.floor(Date.now() / SPINNER_MS) % FRAMES.length];
4991
+ const frame = FRAMES[Math.floor(Date.now() / SPINNER_FRAME_MS) % FRAMES.length];
3991
4992
  for (const sub of this.subagents) {
3992
4993
  const color = sub.agentName === COMPACTION_AGENT ? COMPACTION_COLOR : SUBAGENT_COLORS[this.subagentColorByID.get(sub.id) ?? 0];
3993
4994
  const budget = workCols - 10;
@@ -4035,18 +5036,21 @@ var Tui = class _Tui {
4035
5036
  this.bottomDrawn = true;
4036
5037
  const totalRows = hudRows.length;
4037
5038
  const up = Math.max(0, totalRows - 1 - caretRegionRow);
4038
- let out = "\x1B[J" + hudRows.join("\r\n");
5039
+ let out = SYNC_OUTPUT_BEGIN + moveToTop + "\x1B[J" + hudRows.join("\r\n");
4039
5040
  if (totalRows > 0) {
4040
5041
  out += "\r";
4041
5042
  if (up > 0) out += `\x1B[${up}A`;
4042
5043
  if (caretScreenCol > 0) out += `\x1B[${caretScreenCol}C`;
4043
5044
  }
5045
+ out += SYNC_OUTPUT_END;
4044
5046
  process.stdout.write(out);
4045
5047
  }
4046
5048
  clearBottom() {
4047
5049
  if (!this.bottomDrawn) return;
4048
- this.moveToRegionTop();
4049
- process.stdout.write("\x1B[J");
5050
+ const moveToTop = this.regionTopSequence();
5051
+ process.stdout.write(
5052
+ SYNC_OUTPUT_BEGIN + CURSOR_HIDE + moveToTop + "\x1B[J" + CURSOR_SHOW + SYNC_OUTPUT_END
5053
+ );
4050
5054
  this.bottomDrawn = false;
4051
5055
  }
4052
5056
  // ── live streaming preview ────────────────────────────────────────────────
@@ -4055,15 +5059,15 @@ var Tui = class _Tui {
4055
5059
  // wipes it right before the finished message is committed to the transcript
4056
5060
  // (which renders full markdown), so there's no double-render.
4057
5061
  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.
5062
+ // How long a reasoning-only preview may sit untouched before it's wiped. A
5063
+ // visible answer must NEVER expire: it is the only copy until polling
5064
+ // promotes the durable message into terminal scrollback.
4062
5065
  static STREAM_IDLE_MS = 1e4;
4063
- /** Append a fragment of streamed answer text (rendered plain). */
5066
+ /** Append a fragment of streamed answer text (rendered as progressive Markdown). */
4064
5067
  streamResponseDelta(delta, messageId) {
4065
5068
  this.beginStreamMessage(messageId);
4066
- this.streamResponse += delta;
5069
+ this.streamDraft = appendLiveDraft(this.streamDraft, { text: delta, messageId });
5070
+ this.streamResponse = this.streamDraft.text;
4067
5071
  this.scheduleStreamRedraw();
4068
5072
  this.armStreamIdleExpiry();
4069
5073
  }
@@ -4083,8 +5087,7 @@ var Tui = class _Tui {
4083
5087
  beginStreamMessage(messageId) {
4084
5088
  if (messageId !== void 0 && messageId !== this.streamMessageId) {
4085
5089
  this.streamMessageId = messageId;
4086
- this.streamThinking = "";
4087
- this.streamResponse = "";
5090
+ if (!this.streamResponse) this.streamThinking = "";
4088
5091
  }
4089
5092
  }
4090
5093
  /** Wipe the live preview — call right before committing the final message. */
@@ -4098,6 +5101,7 @@ var Tui = class _Tui {
4098
5101
  this.streamIdleTimer = null;
4099
5102
  }
4100
5103
  this.streamMessageId = null;
5104
+ this.streamDraft = emptyLiveDraft();
4101
5105
  if (!this.streamThinking && !this.streamResponse) return;
4102
5106
  this.streamThinking = "";
4103
5107
  this.streamResponse = "";
@@ -4110,17 +5114,16 @@ var Tui = class _Tui {
4110
5114
  this.renderBottom();
4111
5115
  }, 40);
4112
5116
  }
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. */
5117
+ /** Re-armed for reasoning-only deltas. Once answer prose exists it remains
5118
+ * visible until clearStream() runs immediately before durable promotion. */
4117
5119
  armStreamIdleExpiry() {
4118
5120
  if (this.streamIdleTimer) clearTimeout(this.streamIdleTimer);
5121
+ this.streamIdleTimer = null;
5122
+ if (this.streamResponse) return;
4119
5123
  this.streamIdleTimer = setTimeout(() => {
4120
5124
  this.streamIdleTimer = null;
4121
- if (!this.streamThinking && !this.streamResponse) return;
5125
+ if (this.streamResponse || !this.streamThinking) return;
4122
5126
  this.streamThinking = "";
4123
- this.streamResponse = "";
4124
5127
  this.renderBottom();
4125
5128
  }, _Tui.STREAM_IDLE_MS);
4126
5129
  }
@@ -4136,23 +5139,28 @@ var Tui = class _Tui {
4136
5139
  */
4137
5140
  streamPreviewLines(cols2) {
4138
5141
  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}`;
4143
- };
4144
5142
  const leakedMarkup = /<\/?[||]DSML[||]|^\s*\[\/?SESSION\]\s*<?\s*$/;
4145
- const realLines = (text) => text.replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "" && !leakedMarkup.test(l));
5143
+ const renderedLines = (text) => {
5144
+ const clean2 = text.replace(/\r/g, "").split("\n").filter((line) => !leakedMarkup.test(line)).join("\n");
5145
+ const rows = [];
5146
+ for (const line of renderStreamingMarkdown(clean2, Math.max(8, cols2 - 2))) {
5147
+ const blank = line.replace(/\x1b\[[0-9;]*m/g, "").trim() === "";
5148
+ if (blank && (!rows.length || rows[rows.length - 1] === "")) continue;
5149
+ rows.push(blank ? "" : line);
5150
+ }
5151
+ while (rows.length && rows[rows.length - 1] === "") rows.pop();
5152
+ return rows;
5153
+ };
4146
5154
  if (this.streamResponse) {
4147
- const all = realLines(this.streamResponse);
5155
+ const all = renderedLines(this.streamResponse);
4148
5156
  const shown = all.slice(-20);
4149
5157
  const firstVisible = all.length <= _Tui.STREAM_TAIL;
4150
5158
  return shown.map(
4151
- (l, i) => clamp2(l, "", i === 0 && firstVisible ? `${C.gray}\u2022${C.reset} ` : " ")
5159
+ (l, i) => `${i === 0 && firstVisible ? `${C.gray}\u2022${C.reset} ` : " "}${l}`
4152
5160
  );
4153
5161
  }
4154
5162
  if (this.streamThinking) {
4155
- return realLines(this.streamThinking).slice(-20).map((l) => clamp2(l, thinkStyle, " "));
5163
+ return renderedLines(this.streamThinking).slice(-20).map((l) => ` ${thinkStyle}${l.replace(/\x1b\[0m/g, `${C.reset}${thinkStyle}`)}${C.reset}`);
4156
5164
  }
4157
5165
  return [];
4158
5166
  }
@@ -4280,6 +5288,7 @@ var Tui = class _Tui {
4280
5288
  }
4281
5289
  // ─── working indicator (turn state) ───────────────────────────────────────
4282
5290
  setWorking(on) {
5291
+ if (on === this.working) return;
4283
5292
  if (on && !this.working) {
4284
5293
  this.working = true;
4285
5294
  this.workingStart = Date.now();
@@ -4295,6 +5304,11 @@ var Tui = class _Tui {
4295
5304
  * stable, distinct colour for as long as it's active; the compaction agent
4296
5305
  * is always orange (its colour never comes from the shared pool). */
4297
5306
  setSubagents(subagents) {
5307
+ const unchanged = subagents.length === this.subagents.length && subagents.every((sub, i) => {
5308
+ const current = this.subagents[i];
5309
+ return current?.id === sub.id && current.label === sub.label && current.agentName === sub.agentName;
5310
+ });
5311
+ if (unchanged) return;
4298
5312
  this.subagents = subagents;
4299
5313
  const active = new Set(subagents.map((s) => s.id));
4300
5314
  for (const id of [...this.subagentColorByID.keys()]) {
@@ -4315,7 +5329,7 @@ var Tui = class _Tui {
4315
5329
  syncSpinner() {
4316
5330
  const spinning = this.working || this.subagents.length > 0;
4317
5331
  if (spinning && !this.spinnerTimer) {
4318
- this.spinnerTimer = setInterval(() => this.renderBottom(), SPINNER_MS);
5332
+ this.spinnerTimer = setInterval(() => this.renderBottom(), ANIMATION_TICK_MS);
4319
5333
  } else if (!spinning && this.spinnerTimer) {
4320
5334
  clearInterval(this.spinnerTimer);
4321
5335
  this.spinnerTimer = null;
@@ -4325,15 +5339,18 @@ var Tui = class _Tui {
4325
5339
  return this.working;
4326
5340
  }
4327
5341
  setBackgroundCount(n) {
5342
+ if (n === this.bgCount) return;
4328
5343
  this.bgCount = n;
4329
5344
  if (n === 0) this.bgBadgeSelected = false;
4330
5345
  this.renderBottom();
4331
5346
  }
4332
5347
  setQueuedCount(n) {
5348
+ if (n === this.queuedCount) return;
4333
5349
  this.queuedCount = n;
4334
5350
  this.renderBottom();
4335
5351
  }
4336
5352
  setConnected(connected) {
5353
+ if (connected === this.connected) return;
4337
5354
  this.connected = connected;
4338
5355
  this.renderBottom();
4339
5356
  }
@@ -4342,15 +5359,25 @@ var Tui = class _Tui {
4342
5359
  return this.inputBuffer;
4343
5360
  }
4344
5361
  /** 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;
5362
+ setInput(text, images = [], notifyDraft = true, restoreExternalImages = false) {
5363
+ this.inputBuffer = restoreExternalImages ? ensureImagePlaceholders(text, images) : text;
5364
+ this.cursorPos = this.inputBuffer.length;
4348
5365
  this.pendingImages = images;
4349
5366
  this.historyIdx = null;
5367
+ if (!notifyDraft) this.lastDraftSeen = this.inputBuffer;
4350
5368
  this.renderBottom();
4351
5369
  }
5370
+ setExternalAttachmentNames(names) {
5371
+ if (names.length === this.externalAttachmentNames.length && names.every((name, i) => name === this.externalAttachmentNames[i])) return;
5372
+ this.externalAttachmentNames = [...names];
5373
+ this.renderBottom();
5374
+ }
5375
+ hasExternalAttachments() {
5376
+ return this.externalAttachmentNames.length > 0;
5377
+ }
4352
5378
  /** Cumulative token totals shown on the prompt line (`outTokens` includes live). */
4353
5379
  setTokens(inTokens, outTokens) {
5380
+ if (inTokens === this.tokensIn && outTokens === this.tokensOut) return;
4354
5381
  this.tokensIn = inTokens;
4355
5382
  this.tokensOut = outTokens;
4356
5383
  this.renderBottom();
@@ -4362,6 +5389,7 @@ var Tui = class _Tui {
4362
5389
  */
4363
5390
  setStep(label, outTokens) {
4364
5391
  const next = label && label.trim() ? label.replace(/\s+/g, " ").trim() : null;
5392
+ if (next === this.step && outTokens === this.stepOut) return;
4365
5393
  if (next !== this.step) {
4366
5394
  this.step = next;
4367
5395
  this.stepStart = Date.now();
@@ -4641,7 +5669,7 @@ var Tui = class _Tui {
4641
5669
  */
4642
5670
  select(title, items) {
4643
5671
  return new Promise((resolve) => {
4644
- let idx = 0;
5672
+ let idx = Math.max(0, items.findIndex((it) => !it.disabled));
4645
5673
  this.beginTakeover();
4646
5674
  const cols2 = process.stdout.columns || 80;
4647
5675
  const geo = inputBoxGeometry(cols2);
@@ -4662,7 +5690,7 @@ ${pad}${title}
4662
5690
  let label = it.label.replace(/\s+/g, " ").trim();
4663
5691
  if (label.length > labelMax) label = label.slice(0, Math.max(0, labelMax - 1)) + "\u2026";
4664
5692
  const pointer = sel ? `${C.magenta}\u276F${C.reset} ` : " ";
4665
- const styledLabel = sel ? `${C.bold}${C.cyan}${label}${C.reset}` : `${C.dim}${label}${C.reset}`;
5693
+ const styledLabel = it.disabled ? `${C.gray}${label}${C.reset}` : sel ? `${C.bold}${C.cyan}${label}${C.reset}` : `${C.dim}${label}${C.reset}`;
4666
5694
  let content = `${pointer}${styledLabel}`;
4667
5695
  if (hintW) {
4668
5696
  const used = pointerW + label.length;
@@ -4707,7 +5735,7 @@ ${pad}${title}
4707
5735
  idx = (idx + 1) % items.length;
4708
5736
  draw(true);
4709
5737
  } else if (key.name === "return" || key.name === "enter") {
4710
- close(items[idx].value);
5738
+ if (!items[idx].disabled) close(items[idx].value);
4711
5739
  } else if (key.name === "escape") {
4712
5740
  close(void 0);
4713
5741
  }
@@ -4744,212 +5772,50 @@ ${C.cyan}\u2503${C.reset} ${question}
4744
5772
  if (key?.name === "backspace") {
4745
5773
  buf = buf.slice(0, -1);
4746
5774
  draw();
4747
- return;
4748
- }
4749
- if (str && !key?.ctrl && !key?.meta && str >= " ") {
4750
- buf += str;
4751
- draw();
4752
- }
4753
- };
4754
- });
4755
- }
4756
- banner(lines) {
4757
- this.clearBottom();
4758
- process.stdout.write("\n");
4759
- for (const l of lines) process.stdout.write(l + "\n");
4760
- }
4761
- };
4762
-
4763
- // src/history.ts
4764
- var HISTORY_KEY = "input_history";
4765
- var MAX_ENTRIES = 100;
4766
- function clean(value) {
4767
- if (!Array.isArray(value)) return [];
4768
- return value.filter((e) => typeof e === "string" && e.trim() !== "").slice(-MAX_ENTRIES);
4769
- }
4770
- async function loadHistory(store, threadId, seedThreadId) {
4771
- const own = clean(await store.kvGet(threadId, HISTORY_KEY));
4772
- if (own.length) return own;
4773
- if (seedThreadId && seedThreadId !== threadId) {
4774
- const seeded = clean(await store.kvGet(seedThreadId, HISTORY_KEY));
4775
- if (seeded.length) {
4776
- void store.kvSet(threadId, HISTORY_KEY, seeded);
4777
- return seeded;
4778
- }
4779
- }
4780
- return [];
4781
- }
4782
- function appendHistory(store, threadId, history, text) {
4783
- const t = text.trim();
4784
- if (!t || history[history.length - 1] === t) return history;
4785
- history.push(t);
4786
- if (history.length > MAX_ENTRIES) history.splice(0, history.length - MAX_ENTRIES);
4787
- void store.kvSet(threadId, HISTORY_KEY, [...history]);
4788
- return history;
4789
- }
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] ?? ""))));
5775
+ return;
5776
+ }
5777
+ if (str && !key?.ctrl && !key?.meta && str >= " ") {
5778
+ buf += str;
5779
+ draw();
5780
+ }
5781
+ };
5782
+ });
4867
5783
  }
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;
5784
+ banner(lines) {
5785
+ this.clearBottom();
5786
+ process.stdout.write("\n");
5787
+ for (const l of lines) process.stdout.write(l + "\n");
5788
+ }
5789
+ };
5790
+
5791
+ // src/history.ts
5792
+ var HISTORY_KEY = "input_history";
5793
+ var MAX_ENTRIES = 100;
5794
+ function clean(value) {
5795
+ if (!Array.isArray(value)) return [];
5796
+ return value.filter((e) => typeof e === "string" && e.trim() !== "").slice(-MAX_ENTRIES);
4884
5797
  }
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;
5798
+ async function loadHistory(store, threadId, seedThreadId) {
5799
+ const own = clean(await store.kvGet(threadId, HISTORY_KEY));
5800
+ if (own.length) return own;
5801
+ if (seedThreadId && seedThreadId !== threadId) {
5802
+ const seeded = clean(await store.kvGet(seedThreadId, HISTORY_KEY));
5803
+ if (seeded.length) {
5804
+ void store.kvSet(threadId, HISTORY_KEY, seeded);
5805
+ return seeded;
4945
5806
  }
4946
- if (line.trim()) wrapBlock(out, cols2, "", "", 0, inline(line));
4947
- else out.push("");
4948
- i++;
4949
5807
  }
4950
- return out;
5808
+ return [];
5809
+ }
5810
+ function appendHistory(store, threadId, history, text) {
5811
+ const t = text.trim();
5812
+ if (!t || history[history.length - 1] === t) return history;
5813
+ history.push(t);
5814
+ if (history.length > MAX_ENTRIES) history.splice(0, history.length - MAX_ENTRIES);
5815
+ void store.kvSet(threadId, HISTORY_KEY, [...history]);
5816
+ return history;
4951
5817
  }
4952
- var DIR = path3.join(os10.homedir(), ".standardagents");
5818
+ var DIR = path3.join(os7.homedir(), ".standardagents");
4953
5819
  var FILE = path3.join(DIR, "credentials");
4954
5820
  function normalizeEndpoint(endpoint) {
4955
5821
  let e = endpoint.trim();
@@ -5054,6 +5920,13 @@ var AGENT_ID_VARIANTS = [
5054
5920
  "standard_code_low_agent",
5055
5921
  "standard_code_high_agent"
5056
5922
  ];
5923
+ var OPENSAMA_AGENT_ID = "opensama_agent";
5924
+ var AGENT_CHOICES = [
5925
+ { id: "frontier_one", title: "Frontier One", description: "coming soon", disabled: true },
5926
+ { id: AGENT_ID, title: "Unlimited One", description: "adaptive high/low routing" },
5927
+ { id: OPENSAMA_AGENT_ID, title: "Sama One", description: "ChatGPT Pro sign-in required" },
5928
+ { id: "dario", title: "Dario", description: "unavailable", disabled: true }
5929
+ ];
5057
5930
  var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
5058
5931
  function readVersion() {
5059
5932
  try {
@@ -5083,7 +5956,7 @@ function relaxTlsForLocalEndpoint(endpoint) {
5083
5956
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
5084
5957
  return true;
5085
5958
  }
5086
- var dir = () => path3.join(os10.homedir(), ".standardagents");
5959
+ var dir = () => path3.join(os7.homedir(), ".standardagents");
5087
5960
  var file = () => path3.join(dir(), "machine.json");
5088
5961
  function loadMachineIdentity() {
5089
5962
  try {
@@ -5153,20 +6026,55 @@ function parseFsRequest(value) {
5153
6026
  requested_at: typeof r.requested_at === "number" ? r.requested_at : Date.now()
5154
6027
  };
5155
6028
  }
6029
+ async function writeFsRequest(api, machineId, req) {
6030
+ await api.userKvSet(fsRequestKey(machineId), { ...req, requested_at: Date.now() });
6031
+ }
6032
+ async function readFsResponse(api, machineId) {
6033
+ const value = await api.userKvGet(fsResponseKey(machineId));
6034
+ if (!value || typeof value !== "object") return null;
6035
+ const r = value;
6036
+ if (typeof r.nonce !== "string" || typeof r.ok !== "boolean") return null;
6037
+ return {
6038
+ nonce: r.nonce,
6039
+ ok: r.ok,
6040
+ result: r.result,
6041
+ error: typeof r.error === "string" ? r.error : void 0,
6042
+ responded_at: typeof r.responded_at === "number" ? r.responded_at : 0
6043
+ };
6044
+ }
5156
6045
  async function readFsRequest(api, machineId) {
5157
6046
  return parseFsRequest(await api.userKvGet(fsRequestKey(machineId)));
5158
6047
  }
5159
6048
  async function writeFsResponse(api, machineId, res) {
5160
6049
  await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
5161
6050
  }
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";
6051
+ async function resolveRemoteProjectPath(api, machineId, typedPath, timeoutMs = 12e3) {
6052
+ const trimmed = typedPath.trim();
6053
+ if (!trimmed.startsWith("~")) return trimmed;
6054
+ const nonce = crypto.randomBytes(8).toString("hex");
6055
+ try {
6056
+ await writeFsRequest(api, machineId, { nonce, op: "list", path: trimmed });
6057
+ const deadline = Date.now() + timeoutMs;
6058
+ while (Date.now() < deadline) {
6059
+ await new Promise((r) => setTimeout(r, 700));
6060
+ const res = await readFsResponse(api, machineId).catch(() => null);
6061
+ if (res && res.nonce === nonce) {
6062
+ const resolved = res.result && typeof res.result.path === "string" ? res.result.path : "";
6063
+ return resolved || trimmed;
6064
+ }
6065
+ }
6066
+ } catch {
6067
+ }
6068
+ return trimmed;
6069
+ }
6070
+ function machineIcon(record2) {
6071
+ if (record2.icon && record2.icon.trim()) return record2.icon.trim();
6072
+ if (record2.platform === "darwin") return "\u{1F4BB}";
6073
+ if (record2.platform === "win32") return "\u{1F5A5}\uFE0F";
5166
6074
  return "\u{1F5B3}";
5167
6075
  }
5168
- function machineDisplayName(record) {
5169
- return record.name?.trim() || "" || (record.hostname || "") || (record.id || "") || "machine";
6076
+ function machineDisplayName(record2) {
6077
+ return record2.name?.trim() || "" || (record2.hostname || "") || (record2.id || "") || "machine";
5170
6078
  }
5171
6079
  async function getMachineName(api, machineId) {
5172
6080
  const v = await api.userKvGet(nameKey(machineId));
@@ -5241,15 +6149,15 @@ async function loadMachine(api, machineId) {
5241
6149
  if (icon) rec.icon = icon;
5242
6150
  return rec;
5243
6151
  }
5244
- function daemonOnline(record, now = Date.now()) {
5245
- return !!record.daemon && now - record.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
6152
+ function daemonOnline(record2, now = Date.now()) {
6153
+ return !!record2.daemon && now - record2.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
5246
6154
  }
5247
6155
  function newRecord(identity) {
5248
6156
  const now = Date.now();
5249
6157
  return {
5250
6158
  id: identity.machine_id,
5251
- name: os10.hostname(),
5252
- hostname: os10.hostname(),
6159
+ name: os7.hostname(),
6160
+ hostname: os7.hostname(),
5253
6161
  platform: process.platform,
5254
6162
  arch: process.arch,
5255
6163
  version: readVersion() || void 0,
@@ -5261,15 +6169,15 @@ function newRecord(identity) {
5261
6169
  }
5262
6170
  async function updateOwnMachineRecord(api, identity, mutate) {
5263
6171
  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;
6172
+ const record2 = existing ?? newRecord(identity);
6173
+ record2.hostname = os7.hostname();
6174
+ record2.platform = process.platform;
6175
+ record2.arch = process.arch;
6176
+ record2.version = readVersion() || record2.version;
6177
+ mutate?.(record2);
6178
+ record2.updated_at = Date.now();
6179
+ await api.userKvSet(machineKey(identity.machine_id), record2);
6180
+ return record2;
5273
6181
  }
5274
6182
  function projectRepository(projectDir) {
5275
6183
  try {
@@ -5283,35 +6191,54 @@ function projectRepository(projectDir) {
5283
6191
  return null;
5284
6192
  }
5285
6193
  }
6194
+ function normalizeProjectDir(projectDir) {
6195
+ let p = projectDir.trim();
6196
+ if (!p) return p;
6197
+ if (p === "~") p = os7.homedir();
6198
+ else if (p.startsWith("~/")) p = path3.join(os7.homedir(), p.slice(2));
6199
+ return path3.resolve(p);
6200
+ }
5286
6201
  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,
6202
+ const dir2 = normalizeProjectDir(projectDir);
6203
+ if (!dir2) return;
6204
+ const repository = projectRepository(dir2);
6205
+ await updateOwnMachineRecord(api, identity, (record2) => {
6206
+ for (const existing of Object.keys(record2.projects)) {
6207
+ if (existing !== dir2 && normalizeProjectDir(existing) === dir2) {
6208
+ delete record2.projects[existing];
6209
+ }
6210
+ }
6211
+ record2.projects[dir2] = {
6212
+ name: dir2.split("/").filter(Boolean).pop() || dir2,
5291
6213
  last_used_at: Date.now(),
5292
6214
  repository
5293
6215
  };
5294
6216
  });
5295
6217
  }
5296
6218
  async function unregisterProject(api, identity, projectDir) {
5297
- await updateOwnMachineRecord(api, identity, (record) => {
5298
- delete record.projects[projectDir];
6219
+ const dir2 = normalizeProjectDir(projectDir);
6220
+ await updateOwnMachineRecord(api, identity, (record2) => {
6221
+ for (const existing of Object.keys(record2.projects)) {
6222
+ if (existing === projectDir || existing === dir2 || normalizeProjectDir(existing) === dir2) {
6223
+ delete record2.projects[existing];
6224
+ }
6225
+ }
5299
6226
  });
5300
6227
  }
5301
6228
  async function touchDaemon(api, identity, version) {
5302
- await updateOwnMachineRecord(api, identity, (record) => {
6229
+ await updateOwnMachineRecord(api, identity, (record2) => {
5303
6230
  const now = Date.now();
5304
- record.daemon = {
6231
+ record2.daemon = {
5305
6232
  version,
5306
- installed_at: record.daemon?.installed_at ?? now,
6233
+ installed_at: record2.daemon?.installed_at ?? now,
5307
6234
  last_seen_at: now,
5308
6235
  pid: process.pid
5309
6236
  };
5310
6237
  });
5311
6238
  }
5312
6239
  async function clearDaemon(api, identity) {
5313
- await updateOwnMachineRecord(api, identity, (record) => {
5314
- record.daemon = null;
6240
+ await updateOwnMachineRecord(api, identity, (record2) => {
6241
+ record2.daemon = null;
5315
6242
  });
5316
6243
  }
5317
6244
  function parseCommands(value) {
@@ -5347,16 +6274,16 @@ async function clearMachineCommands(api, machineId, appliedIds) {
5347
6274
  async function applyMachineCommand(api, identity, cmd) {
5348
6275
  switch (cmd.kind) {
5349
6276
  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}`;
6277
+ const path14 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
6278
+ if (!path14) return "add_project: ignored (no path)";
6279
+ await registerProject(api, identity, path14);
6280
+ return `added project ${path14}`;
5354
6281
  }
5355
6282
  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}`;
6283
+ const path14 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
6284
+ if (!path14) return "remove_project: ignored (no path)";
6285
+ await unregisterProject(api, identity, path14);
6286
+ return `removed project ${path14}`;
5360
6287
  }
5361
6288
  case "update":
5362
6289
  return "update requested";
@@ -5433,7 +6360,7 @@ var PROJECT_MARKERS = [
5433
6360
  ];
5434
6361
  var MAX_ENTRIES2 = 500;
5435
6362
  function resolveBrowsePath(input3) {
5436
- const home = os10.homedir();
6363
+ const home = os7.homedir();
5437
6364
  let p = (input3 ?? "").trim();
5438
6365
  if (!p) return home;
5439
6366
  if (p === "~") return home;
@@ -5458,7 +6385,7 @@ function markers(dirPath) {
5458
6385
  return { project, repo };
5459
6386
  }
5460
6387
  function browseDirectory(input3, opts = {}) {
5461
- const home = os10.homedir();
6388
+ const home = os7.homedir();
5462
6389
  const abs = resolveBrowsePath(input3);
5463
6390
  const parent = path3.dirname(abs);
5464
6391
  const base = {
@@ -5712,7 +6639,7 @@ var UPDATE_CHECK_MS = 6 * 60 * 6e4;
5712
6639
  var SWEEP_MS = 10 * 6e4;
5713
6640
  var MAX_WORKERS = 30;
5714
6641
  var LOG_MAX_BYTES = 1e6;
5715
- var LOG_FILE = path3.join(os10.homedir(), ".standardagents", "daemon.log");
6642
+ var LOG_FILE = path3.join(os7.homedir(), ".standardagents", "daemon.log");
5716
6643
  function daemonLog(line) {
5717
6644
  try {
5718
6645
  fs4.mkdirSync(path3.dirname(LOG_FILE), { recursive: true });
@@ -5731,13 +6658,13 @@ function pathFromTags(tags) {
5731
6658
  const tag = tags.find((t) => t.startsWith("path:"));
5732
6659
  if (!tag) return null;
5733
6660
  const raw = tag.slice("path:".length);
5734
- return raw.replace(/^~(?=\/|$)/, os10.homedir());
6661
+ return raw.replace(/^~(?=\/|$)/, os7.homedir());
5735
6662
  }
5736
6663
  var ThreadWorker = class {
5737
- constructor(api, identity, machineName, threadId, projectDir, createdAt) {
6664
+ constructor(api, identity, machineName, threadId, projectDir, createdAt2) {
5738
6665
  this.threadId = threadId;
5739
6666
  this.projectDir = projectDir;
5740
- this.createdAt = createdAt;
6667
+ this.createdAt = createdAt2;
5741
6668
  this.perm = { level: 1, alwaysAllow: /* @__PURE__ */ new Set(), allowRisk: /* @__PURE__ */ new Set() };
5742
6669
  this.session = new ExecutionSession({
5743
6670
  api,
@@ -5863,7 +6790,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5863
6790
  process.exit(1);
5864
6791
  }
5865
6792
  let displayName = machineDisplayName(await loadMachine(api, identity.machine_id).catch(() => null) ?? {
5866
- hostname: os10.hostname(),
6793
+ hostname: os7.hostname(),
5867
6794
  id: identity.machine_id
5868
6795
  });
5869
6796
  const applied = consumeAppliedUpdate(version);
@@ -5877,8 +6804,19 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5877
6804
  process.on("unhandledRejection", (err) => {
5878
6805
  daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
5879
6806
  });
6807
+ await touchDaemon(api, identity, version).catch(
6808
+ (e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
6809
+ );
6810
+ const heartbeat = setInterval(() => {
6811
+ void touchDaemon(api, identity, version).catch(() => {
6812
+ });
6813
+ void getMachineName(api, identity.machine_id).then((n) => {
6814
+ if (n) displayName = n;
6815
+ }).catch(() => {
6816
+ });
6817
+ }, HEARTBEAT_MS);
5880
6818
  const workers = /* @__PURE__ */ new Map();
5881
- const attach = async (threadId, tags, createdAt = 0) => {
6819
+ const attach = async (threadId, tags, createdAt2 = 0) => {
5882
6820
  if (workers.has(threadId)) return;
5883
6821
  const projectDir = pathFromTags(tags);
5884
6822
  if (!projectDir) return;
@@ -5895,7 +6833,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5895
6833
  daemonLog(`cannot prepare project dir ${projectDir}: ${e instanceof Error ? e.message : String(e)}`);
5896
6834
  return;
5897
6835
  }
5898
- const worker = new ThreadWorker(api, identity, displayName, threadId, projectDir, createdAt || Date.now());
6836
+ const worker = new ThreadWorker(api, identity, displayName, threadId, projectDir, createdAt2 || Date.now());
5899
6837
  worker.onEvicted = (id) => detach(id);
5900
6838
  workers.set(threadId, worker);
5901
6839
  daemonLog(`attached ${threadId.slice(0, 8)} \u2192 ${projectDir}`);
@@ -5912,7 +6850,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5912
6850
  };
5913
6851
  const sweep = async () => {
5914
6852
  try {
5915
- const threads = await api.listThreads(AGENT_ID_VARIANTS, [runnerTag]);
6853
+ const threads = await api.listThreads([...AGENT_ID_VARIANTS, OPENSAMA_AGENT_ID], [runnerTag]);
5916
6854
  const recent = threads.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)).slice(0, MAX_WORKERS);
5917
6855
  for (const t of recent) {
5918
6856
  await attach(t.id, t.tags, (t.created_at ?? 0) * 1e3);
@@ -5934,17 +6872,6 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
5934
6872
  });
5935
6873
  events.connect();
5936
6874
  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
6875
  const reclaim = setInterval(() => {
5949
6876
  for (const worker of workers.values()) {
5950
6877
  if (!worker.isOwner) {
@@ -6121,8 +7048,8 @@ function run2(cmd, args) {
6121
7048
  const output4 = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
6122
7049
  return { ok: res.status === 0, output: output4 };
6123
7050
  }
6124
- var plistPath = () => path3.join(os10.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
6125
- var unitPath = () => path3.join(os10.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
7051
+ var plistPath = () => path3.join(os7.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
7052
+ var unitPath = () => path3.join(os7.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
6126
7053
  var xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
6127
7054
  function installService(command, endpoint) {
6128
7055
  if (process.platform === "darwin") return installLaunchd(command, endpoint);
@@ -6134,7 +7061,7 @@ function installService(command, endpoint) {
6134
7061
  };
6135
7062
  }
6136
7063
  function installLaunchd(command, endpoint) {
6137
- const logDir = path3.join(os10.homedir(), ".standardagents");
7064
+ const logDir = path3.join(os7.homedir(), ".standardagents");
6138
7065
  fs4.mkdirSync(logDir, { recursive: true });
6139
7066
  fs4.mkdirSync(path3.dirname(plistPath()), { recursive: true });
6140
7067
  const envEntries = [
@@ -6207,10 +7134,10 @@ WantedBy=default.target
6207
7134
  if (!enable.ok) {
6208
7135
  return { ok: false, detail: `systemctl enable failed: ${enable.output}` };
6209
7136
  }
6210
- const linger = run2("loginctl", ["enable-linger", os10.userInfo().username]);
7137
+ const linger = run2("loginctl", ["enable-linger", os7.userInfo().username]);
6211
7138
  return {
6212
7139
  ok: true,
6213
- detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${os10.userInfo().username}`)
7140
+ detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${os7.userInfo().username}`)
6214
7141
  };
6215
7142
  }
6216
7143
  function uninstallService() {
@@ -6338,7 +7265,7 @@ async function installCommand(endpointFlag) {
6338
7265
  const api = await ensureSignedIn(endpoint);
6339
7266
  const identity = loadMachineIdentity();
6340
7267
  const existing = await loadMachine(api, identity.machine_id).catch(() => null);
6341
- const suggested = machineDisplayName(existing ?? { hostname: os10.hostname(), id: identity.machine_id });
7268
+ const suggested = machineDisplayName(existing ?? { hostname: os7.hostname(), id: identity.machine_id });
6342
7269
  const rl = readline2.createInterface({ input: stdin, output: stdout });
6343
7270
  const answer = (await rl.question(
6344
7271
  `${c2.bold}Machine name${c2.reset} ${c2.dim}(shown in the session picker)${c2.reset} [${suggested}]: `
@@ -6362,12 +7289,12 @@ async function installCommand(endpointFlag) {
6362
7289
  `);
6363
7290
  stdout.write(`${c2.dim}Waiting for the daemon's first heartbeat\u2026${c2.reset}
6364
7291
  `);
6365
- const deadline = Date.now() + 3e4;
7292
+ const deadline = Date.now() + 6e4;
6366
7293
  let alive = false;
6367
7294
  while (Date.now() < deadline) {
6368
7295
  await new Promise((r) => setTimeout(r, 2e3));
6369
- const record = await loadMachine(api, identity.machine_id);
6370
- if (record && daemonOnline(record)) {
7296
+ const record2 = await loadMachine(api, identity.machine_id);
7297
+ if (record2 && daemonOnline(record2)) {
6371
7298
  alive = true;
6372
7299
  break;
6373
7300
  }
@@ -6398,7 +7325,7 @@ async function statusCommand() {
6398
7325
  const cred = getCredential(endpoint);
6399
7326
  if (!cred) {
6400
7327
  stdout.write(
6401
- `${c2.bold}Machine:${c2.reset} ${os10.hostname()} ${c2.dim}(${identity.machine_id})${c2.reset}
7328
+ `${c2.bold}Machine:${c2.reset} ${os7.hostname()} ${c2.dim}(${identity.machine_id})${c2.reset}
6402
7329
  `
6403
7330
  );
6404
7331
  stdout.write(`${c2.bold}Account:${c2.reset} ${c2.yellow}not signed in to ${endpoint}${c2.reset}
@@ -6407,21 +7334,21 @@ async function statusCommand() {
6407
7334
  }
6408
7335
  relaxTlsForLocalEndpoint(endpoint);
6409
7336
  const api = new ApiClient(endpoint, cred.access_token);
6410
- const record = await loadMachine(api, identity.machine_id).catch(() => null);
7337
+ const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
6411
7338
  stdout.write(
6412
- `${c2.bold}Machine:${c2.reset} ${machineDisplayName(record ?? { hostname: os10.hostname(), id: identity.machine_id })} ${c2.dim}(${identity.machine_id})${c2.reset}
7339
+ `${c2.bold}Machine:${c2.reset} ${machineDisplayName(record2 ?? { hostname: os7.hostname(), id: identity.machine_id })} ${c2.dim}(${identity.machine_id})${c2.reset}
6413
7340
  `
6414
7341
  );
6415
- if (!record) {
7342
+ if (!record2) {
6416
7343
  stdout.write(`${c2.bold}Registry:${c2.reset} not registered yet
6417
7344
  `);
6418
7345
  return;
6419
7346
  }
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";
7347
+ const online = daemonOnline(record2);
7348
+ const seen = record2.daemon ? `${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
6422
7349
  stdout.write(`${c2.bold}Registry:${c2.reset} ${online ? `${c2.green}online${c2.reset}` : `${c2.yellow}offline${c2.reset}`} \xB7 last heartbeat ${seen}
6423
7350
  `);
6424
- const projects = Object.keys(record.projects);
7351
+ const projects = Object.keys(record2.projects);
6425
7352
  stdout.write(`${c2.bold}Projects:${c2.reset} ${projects.length ? "" : c2.dim + "none registered" + c2.reset}
6426
7353
  `);
6427
7354
  for (const p of projects.sort()) stdout.write(` ${c2.dim}${p}${c2.reset}
@@ -6442,8 +7369,8 @@ async function projectCommand(action, target) {
6442
7369
  const endpoint = resolveEndpoint();
6443
7370
  const api = await ensureSignedIn(endpoint);
6444
7371
  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 });
7372
+ const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
7373
+ const displayName = machineDisplayName(record2 ?? { hostname: os7.hostname(), id: identity.machine_id });
6447
7374
  if (action === "add") {
6448
7375
  await registerProject(api, identity, dir2);
6449
7376
  stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir2} for remote sessions on ${displayName}.
@@ -6537,6 +7464,9 @@ function printUsage() {
6537
7464
  " If url is omitted, prompt for it.",
6538
7465
  " Credentials are remembered for that endpoint, but",
6539
7466
  " the saved default endpoint is not changed.",
7467
+ " --agent <name> Pick the coding agent without the menu:",
7468
+ " 'unlimited' (Unlimited One, default) or 'sama'",
7469
+ " (Sama One \u2014 requires the ChatGPT connection).",
6540
7470
  " -h, --help Show this help.",
6541
7471
  ""
6542
7472
  ].join("\n")
@@ -6550,6 +7480,18 @@ function parseArgs2(args) {
6550
7480
  parsed.help = true;
6551
7481
  continue;
6552
7482
  }
7483
+ if (arg === "--agent") {
7484
+ const value = args[i + 1];
7485
+ if (!value || value.startsWith("-")) throw new Error("--agent requires a name (unlimited or sama).");
7486
+ parsed.agent = value;
7487
+ i++;
7488
+ continue;
7489
+ }
7490
+ if (arg.startsWith("--agent=")) {
7491
+ parsed.agent = arg.slice("--agent=".length);
7492
+ if (!parsed.agent) throw new Error("--agent requires a name (unlimited or sama).");
7493
+ continue;
7494
+ }
6553
7495
  if (arg === "--endpoint" || arg === "-e") {
6554
7496
  const value = args[i + 1];
6555
7497
  if (value && !value.startsWith("-")) {
@@ -6601,7 +7543,7 @@ function printAssistant(tui, text) {
6601
7543
  tui.clearStream();
6602
7544
  tui.print("");
6603
7545
  let dotted = false;
6604
- for (const line of renderMarkdown(text, cols2)) {
7546
+ for (const line of renderStreamingMarkdown(text, cols2)) {
6605
7547
  if (!dotted && line.trim()) {
6606
7548
  tui.print(`${c3.gray}\u2022${c3.reset} ${line}`);
6607
7549
  dotted = true;
@@ -6643,18 +7585,21 @@ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.rese
6643
7585
  `);
6644
7586
  }
6645
7587
  function printWelcome(endpoint, projectDir) {
6646
- const home = os10.homedir();
7588
+ const home = os7.homedir();
6647
7589
  const dir2 = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
6648
7590
  const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
6649
7591
  const version = readVersion();
6650
7592
  const pad = " ";
7593
+ const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
7594
+ const terminalColumns = Math.max(20, process.stdout.columns || 80);
7595
+ const metaWidth = Math.max(1, terminalColumns - pad.length - markWidth - 3 - 1);
7596
+ const displayDir = truncateMiddle(dir2, metaWidth);
6651
7597
  const meta = [
6652
7598
  `${c3.bold}${gradientText("Standard Code")}${c3.reset}${version ? ` ${c3.dim}v${version}${c3.reset}` : ""}`,
6653
7599
  `${c3.dim}terminal coding agent${c3.reset}`,
6654
7600
  ...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c3.teal}${host}${c3.reset}`],
6655
- `${c3.dim}${dir2}${c3.reset}`
7601
+ `${c3.dim}${displayDir}${c3.reset}`
6656
7602
  ];
6657
- const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
6658
7603
  const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
6659
7604
  stdout.write("\n");
6660
7605
  for (let i = 0; i < LOGO_MARK.length; i++) {
@@ -6711,7 +7656,7 @@ async function main() {
6711
7656
  const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
6712
7657
  const dirArg = cliArgs.dir;
6713
7658
  const projectDir = path3.resolve(dirArg || process.cwd());
6714
- const machine = os10.hostname();
7659
+ const machine = os7.hostname();
6715
7660
  const reader = { rl: null };
6716
7661
  let handoffClosing = false;
6717
7662
  let preflightArmed = false;
@@ -6896,7 +7841,46 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6896
7841
  void registerProject(api, identity, projectDir).catch(() => {
6897
7842
  });
6898
7843
  const tui = new Tui(1);
6899
- const home = os10.homedir();
7844
+ let selectedAgent = AGENT_ID;
7845
+ if (cliArgs.agent) {
7846
+ const wanted = cliArgs.agent.toLowerCase();
7847
+ if (["sama", "opensama", "sama_one", OPENSAMA_AGENT_ID].includes(wanted)) {
7848
+ selectedAgent = OPENSAMA_AGENT_ID;
7849
+ } else if (["unlimited", "standard", "unlimited_one", AGENT_ID].includes(wanted)) {
7850
+ selectedAgent = AGENT_ID;
7851
+ } else {
7852
+ stdout.write(`${c3.red}error:${c3.reset} Unknown agent "${cliArgs.agent}". Use unlimited or sama.
7853
+ `);
7854
+ process.exit(1);
7855
+ }
7856
+ } else {
7857
+ const picked = await tui.select(
7858
+ `${c3.bold}${gradientText("Which agent?")}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
7859
+ AGENT_CHOICES.map((choice) => ({
7860
+ label: choice.title,
7861
+ hint: choice.description,
7862
+ value: choice.id,
7863
+ disabled: choice.disabled
7864
+ }))
7865
+ );
7866
+ if (!picked) process.exit(0);
7867
+ selectedAgent = picked;
7868
+ }
7869
+ if (selectedAgent === OPENSAMA_AGENT_ID) {
7870
+ const connecting = startLoader("Checking your ChatGPT connection");
7871
+ const hasKey = await api.openSamaStatus().catch(() => false);
7872
+ connecting.stop();
7873
+ if (!hasKey) {
7874
+ stdout.write(
7875
+ `${c3.yellow}Sama One needs your ChatGPT account connected first.${c3.reset}
7876
+ Opening ${c3.teal}https://standardcode.ai/app${c3.reset} \u2014 connect ChatGPT there, then run standardcode again.
7877
+ `
7878
+ );
7879
+ openUrl("https://standardcode.ai/app");
7880
+ process.exit(0);
7881
+ }
7882
+ }
7883
+ const home = os7.homedir();
6900
7884
  const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
6901
7885
  const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
6902
7886
  const session = { mode: "local", identity };
@@ -6919,7 +7903,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6919
7903
  ]
6920
7904
  );
6921
7905
  if (where) {
6922
- const remotePath = await pickRemoteProject(tui, where);
7906
+ const remotePath = await pickRemoteProject(tui, api, where);
6923
7907
  if (remotePath) {
6924
7908
  session.mode = "remote";
6925
7909
  session.runner = where;
@@ -6944,17 +7928,24 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6944
7928
  const loadingSessions = startLoader("Loading sessions");
6945
7929
  let existing = [];
6946
7930
  try {
6947
- existing = await api.listThreads(AGENT_ID_VARIANTS, resumeTags);
7931
+ existing = await api.listThreads(
7932
+ selectedAgent === OPENSAMA_AGENT_ID ? [OPENSAMA_AGENT_ID] : AGENT_ID_VARIANTS,
7933
+ resumeTags
7934
+ );
6948
7935
  } catch {
6949
7936
  existing = [];
6950
7937
  }
6951
7938
  const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
6952
7939
  loadingSessions.stop();
6953
7940
  const createSessionThread = async () => {
6954
- const id = await api.createThread(AGENT_ID, tags);
7941
+ const id = await api.createThread(selectedAgent, tags);
6955
7942
  if (session.mode === "remote" && session.runner && session.remotePath) {
6956
7943
  await api.kvSet(id, "session_info", { cwd: session.remotePath, machine: session.runner.name }).catch(() => {
6957
7944
  });
7945
+ void enqueueMachineCommand(api, session.runner.id, "add_project", {
7946
+ path: session.remotePath
7947
+ }).catch(() => {
7948
+ });
6958
7949
  }
6959
7950
  return id;
6960
7951
  };
@@ -6994,7 +7985,38 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
6994
7985
  function shortenPath(p, max = 38) {
6995
7986
  return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
6996
7987
  }
6997
- async function pickRemoteProject(tui, runner) {
7988
+ async function runAgentSwitchMenu(tui, api, threadId) {
7989
+ const picked = await tui.select(
7990
+ `${c3.bold}${gradientText("Switch agent")}${c3.reset} ${c3.dim}takes effect on the next message${c3.reset}`,
7991
+ AGENT_CHOICES.map((choice) => ({
7992
+ label: choice.title,
7993
+ hint: choice.description,
7994
+ value: choice.id,
7995
+ disabled: choice.disabled
7996
+ }))
7997
+ );
7998
+ if (!picked) return;
7999
+ if (picked === OPENSAMA_AGENT_ID) {
8000
+ const hasKey = await api.openSamaStatus().catch(() => false);
8001
+ if (!hasKey) {
8002
+ tui.print(
8003
+ `${c3.yellow}Sama One needs your ChatGPT account connected first \u2014 opening ${c3.teal}standardcode.ai/app${c3.reset}${c3.yellow}.${c3.reset}`
8004
+ );
8005
+ openUrl("https://standardcode.ai/app");
8006
+ return;
8007
+ }
8008
+ }
8009
+ const title = AGENT_CHOICES.find((choice) => choice.id === picked)?.title ?? picked;
8010
+ try {
8011
+ await api.setThreadAgent(threadId, picked);
8012
+ tui.print(`${c3.green}\u2713${c3.reset} Session handed to ${c3.bold}${title}${c3.reset} \u2014 applies from your next message.`);
8013
+ } catch (e) {
8014
+ tui.print(
8015
+ `${c3.red}\u2717 couldn't switch agent:${c3.reset} ${c3.gray}${e instanceof Error ? e.message : String(e)}${c3.reset}`
8016
+ );
8017
+ }
8018
+ }
8019
+ async function pickRemoteProject(tui, api, runner) {
6998
8020
  const ENTER_PATH = "__enter_path__";
6999
8021
  const projects = Object.entries(runner.projects).sort(
7000
8022
  (a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
@@ -7021,7 +8043,7 @@ async function pickRemoteProject(tui, runner) {
7021
8043
  tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
7022
8044
  return null;
7023
8045
  }
7024
- return trimmed;
8046
+ return await resolveRemoteProjectPath(api, runner.id, trimmed);
7025
8047
  }
7026
8048
  function isSilentMessage(m) {
7027
8049
  return m?.silent === true || m?.metadata?.silent === true;
@@ -7056,31 +8078,6 @@ function relativeTime(unixSeconds) {
7056
8078
  if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
7057
8079
  return `${Math.floor(diff / 86400)}d ago`;
7058
8080
  }
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
8081
  async function printHistory(api, threadId, tui) {
7085
8082
  let msgs;
7086
8083
  try {
@@ -7139,9 +8136,19 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
7139
8136
  saveApprovals(api, threadId, perm);
7140
8137
  const attaching = startLoader("Attaching to thread");
7141
8138
  let busy = false;
7142
- let interrupting = false;
7143
- const queued = [];
7144
- let editingQueued = false;
8139
+ let sharedMessaging = emptySharedMessagingSnapshot();
8140
+ let mirroredDraftRefs = [];
8141
+ let sharedMessagingReady = false;
8142
+ let sharedMessagingUnavailableShown = false;
8143
+ const messagingOrigin = {
8144
+ originClientId: `tui:${Math.random().toString(36).slice(2, 10)}`,
8145
+ originClientKind: "tui"
8146
+ };
8147
+ let editingPendingId = null;
8148
+ let reconcileSharedMessaging = async () => {
8149
+ };
8150
+ let refreshSessionProjection = async () => {
8151
+ };
7145
8152
  const shownIds = /* @__PURE__ */ new Set();
7146
8153
  const pendingSent = /* @__PURE__ */ new Map();
7147
8154
  let lastSent = null;
@@ -7239,6 +8246,10 @@ why: ${req.requestPermission}` : ""}`,
7239
8246
  bridge = exec.bridge;
7240
8247
  }
7241
8248
  const stream = new MessageStream(api, threadId, {
8249
+ onOpen: () => {
8250
+ void reconcileSharedMessaging(true);
8251
+ void refreshSessionProjection();
8252
+ },
7242
8253
  // Live streaming preview: answer text and (opt-in) internal reasoning feed
7243
8254
  // the TUI's ephemeral preview; the committed message still renders from
7244
8255
  // polling, which calls tui.clearStream() first so there's no double-render.
@@ -7246,6 +8257,12 @@ why: ${req.requestPermission}` : ""}`,
7246
8257
  onReasoningChunk: (text, mid) => tui.streamThinkingDelta(text, mid),
7247
8258
  onAssistantText: () => {
7248
8259
  },
8260
+ onMessage: (message) => {
8261
+ if (message?.role === "user" || message?.status === "pending") {
8262
+ busy = true;
8263
+ tui.setWorking(true);
8264
+ }
8265
+ },
7249
8266
  onEvent: (eventType, data) => {
7250
8267
  if (eventType === "generation" && typeof data?.outputTokens === "number") {
7251
8268
  liveOut = data.outputTokens;
@@ -7258,6 +8275,8 @@ why: ${req.requestPermission}` : ""}`,
7258
8275
  refreshStatus();
7259
8276
  } else if (eventType === "goal_updated" && data) {
7260
8277
  tui.setGoal(data);
8278
+ } else if (eventType === SHARED_MESSAGING_EVENT) {
8279
+ void reconcileSharedMessaging(true);
7261
8280
  }
7262
8281
  },
7263
8282
  // A failed turn whose message is the lease service's at-limit denial → offer
@@ -7334,7 +8353,7 @@ why: ${req.requestPermission}` : ""}`,
7334
8353
  const sessionEnded = new Promise((r) => endSession = r);
7335
8354
  const quit = async () => {
7336
8355
  tui.end();
7337
- const stopped2 = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
8356
+ const stopped2 = bridge?.isOwner ?? false ? api.requestSharedStop(threadId, messagingOrigin).catch(() => {
7338
8357
  }) : Promise.resolve();
7339
8358
  const procsStopped2 = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
7340
8359
  bridge?.close();
@@ -7409,23 +8428,89 @@ why: ${req.requestPermission}` : ""}`,
7409
8428
  };
7410
8429
  const extFor = (mime) => ({ "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" })[mime] ?? "bin";
7411
8430
  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);
8431
+ const toSharedAttachments = (images) => toAttachments(images);
8432
+ const fromSharedAttachments = (attachments2) => attachments2.filter(isInlineSharedAttachment).map((attachment, index) => {
8433
+ const namedSequence = /(?:image-|Image\s+)(\d+)/i.exec(attachment.name)?.[1];
8434
+ return {
8435
+ seq: namedSequence ? Number(namedSequence) : index + 1,
8436
+ data: attachment.data,
8437
+ mime: attachment.mimeType
8438
+ };
8439
+ });
8440
+ const applySharedMessaging = (snapshot, mirrorDraft) => {
8441
+ const firstSnapshot = !sharedMessagingReady;
8442
+ const previousDraftRevision = sharedMessaging.draft.revision;
8443
+ sharedMessaging = firstSnapshot ? snapshot : mergeSharedMessagingSnapshot(sharedMessaging, snapshot);
8444
+ sharedMessagingReady = true;
8445
+ sharedMessagingUnavailableShown = false;
8446
+ tui.setQueuedCount(sharedMessaging.pending.items.length);
8447
+ if (mirrorDraft && (firstSnapshot || sharedMessaging.draft.revision > previousDraftRevision) && (firstSnapshot || sharedMessaging.draft.originClientId !== messagingOrigin.originClientId)) {
8448
+ mirroredDraftRefs = sharedMessaging.draft.attachments.filter(isSharedAttachmentRef);
8449
+ tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
8450
+ tui.setInput(
8451
+ sharedMessaging.draft.content,
8452
+ fromSharedAttachments(sharedMessaging.draft.attachments),
8453
+ false,
8454
+ true
8455
+ );
8456
+ }
8457
+ };
8458
+ reconcileSharedMessaging = async (mirrorDraft = false) => {
8459
+ try {
8460
+ applySharedMessaging(await api.getSharedMessaging(threadId), mirrorDraft);
8461
+ } catch (error) {
8462
+ if (!sharedMessagingReady && !sharedMessagingUnavailableShown) {
8463
+ sharedMessagingUnavailableShown = true;
8464
+ tui.print(`${c3.dim}shared messaging unavailable: ${error instanceof Error ? error.message : String(error)}${c3.reset}`);
8465
+ }
8466
+ }
8467
+ };
8468
+ const onTerminalResume = () => void reconcileSharedMessaging(true);
8469
+ process.on("SIGCONT", onTerminalResume);
8470
+ const applySharedMutation = (promise) => promise.then((snapshot) => {
8471
+ applySharedMessaging(snapshot, false);
8472
+ return true;
8473
+ }).catch((error) => {
8474
+ tui.print(`${c3.dim}shared messaging failed: ${error instanceof Error ? error.message : String(error)}${c3.reset}`);
8475
+ return false;
8476
+ });
8477
+ const appendSharedPending = (text, images, refs = []) => applySharedMutation(api.appendPendingInput(threadId, {
8478
+ content: text,
8479
+ attachments: [...refs, ...toSharedAttachments(images)],
8480
+ ...messagingOrigin
8481
+ }));
8482
+ const steerSharedInput = (text, images, refs = []) => applySharedMutation(api.steerInput(threadId, {
8483
+ content: text,
8484
+ attachments: [...refs, ...toSharedAttachments(images)],
8485
+ ...messagingOrigin
8486
+ }));
8487
+ const editSharedPending = (item, text, images, refs) => applySharedMutation(api.updatePendingInput(threadId, item.id, {
8488
+ content: text,
8489
+ attachments: [
8490
+ ...refs,
8491
+ ...toSharedAttachments(images)
8492
+ ],
8493
+ ...messagingOrigin
8494
+ }));
8495
+ const dismissSharedPending = (item) => applySharedMutation(api.dismissPendingInput(threadId, item.id, messagingOrigin));
8496
+ const promoteSharedPending = (item) => applySharedMutation(api.steerPendingInput(threadId, item.id, messagingOrigin));
8497
+ const sendNow = async (text, images = [], refs = []) => {
8498
+ lastSent = { text, images, refs };
8499
+ tui.printUserMessage(text || `\u{1F4CE} ${refs.length + images.length} attachment(s)`);
7415
8500
  const key = text.trim();
7416
8501
  pendingSent.set(key, (pendingSent.get(key) ?? 0) + 1);
7417
8502
  try {
7418
- await api.sendMessage(threadId, text, toAttachments(images));
8503
+ await api.sendMessage(threadId, text, [...refs, ...toAttachments(images)]);
7419
8504
  } catch (e) {
7420
8505
  const n = (pendingSent.get(key) ?? 1) - 1;
7421
8506
  if (n > 0) pendingSent.set(key, n);
7422
8507
  else pendingSent.delete(key);
7423
8508
  tui.print(`${c3.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
7424
- return;
8509
+ return false;
7425
8510
  }
7426
- interrupting = false;
7427
8511
  busy = true;
7428
8512
  tui.setWorking(true);
8513
+ return true;
7429
8514
  };
7430
8515
  const whereLabel = remote ? runnerName : "this machine";
7431
8516
  let bangRunning = false;
@@ -7446,11 +8531,34 @@ why: ${req.requestPermission}` : ""}`,
7446
8531
  bangRunning = false;
7447
8532
  }
7448
8533
  };
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);
8534
+ const openPendingMenu = async () => {
8535
+ const items = sharedMessaging.pending.items;
8536
+ if (!items.length) {
8537
+ tui.print(`${c3.dim}No pending messages.${c3.reset}`);
8538
+ return;
8539
+ }
8540
+ const picked = await tui.select("Pending messages", items.map((item, index) => ({
8541
+ label: item.content.replace(/\s+/g, " "),
8542
+ hint: `${index + 1} of ${items.length}`,
8543
+ value: item
8544
+ })));
8545
+ if (!picked) return;
8546
+ const action = await tui.select("Pending message", [
8547
+ { label: "Edit", value: "edit" },
8548
+ { label: "Steer next", value: "steer" },
8549
+ { label: "Dismiss", value: "dismiss" }
8550
+ ]);
8551
+ if (!action) return;
8552
+ if (action === "edit") {
8553
+ editingPendingId = picked.id;
8554
+ mirroredDraftRefs = picked.attachments.filter(isSharedAttachmentRef);
8555
+ tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
8556
+ tui.setInput(picked.content, fromSharedAttachments(picked.attachments), true, true);
8557
+ } else if (action === "steer") {
8558
+ await promoteSharedPending(picked);
8559
+ } else {
8560
+ await dismissSharedPending(picked);
8561
+ }
7454
8562
  };
7455
8563
  const requestCompaction = async () => {
7456
8564
  try {
@@ -7554,7 +8662,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7554
8662
  tui.print(`${c3.green}\u2713${c3.reset} ${c3.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c3.reset}`);
7555
8663
  if (opts.auto && lastSent) {
7556
8664
  tui.print(`${c3.gray}Continuing\u2026${c3.reset}`);
7557
- await sendNow(lastSent.text, lastSent.images);
8665
+ await sendNow(lastSent.text, lastSent.images, lastSent.refs);
7558
8666
  }
7559
8667
  } finally {
7560
8668
  upgradeInFlight = false;
@@ -7587,6 +8695,15 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7587
8695
  hint: () => tui.contextPctLabel() || "free up context",
7588
8696
  run: requestCompaction
7589
8697
  },
8698
+ {
8699
+ name: "queue",
8700
+ label: "Pending messages",
8701
+ hint: () => {
8702
+ const count = sharedMessaging.pending.items.length;
8703
+ return count ? `${count} pending` : "none";
8704
+ },
8705
+ run: openPendingMenu
8706
+ },
7590
8707
  { name: "level", label: "Auto-accept level", hint: () => `level ${tui.level}`, run: () => runLevelMenu(tui, perm) },
7591
8708
  {
7592
8709
  name: "permissions",
@@ -7610,6 +8727,12 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7610
8727
  run: () => runMcpMenu(tui, mcpCtl)
7611
8728
  }
7612
8729
  ],
8730
+ {
8731
+ name: "agent",
8732
+ label: "Switch agent",
8733
+ hint: "hand this session to a different agent",
8734
+ run: () => runAgentSwitchMenu(tui, api, threadId)
8735
+ },
7613
8736
  {
7614
8737
  name: "machines",
7615
8738
  label: "Your machines",
@@ -7648,26 +8771,46 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7648
8771
  ]);
7649
8772
  const history = await loadHistory(api, threadId, historySeedThreadId);
7650
8773
  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
- });
8774
+ await reconcileSharedMessaging(true);
8775
+ let draftTimer;
8776
+ const clearComposerDraft = () => {
8777
+ if (draftTimer) clearTimeout(draftTimer);
8778
+ draftTimer = void 0;
8779
+ mirroredDraftRefs = [];
8780
+ tui.setExternalAttachmentNames([]);
8781
+ if (sharedMessagingReady) void applySharedMutation(api.clearSharedDraft(threadId, messagingOrigin));
7658
8782
  };
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);
8783
+ tui.onDraftChange = (textVal, images) => {
8784
+ if (draftTimer) clearTimeout(draftTimer);
8785
+ draftTimer = setTimeout(() => {
8786
+ draftTimer = void 0;
8787
+ if (sharedMessagingReady) {
8788
+ const hasDraft = !!textVal.trim() || images.length > 0 || mirroredDraftRefs.length > 0;
8789
+ const mutation = {
8790
+ content: textVal,
8791
+ attachments: [
8792
+ ...hasDraft ? mirroredDraftRefs : [],
8793
+ ...toSharedAttachments(images)
8794
+ ],
8795
+ ...messagingOrigin
8796
+ };
8797
+ void applySharedMutation(
8798
+ hasDraft ? api.putSharedDraft(threadId, mutation) : api.clearSharedDraft(threadId, messagingOrigin)
8799
+ );
8800
+ }
8801
+ }, 150);
7666
8802
  };
7667
- tui.onSubmit = (text, images) => {
7668
- clearPresence();
8803
+ const submitComposer = async (text, images, steer) => {
8804
+ const draftRefs = mirroredDraftRefs;
8805
+ if (!sharedMessagingReady && (busy || steer || editingPendingId !== null)) {
8806
+ tui.print(`${c3.dim}Restoring shared message state \u2014 try again in a moment.${c3.reset}`);
8807
+ tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
8808
+ tui.setInput(text, images);
8809
+ return;
8810
+ }
8811
+ clearComposerDraft();
7669
8812
  const trimmed = text.trimStart();
7670
- if (trimmed.startsWith("!") && !trimmed.startsWith("!!")) {
8813
+ if (trimmed.startsWith("!") && !trimmed.startsWith("!!") && images.length === 0 && draftRefs.length === 0) {
7671
8814
  const command = trimmed.slice(1).trim();
7672
8815
  if (command) {
7673
8816
  appendHistory(api, threadId, history, text);
@@ -7676,38 +8819,71 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7676
8819
  return;
7677
8820
  }
7678
8821
  const outgoing = trimmed.startsWith("!!") ? text.replace("!!", "!") : text;
7679
- appendHistory(api, threadId, history, outgoing);
8822
+ if (outgoing.trim()) appendHistory(api, threadId, history, outgoing);
7680
8823
  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}`);
8824
+ if (editingPendingId) {
8825
+ const pendingId = editingPendingId;
8826
+ const item = sharedMessaging.pending.items.find((candidate) => candidate.id === pendingId);
8827
+ editingPendingId = null;
8828
+ if (!item) {
8829
+ tui.print(`${c3.dim}That pending message was already dispatched or dismissed.${c3.reset}`);
8830
+ return;
8831
+ }
8832
+ const updated = await editSharedPending(item, text, images, draftRefs);
8833
+ const promoted = !steer || !updated ? updated : await applySharedMutation(api.steerPendingInput(threadId, pendingId, messagingOrigin));
8834
+ if (!promoted) {
8835
+ mirroredDraftRefs = item.attachments.filter(isSharedAttachmentRef);
8836
+ tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
8837
+ tui.setInput(text, images);
8838
+ }
8839
+ return;
8840
+ }
8841
+ if (steer) {
8842
+ tui.print(`${c3.yellow}\u21AA steering at the next safe model boundary:${c3.reset} ${text}`);
8843
+ if (!await steerSharedInput(text, images, draftRefs)) {
8844
+ mirroredDraftRefs = draftRefs;
8845
+ tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
8846
+ tui.setInput(text, images);
8847
+ }
7686
8848
  return;
7687
8849
  }
7688
8850
  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}`);
8851
+ if (await appendSharedPending(text, images, draftRefs)) {
8852
+ tui.print(`${c3.gray}\u23F3 pending:${c3.reset} ${text} ${c3.dim}(/queue to edit, steer, or dismiss)${c3.reset}`);
8853
+ } else {
8854
+ mirroredDraftRefs = draftRefs;
8855
+ tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
8856
+ tui.setInput(text, images);
8857
+ }
7692
8858
  } else {
7693
- void sendNow(text, images);
8859
+ if (!await sendNow(text, images, draftRefs)) {
8860
+ mirroredDraftRefs = draftRefs;
8861
+ tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
8862
+ tui.setInput(text, images);
8863
+ }
7694
8864
  }
7695
8865
  };
8866
+ tui.onSubmit = (text, images) => {
8867
+ void submitComposer(text, images, false);
8868
+ };
8869
+ tui.onSteer = (text, images) => {
8870
+ void submitComposer(text, images, true);
8871
+ };
7696
8872
  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
- });
8873
+ const firstPending = sharedMessaging.pending.items[0];
8874
+ if (!busy && firstPending) {
8875
+ tui.print(`${c3.yellow}\u21AA steering the first pending message\u2026${c3.reset}`);
8876
+ void promoteSharedPending(firstPending);
8877
+ return;
8878
+ }
8879
+ if (busy) {
8880
+ if (!sharedMessagingReady) {
8881
+ tui.print(`${c3.dim}Shared messaging is not connected; the session was not stopped.${c3.reset}`);
8882
+ return;
8883
+ }
8884
+ const advancing = sharedMessaging.pending.items.length > 0;
8885
+ tui.print(`${c3.yellow}${advancing ? "[stopping; next pending message will run]" : "[stopping at the next safe boundary]"}${c3.reset}`);
8886
+ void applySharedMutation(api.requestSharedStop(threadId, messagingOrigin));
7711
8887
  }
7712
8888
  };
7713
8889
  tui.onBgBadge = () => {
@@ -7715,11 +8891,13 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
7715
8891
  });
7716
8892
  };
7717
8893
  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);
8894
+ if (tui.getInput().trim() || tui.hasExternalAttachments()) return false;
8895
+ const item = sharedMessaging.pending.items.at(-1);
8896
+ if (!item) return false;
8897
+ editingPendingId = item.id;
8898
+ mirroredDraftRefs = item.attachments.filter(isSharedAttachmentRef);
8899
+ tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
8900
+ tui.setInput(item.content, fromSharedAttachments(item.attachments), true, true);
7723
8901
  return true;
7724
8902
  };
7725
8903
  events.connect();
@@ -7795,16 +8973,26 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7795
8973
  };
7796
8974
  const poll = async () => {
7797
8975
  let msgs;
8976
+ let serverBusy = null;
8977
+ let serverTool = null;
7798
8978
  try {
7799
- msgs = await api.getMessages(threadId, 60);
8979
+ const snapshot = await api.getSessionState(threadId);
8980
+ msgs = snapshot.messages.slice(-60);
8981
+ serverBusy = snapshot.busy;
8982
+ serverTool = snapshot.current_tool;
7800
8983
  } catch {
7801
- return;
8984
+ try {
8985
+ msgs = await api.getMessages(threadId, 60);
8986
+ } catch {
8987
+ return;
8988
+ }
7802
8989
  }
7803
8990
  const sorted = [...msgs].sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
7804
8991
  for (const m of sorted) {
7805
- if (shownIds.has(m.id) || m.status === "pending") continue;
7806
- shownIds.add(m.id);
8992
+ if (shownIds.has(m.id)) continue;
7807
8993
  const text = messageText(m.content).trim();
8994
+ if (!transcriptMessageReady(m, text)) continue;
8995
+ shownIds.add(m.id);
7808
8996
  const denial = typeof m.error === "string" && m.error || text;
7809
8997
  if (denial && isSessionLimitError(denial)) {
7810
8998
  void offerUpgrade({ auto: true });
@@ -7831,19 +9019,20 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7831
9019
  }
7832
9020
  }
7833
9021
  }
7834
- const polledBusy = threadBusy(msgs);
7835
- if (interrupting) {
7836
- if (!polledBusy) interrupting = false;
7837
- busy = false;
7838
- } else {
7839
- busy = polledBusy;
9022
+ const polledBusy = (serverBusy ?? false) || threadBusy(msgs);
9023
+ if (serverTool && !activeSteps.has(serverTool.id)) {
9024
+ activeSteps.set(serverTool.id, serverTool.name || "working");
9025
+ refreshStatus();
9026
+ } else if (serverBusy !== null && !serverTool && activeSteps.size) {
9027
+ activeSteps.clear();
9028
+ refreshStatus();
7840
9029
  }
9030
+ busy = polledBusy;
7841
9031
  tui.setWorking(busy);
7842
9032
  if (!busy) {
7843
9033
  if (activeSteps.size) activeSteps.clear();
7844
9034
  liveOut = 0;
7845
9035
  refreshStatus();
7846
- if (queued.length > 0 && !editingQueued) await flushQueued();
7847
9036
  }
7848
9037
  refreshBgCount();
7849
9038
  void relayApprovals().catch(() => {
@@ -7879,12 +9068,17 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
7879
9068
  } catch {
7880
9069
  }
7881
9070
  };
7882
- const pollTimer = setInterval(() => void poll().catch(() => {
7883
- }), 1200);
9071
+ refreshSessionProjection = () => poll().catch(() => {
9072
+ });
9073
+ await refreshSessionProjection();
9074
+ const pollTimer = setInterval(() => {
9075
+ if (busy || approvalPromptOpen) void refreshSessionProjection();
9076
+ }, 1200);
7884
9077
  await sessionEnded;
7885
9078
  clearInterval(pollTimer);
7886
9079
  clearInterval(heartbeatPoll);
7887
- const stopped = bridge?.isOwner ?? false ? api.stop(threadId).catch(() => {
9080
+ process.off("SIGCONT", onTerminalResume);
9081
+ const stopped = bridge?.isOwner ?? false ? api.requestSharedStop(threadId, messagingOrigin).catch(() => {
7888
9082
  }) : Promise.resolve();
7889
9083
  const procsStopped = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
7890
9084
  bridge?.close();
@@ -8084,12 +9278,12 @@ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNot
8084
9278
  );
8085
9279
  if (!picked) return;
8086
9280
  if (picked === ADD) {
8087
- const path13 = await tui.prompt(
9281
+ const path14 = await tui.prompt(
8088
9282
  `Absolute project path on ${machine.name}`,
8089
9283
  machine.id === self.machine_id ? process.cwd() : "/home/you/project"
8090
9284
  );
8091
- if (!path13 || !path13.trim()) return;
8092
- const trimmed = path13.trim();
9285
+ if (!path14 || !path14.trim()) return;
9286
+ const trimmed = path14.trim();
8093
9287
  if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
8094
9288
  tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
8095
9289
  return;