@standardagents/code 0.9.5 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/index.js +1523 -451
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
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
|
|
238
|
+
throw new ApiHttpError(
|
|
239
|
+
res.status,
|
|
240
|
+
`${init?.method || "GET"} ${pathname} -> ${res.status}: ${text.slice(0, 300)}`
|
|
241
|
+
);
|
|
106
242
|
}
|
|
107
243
|
try {
|
|
108
244
|
return JSON.parse(text);
|
|
@@ -183,21 +319,36 @@ var ApiClient = class {
|
|
|
183
319
|
*/
|
|
184
320
|
async listThreads(agentId, requireTags) {
|
|
185
321
|
const ids = Array.isArray(agentId) ? agentId : [agentId];
|
|
186
|
-
const pages = await Promise.all(
|
|
187
|
-
|
|
322
|
+
const [me, ...pages] = await Promise.all([
|
|
323
|
+
this.currentUserId(),
|
|
324
|
+
...ids.map(
|
|
188
325
|
(id) => this.json(
|
|
189
326
|
`/api/threads?agent_id=${encodeURIComponent(id)}&limit=100`
|
|
190
327
|
).catch(() => [])
|
|
191
328
|
)
|
|
192
|
-
);
|
|
329
|
+
]);
|
|
193
330
|
const arr = pages.flatMap((res) => Array.isArray(res) ? res : res.threads || []);
|
|
194
331
|
return arr.map((t) => ({
|
|
195
332
|
id: t.id,
|
|
196
333
|
tags: Array.isArray(t.tags) ? t.tags : [],
|
|
197
334
|
created_at: t.created_at,
|
|
198
335
|
title: t.title,
|
|
199
|
-
preview: t.preview || t.last_message
|
|
200
|
-
|
|
336
|
+
preview: t.preview || t.last_message,
|
|
337
|
+
user_id: t.user_id ?? null
|
|
338
|
+
})).filter((t) => !me || !t.user_id || t.user_id === me).filter((t) => requireTags.every((tag) => t.tags.includes(tag)));
|
|
339
|
+
}
|
|
340
|
+
/** The authenticated user's id (cached). Null when the instance doesn't
|
|
341
|
+
* report one (super-admin sessions, very old instances). */
|
|
342
|
+
meUserId;
|
|
343
|
+
async currentUserId() {
|
|
344
|
+
if (this.meUserId !== void 0) return this.meUserId;
|
|
345
|
+
try {
|
|
346
|
+
const res = await this.json(`/api/auth/me`);
|
|
347
|
+
this.meUserId = typeof res?.user?.id === "string" && res.user.id ? res.user.id : null;
|
|
348
|
+
} catch {
|
|
349
|
+
this.meUserId = null;
|
|
350
|
+
}
|
|
351
|
+
return this.meUserId;
|
|
201
352
|
}
|
|
202
353
|
/**
|
|
203
354
|
* Subagent child threads of a thread, each with its current lifecycle status
|
|
@@ -225,14 +376,85 @@ var ApiClient = class {
|
|
|
225
376
|
* `mimeType` — which the server stores in the thread filesystem and injects
|
|
226
377
|
* into the LLM's vision context as real image content blocks.
|
|
227
378
|
*/
|
|
228
|
-
async sendMessage(threadId, content,
|
|
379
|
+
async sendMessage(threadId, content, attachments2) {
|
|
229
380
|
const body = { role: "user", content };
|
|
230
|
-
if (
|
|
381
|
+
if (attachments2 && attachments2.length > 0) body.attachments = attachments2;
|
|
231
382
|
await this.json(`/api/threads/${threadId}/messages`, {
|
|
232
383
|
method: "POST",
|
|
233
384
|
body: JSON.stringify(body)
|
|
234
385
|
});
|
|
235
386
|
}
|
|
387
|
+
// ── portable Standard Code shared messaging endpoints ────────────────────
|
|
388
|
+
messagingPath(threadId, suffix = "") {
|
|
389
|
+
return `/api/threads/${threadId}${SHARED_MESSAGING_ROUTE}${suffix}`;
|
|
390
|
+
}
|
|
391
|
+
async getSharedMessaging(threadId) {
|
|
392
|
+
return parseSharedMessagingSnapshot(await this.json(this.messagingPath(threadId)));
|
|
393
|
+
}
|
|
394
|
+
async appendPendingInput(threadId, mutation) {
|
|
395
|
+
return parseSharedMessagingSnapshot(
|
|
396
|
+
await this.json(this.messagingPath(threadId, "/pending"), {
|
|
397
|
+
method: "POST",
|
|
398
|
+
body: JSON.stringify(mutation)
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
async steerInput(threadId, mutation) {
|
|
403
|
+
return parseSharedMessagingSnapshot(
|
|
404
|
+
await this.json(this.messagingPath(threadId, "/steer"), {
|
|
405
|
+
method: "POST",
|
|
406
|
+
body: JSON.stringify(mutation)
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
async updatePendingInput(threadId, pendingId, mutation) {
|
|
411
|
+
return parseSharedMessagingSnapshot(
|
|
412
|
+
await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}`), {
|
|
413
|
+
method: "PATCH",
|
|
414
|
+
body: JSON.stringify(mutation)
|
|
415
|
+
})
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
async dismissPendingInput(threadId, pendingId, origin2) {
|
|
419
|
+
return parseSharedMessagingSnapshot(
|
|
420
|
+
await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}`), {
|
|
421
|
+
method: "DELETE",
|
|
422
|
+
body: JSON.stringify(origin2)
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
async steerPendingInput(threadId, pendingId, origin2) {
|
|
427
|
+
return parseSharedMessagingSnapshot(
|
|
428
|
+
await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}/steer`), {
|
|
429
|
+
method: "POST",
|
|
430
|
+
body: JSON.stringify(origin2)
|
|
431
|
+
})
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
async putSharedDraft(threadId, mutation) {
|
|
435
|
+
return parseSharedMessagingSnapshot(
|
|
436
|
+
await this.json(this.messagingPath(threadId, "/draft"), {
|
|
437
|
+
method: "PUT",
|
|
438
|
+
body: JSON.stringify(mutation)
|
|
439
|
+
})
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
async clearSharedDraft(threadId, origin2) {
|
|
443
|
+
return parseSharedMessagingSnapshot(
|
|
444
|
+
await this.json(this.messagingPath(threadId, "/draft"), {
|
|
445
|
+
method: "DELETE",
|
|
446
|
+
body: JSON.stringify(origin2)
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
async requestSharedStop(threadId, origin2) {
|
|
451
|
+
return parseSharedMessagingSnapshot(
|
|
452
|
+
await this.json(this.messagingPath(threadId, "/stop"), {
|
|
453
|
+
method: "POST",
|
|
454
|
+
body: JSON.stringify(origin2)
|
|
455
|
+
})
|
|
456
|
+
);
|
|
457
|
+
}
|
|
236
458
|
async getMessages(threadId, limit = 50, order) {
|
|
237
459
|
const orderParam = order ? `&order=${order}` : "";
|
|
238
460
|
const res = await this.json(
|
|
@@ -240,6 +462,14 @@ var ApiClient = class {
|
|
|
240
462
|
);
|
|
241
463
|
return Array.isArray(res) ? res : res.messages || [];
|
|
242
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* One server-derived projection for busy/idle, current tool, conversation,
|
|
467
|
+
* goal, and live children. Streams provide immediacy; this snapshot settles
|
|
468
|
+
* state after reconnects and prevents each UI from inventing lifecycle rules.
|
|
469
|
+
*/
|
|
470
|
+
async getSessionState(threadId, limit = 500) {
|
|
471
|
+
return this.json(`/api/threads/${threadId}/session_state?limit=${limit}`);
|
|
472
|
+
}
|
|
243
473
|
async getLogs(threadId, limit = 100) {
|
|
244
474
|
const res = await this.json(
|
|
245
475
|
`/api/threads/${threadId}/logs?limit=${limit}&order=desc`
|
|
@@ -403,12 +633,6 @@ var ApiClient = class {
|
|
|
403
633
|
async compact(threadId) {
|
|
404
634
|
await this.json(`/api/threads/${threadId}/compact`, { method: "POST" });
|
|
405
635
|
}
|
|
406
|
-
async stop(threadId) {
|
|
407
|
-
try {
|
|
408
|
-
await this.json(`/api/threads/${threadId}/stop`, { method: "POST" });
|
|
409
|
-
} catch {
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
636
|
/**
|
|
413
637
|
* Run a user-typed `!command` on the thread's execution owner (wherever the
|
|
414
638
|
* session runs — e.g. a remote VPS daemon). The instance forwards it over
|
|
@@ -504,6 +728,7 @@ var Heartbeat = class {
|
|
|
504
728
|
this.onDead = onDead;
|
|
505
729
|
this.intervalMs = options.intervalMs ?? HEARTBEAT_INTERVAL_MS;
|
|
506
730
|
this.silenceMs = options.silenceMs ?? CONNECTION_SILENCE_TIMEOUT_MS;
|
|
731
|
+
this.request = options.request ?? "ping";
|
|
507
732
|
}
|
|
508
733
|
ws;
|
|
509
734
|
onDead;
|
|
@@ -511,6 +736,7 @@ var Heartbeat = class {
|
|
|
511
736
|
lastRecvAt = 0;
|
|
512
737
|
intervalMs;
|
|
513
738
|
silenceMs;
|
|
739
|
+
request;
|
|
514
740
|
start() {
|
|
515
741
|
this.stop();
|
|
516
742
|
this.lastRecvAt = Date.now();
|
|
@@ -520,6 +746,10 @@ var Heartbeat = class {
|
|
|
520
746
|
markAlive() {
|
|
521
747
|
this.lastRecvAt = Date.now();
|
|
522
748
|
}
|
|
749
|
+
/** Change the heartbeat frame without restarting the connection timer. */
|
|
750
|
+
setRequest(request) {
|
|
751
|
+
this.request = request;
|
|
752
|
+
}
|
|
523
753
|
stop() {
|
|
524
754
|
if (this.timer) {
|
|
525
755
|
clearInterval(this.timer);
|
|
@@ -532,7 +762,7 @@ var Heartbeat = class {
|
|
|
532
762
|
return;
|
|
533
763
|
}
|
|
534
764
|
try {
|
|
535
|
-
if (this.ws.readyState === WebSocket.OPEN) this.ws.send(
|
|
765
|
+
if (this.ws.readyState === WebSocket.OPEN) this.ws.send(this.request);
|
|
536
766
|
else this.fail();
|
|
537
767
|
} catch {
|
|
538
768
|
this.fail();
|
|
@@ -554,6 +784,56 @@ var DIM = "\x1B[2m";
|
|
|
554
784
|
var ADD_BG = "\x1B[48;5;22m\x1B[38;5;254m";
|
|
555
785
|
var DEL_BG = "\x1B[48;5;52m\x1B[38;5;254m";
|
|
556
786
|
var MAX_SIDE_LINES = 4;
|
|
787
|
+
function terminalGlyphWidth(ch) {
|
|
788
|
+
const cp = ch.codePointAt(0);
|
|
789
|
+
return cp >= 4352 && cp <= 4447 || cp >= 11904 && cp <= 42191 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65072 && cp <= 65103 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 127744 && cp <= 129791 || cp >= 131072 ? 2 : 1;
|
|
790
|
+
}
|
|
791
|
+
function truncateMiddle(text, maxColumns) {
|
|
792
|
+
const limit = Math.max(0, Math.floor(maxColumns));
|
|
793
|
+
if (limit === 0) return "";
|
|
794
|
+
const chars = [...text];
|
|
795
|
+
const totalWidth = chars.reduce((width, ch) => width + terminalGlyphWidth(ch), 0);
|
|
796
|
+
if (totalWidth <= limit) return text;
|
|
797
|
+
if (limit === 1) return "\u2026";
|
|
798
|
+
const available = limit - 1;
|
|
799
|
+
const headBudget = Math.ceil(available / 2);
|
|
800
|
+
const tailBudget = Math.floor(available / 2);
|
|
801
|
+
const head = [];
|
|
802
|
+
const tail = [];
|
|
803
|
+
let headWidth = 0;
|
|
804
|
+
let tailWidth = 0;
|
|
805
|
+
for (let i = 0; i < chars.length; i++) {
|
|
806
|
+
const width = terminalGlyphWidth(chars[i]);
|
|
807
|
+
if (headWidth + width > headBudget) break;
|
|
808
|
+
head.push(chars[i]);
|
|
809
|
+
headWidth += width;
|
|
810
|
+
}
|
|
811
|
+
for (let i = chars.length - 1; i >= head.length; i--) {
|
|
812
|
+
const width = terminalGlyphWidth(chars[i]);
|
|
813
|
+
if (tailWidth + width > tailBudget) break;
|
|
814
|
+
tail.unshift(chars[i]);
|
|
815
|
+
tailWidth += width;
|
|
816
|
+
}
|
|
817
|
+
const isSeparator = (ch) => ch === "/" || ch === "\\";
|
|
818
|
+
let headEnd = head.length;
|
|
819
|
+
let tailStart = chars.length - tail.length;
|
|
820
|
+
for (let i = headEnd - 1; i >= 0; i--) {
|
|
821
|
+
if (isSeparator(chars[i])) {
|
|
822
|
+
headEnd = i + 1;
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
for (let i = tailStart; i < chars.length; i++) {
|
|
827
|
+
if (isSeparator(chars[i])) {
|
|
828
|
+
tailStart = i;
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (headEnd < tailStart && headEnd > 0 && tailStart < chars.length) {
|
|
833
|
+
return `${chars.slice(0, headEnd).join("")}\u2026${chars.slice(tailStart).join("")}`;
|
|
834
|
+
}
|
|
835
|
+
return `${head.join("")}\u2026${tail.join("")}`;
|
|
836
|
+
}
|
|
557
837
|
function clamp(s, max) {
|
|
558
838
|
return s.length > max ? s.slice(0, Math.max(0, max - 1)) + "\u2026" : s;
|
|
559
839
|
}
|
|
@@ -727,6 +1007,7 @@ var Bridge = class {
|
|
|
727
1007
|
ws = null;
|
|
728
1008
|
closed = false;
|
|
729
1009
|
heartbeat = null;
|
|
1010
|
+
activeToolRequests = 0;
|
|
730
1011
|
reconnectAttempt = 0;
|
|
731
1012
|
reconnectTimer = null;
|
|
732
1013
|
resolveConnected = null;
|
|
@@ -850,9 +1131,14 @@ var Bridge = class {
|
|
|
850
1131
|
}
|
|
851
1132
|
startHeartbeat(ws) {
|
|
852
1133
|
this.stopHeartbeat();
|
|
853
|
-
this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws)
|
|
1134
|
+
this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), {
|
|
1135
|
+
request: this.activeToolRequests > 0 ? "ping" : "stream_ping"
|
|
1136
|
+
});
|
|
854
1137
|
this.heartbeat.start();
|
|
855
1138
|
}
|
|
1139
|
+
updateHeartbeatMode() {
|
|
1140
|
+
this.heartbeat?.setRequest(this.activeToolRequests > 0 ? "ping" : "stream_ping");
|
|
1141
|
+
}
|
|
856
1142
|
stopHeartbeat() {
|
|
857
1143
|
if (this.heartbeat) {
|
|
858
1144
|
this.heartbeat.stop();
|
|
@@ -938,7 +1224,14 @@ var Bridge = class {
|
|
|
938
1224
|
if (msg.type !== "tool_request") return;
|
|
939
1225
|
if (!this.owner) return;
|
|
940
1226
|
const req = msg;
|
|
941
|
-
|
|
1227
|
+
this.activeToolRequests++;
|
|
1228
|
+
this.updateHeartbeatMode();
|
|
1229
|
+
try {
|
|
1230
|
+
await this.handleToolRequest(req);
|
|
1231
|
+
} finally {
|
|
1232
|
+
this.activeToolRequests = Math.max(0, this.activeToolRequests - 1);
|
|
1233
|
+
this.updateHeartbeatMode();
|
|
1234
|
+
}
|
|
942
1235
|
}
|
|
943
1236
|
/**
|
|
944
1237
|
* Reply to a tool request. Durable calls (the agent parked them) deliver the
|
|
@@ -1112,7 +1405,7 @@ function detailSuffix(tool, result) {
|
|
|
1112
1405
|
const lines = result.split("\n").length;
|
|
1113
1406
|
return ` (${lines} line${lines === 1 ? "" : "s"})`;
|
|
1114
1407
|
}
|
|
1115
|
-
var LOG_DIR = path3.join(
|
|
1408
|
+
var LOG_DIR = path3.join(os7.homedir(), ".standardagents", "process-logs");
|
|
1116
1409
|
var KEY2 = "bg_processes";
|
|
1117
1410
|
function isAlive(pid) {
|
|
1118
1411
|
try {
|
|
@@ -1189,7 +1482,7 @@ var ProcessRegistry = class {
|
|
|
1189
1482
|
}
|
|
1190
1483
|
};
|
|
1191
1484
|
function configFile() {
|
|
1192
|
-
return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(
|
|
1485
|
+
return process.env.STANDARDAGENTS_MCP_CONFIG || path3.join(os7.homedir(), ".standardagents", "mcp.json");
|
|
1193
1486
|
}
|
|
1194
1487
|
function loadMcpConfig() {
|
|
1195
1488
|
try {
|
|
@@ -1491,7 +1784,7 @@ var HostTools = class {
|
|
|
1491
1784
|
}
|
|
1492
1785
|
}
|
|
1493
1786
|
const hash = crypto.createHash("sha256").update(JSON.stringify(files)).digest("hex").slice(0, 12);
|
|
1494
|
-
const skillDir = path3.join(
|
|
1787
|
+
const skillDir = path3.join(os7.tmpdir(), "standardcode-skills", `${skill}-${hash}`);
|
|
1495
1788
|
for (const f of files) {
|
|
1496
1789
|
const dest = path3.resolve(skillDir, f.path);
|
|
1497
1790
|
if (path3.relative(skillDir, dest).startsWith("..")) {
|
|
@@ -2418,6 +2711,7 @@ var MessageStream = class {
|
|
|
2418
2711
|
ws.addEventListener("open", () => {
|
|
2419
2712
|
this.reconnectAttempt = 0;
|
|
2420
2713
|
this.startHeartbeat(ws);
|
|
2714
|
+
this.hooks.onOpen?.();
|
|
2421
2715
|
this.resolveConnected?.();
|
|
2422
2716
|
});
|
|
2423
2717
|
ws.addEventListener("message", (ev) => {
|
|
@@ -2435,7 +2729,7 @@ var MessageStream = class {
|
|
|
2435
2729
|
}
|
|
2436
2730
|
startHeartbeat(ws) {
|
|
2437
2731
|
this.stopHeartbeat();
|
|
2438
|
-
this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
|
|
2732
|
+
this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
|
|
2439
2733
|
this.heartbeat.start();
|
|
2440
2734
|
}
|
|
2441
2735
|
stopHeartbeat() {
|
|
@@ -2484,10 +2778,11 @@ var MessageStream = class {
|
|
|
2484
2778
|
}
|
|
2485
2779
|
if (msg.type === "message_data" && (msg.depth ?? 0) === 0) {
|
|
2486
2780
|
const data = msg.data || {};
|
|
2781
|
+
this.hooks.onMessage?.(data);
|
|
2487
2782
|
if (data.role === "assistant" && typeof data.content === "string" && data.content.trim()) {
|
|
2488
2783
|
const tc = data.tool_calls;
|
|
2489
|
-
const
|
|
2490
|
-
this.hooks.onAssistantText(data.content,
|
|
2784
|
+
const hasToolCalls = Array.isArray(tc) ? tc.length > 0 : typeof tc === "string" && tc.trim() !== "" && tc.trim() !== "null" && tc.trim() !== "[]";
|
|
2785
|
+
this.hooks.onAssistantText(data.content, hasToolCalls);
|
|
2491
2786
|
}
|
|
2492
2787
|
if (data.role === "assistant" && data.status === "failed" && data.error) {
|
|
2493
2788
|
this.hooks.onError(String(data.error));
|
|
@@ -2537,7 +2832,7 @@ var SystemEvents = class {
|
|
|
2537
2832
|
}
|
|
2538
2833
|
startHeartbeat(ws) {
|
|
2539
2834
|
this.stopHeartbeat();
|
|
2540
|
-
this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws));
|
|
2835
|
+
this.heartbeat = new Heartbeat(ws, () => this.handleDrop(ws), { request: "stream_ping" });
|
|
2541
2836
|
this.heartbeat.start();
|
|
2542
2837
|
}
|
|
2543
2838
|
stopHeartbeat() {
|
|
@@ -2670,6 +2965,664 @@ var SubagentActivity = class {
|
|
|
2670
2965
|
}
|
|
2671
2966
|
};
|
|
2672
2967
|
|
|
2968
|
+
// src/session-state.ts
|
|
2969
|
+
var emptyLiveDraft = () => ({ version: 1, text: "", messageIds: [] });
|
|
2970
|
+
function normalizeLiveDraft(state) {
|
|
2971
|
+
return {
|
|
2972
|
+
version: 1,
|
|
2973
|
+
text: typeof state?.text === "string" ? state.text : "",
|
|
2974
|
+
messageIds: Array.isArray(state?.messageIds) ? [...new Set(state.messageIds.filter((id) => typeof id === "string" && !!id))] : []
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
function appendLiveDraft(state, chunks) {
|
|
2978
|
+
const current = normalizeLiveDraft(state);
|
|
2979
|
+
const nextIds = [...current.messageIds];
|
|
2980
|
+
let text = current.text;
|
|
2981
|
+
for (const chunk of Array.isArray(chunks) ? chunks : [chunks]) {
|
|
2982
|
+
if (typeof chunk?.text !== "string" || !chunk.text) continue;
|
|
2983
|
+
text += chunk.text;
|
|
2984
|
+
if (typeof chunk.messageId === "string" && chunk.messageId && !nextIds.includes(chunk.messageId)) {
|
|
2985
|
+
nextIds.push(chunk.messageId);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return { version: 1, text, messageIds: nextIds.slice(-64) };
|
|
2989
|
+
}
|
|
2990
|
+
var createdAt = (message) => Number(message.created_at ?? message.createdAt ?? 0);
|
|
2991
|
+
function messageText(content) {
|
|
2992
|
+
if (typeof content === "string") return content;
|
|
2993
|
+
if (Array.isArray(content)) {
|
|
2994
|
+
return content.map((block) => typeof block === "string" ? block : typeof block?.text === "string" ? block.text : "").join("");
|
|
2995
|
+
}
|
|
2996
|
+
return "";
|
|
2997
|
+
}
|
|
2998
|
+
function parseObject(value) {
|
|
2999
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
3000
|
+
if (typeof value !== "string" || !value.trim()) return {};
|
|
3001
|
+
try {
|
|
3002
|
+
const parsed = JSON.parse(value);
|
|
3003
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3004
|
+
} catch {
|
|
3005
|
+
return {};
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
function parseToolCalls(message) {
|
|
3009
|
+
if (Array.isArray(message.toolCalls)) return message.toolCalls;
|
|
3010
|
+
let raw = message.tool_calls;
|
|
3011
|
+
if (typeof raw === "string") {
|
|
3012
|
+
try {
|
|
3013
|
+
raw = JSON.parse(raw);
|
|
3014
|
+
} catch {
|
|
3015
|
+
return [];
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
if (!Array.isArray(raw)) return [];
|
|
3019
|
+
return raw.flatMap((item) => {
|
|
3020
|
+
const id = typeof item?.id === "string" ? item.id : "";
|
|
3021
|
+
const nameValue = item?.function?.name ?? item?.name;
|
|
3022
|
+
const name = typeof nameValue === "string" ? nameValue : "";
|
|
3023
|
+
if (!id || !name) return [];
|
|
3024
|
+
return [{ id, name, arguments: parseObject(item?.function?.arguments ?? item?.arguments ?? item?.args) }];
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
function deriveSessionActivity(messages) {
|
|
3028
|
+
const visible = messages.filter((message) => message.silent !== true && message.metadata?.silent !== true).sort((a, b) => createdAt(a) - createdAt(b));
|
|
3029
|
+
if (visible.some((message) => message.status === "pending")) return { busy: true, currentTool: unresolvedTool(visible) };
|
|
3030
|
+
const last = visible.at(-1);
|
|
3031
|
+
if (!last) return { busy: false, currentTool: null };
|
|
3032
|
+
const currentTool = unresolvedTool(visible);
|
|
3033
|
+
if (currentTool) return { busy: true, currentTool };
|
|
3034
|
+
if (last.role === "user" || last.role === "tool") return { busy: true, currentTool: null };
|
|
3035
|
+
if (last.role === "assistant" && last.status !== "failed" && !messageText(last.content).trim()) {
|
|
3036
|
+
return { busy: true, currentTool: null };
|
|
3037
|
+
}
|
|
3038
|
+
return { busy: false, currentTool: null };
|
|
3039
|
+
}
|
|
3040
|
+
function threadBusy(messages) {
|
|
3041
|
+
return deriveSessionActivity(messages).busy;
|
|
3042
|
+
}
|
|
3043
|
+
function unresolvedTool(messages) {
|
|
3044
|
+
const resultIds = new Set(messages.flatMap((message) => {
|
|
3045
|
+
const id = message.tool_call_id ?? message.toolCallId;
|
|
3046
|
+
return message.role === "tool" && typeof id === "string" ? [id] : [];
|
|
3047
|
+
}));
|
|
3048
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
3049
|
+
if (messages[i].role === "user") return null;
|
|
3050
|
+
if (messages[i].role !== "assistant") continue;
|
|
3051
|
+
const calls = parseToolCalls(messages[i]);
|
|
3052
|
+
if (!calls.length) return null;
|
|
3053
|
+
return calls.find((tool) => !resultIds.has(tool.id)) ?? null;
|
|
3054
|
+
}
|
|
3055
|
+
return null;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
// src/transcript-delivery.ts
|
|
3059
|
+
function transcriptMessageReady(message, text) {
|
|
3060
|
+
if (message.status === "pending") return false;
|
|
3061
|
+
if (message.role === "assistant" && message.status !== "failed" && !text.trim()) return false;
|
|
3062
|
+
return true;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
// src/progressive-markdown.ts
|
|
3066
|
+
var punctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
|
|
3067
|
+
var isWhitespace = (value) => value === void 0 || /\s/.test(value);
|
|
3068
|
+
var isPunctuation = (value) => value !== void 0 && punctuation.test(value);
|
|
3069
|
+
function runLength(source, start, marker) {
|
|
3070
|
+
let end = start;
|
|
3071
|
+
while (source[end] === marker) end++;
|
|
3072
|
+
return end - start;
|
|
3073
|
+
}
|
|
3074
|
+
function delimiterFlanking(source, at, length, marker) {
|
|
3075
|
+
const previous = at > 0 && source[at - 1] !== "\n" ? source[at - 1] : void 0;
|
|
3076
|
+
const next = at + length < source.length && source[at + length] !== "\n" ? source[at + length] : void 0;
|
|
3077
|
+
const previousWhitespace = isWhitespace(previous);
|
|
3078
|
+
const nextWhitespace = isWhitespace(next);
|
|
3079
|
+
const previousPunctuation = isPunctuation(previous);
|
|
3080
|
+
const nextPunctuation = isPunctuation(next);
|
|
3081
|
+
const leftFlanking = !nextWhitespace && (!nextPunctuation || previousWhitespace || previousPunctuation);
|
|
3082
|
+
const rightFlanking = !previousWhitespace && (!previousPunctuation || nextWhitespace || nextPunctuation);
|
|
3083
|
+
if (marker === "_") {
|
|
3084
|
+
return {
|
|
3085
|
+
canOpen: leftFlanking && (!rightFlanking || previousPunctuation),
|
|
3086
|
+
canClose: rightFlanking && (!leftFlanking || nextPunctuation)
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
return { canOpen: leftFlanking, canClose: rightFlanking };
|
|
3090
|
+
}
|
|
3091
|
+
function sameStyles(a, b) {
|
|
3092
|
+
return a.length === b.length && a.every((style, index) => style === b[index]);
|
|
3093
|
+
}
|
|
3094
|
+
function parseProgressiveInline(source, baseOffset = 0) {
|
|
3095
|
+
const runs = [];
|
|
3096
|
+
const stack = [];
|
|
3097
|
+
let inlineTicks = 0;
|
|
3098
|
+
let i = 0;
|
|
3099
|
+
const styles = () => [
|
|
3100
|
+
...stack.map((frame) => frame.style),
|
|
3101
|
+
...inlineTicks > 0 ? ["code"] : []
|
|
3102
|
+
];
|
|
3103
|
+
const append = (text, at, href) => {
|
|
3104
|
+
if (!text) return;
|
|
3105
|
+
const active = styles();
|
|
3106
|
+
const last = runs.at(-1);
|
|
3107
|
+
if (last && last._end === at && last.href === href && sameStyles(last.styles, active)) {
|
|
3108
|
+
last.text += text;
|
|
3109
|
+
last._end = at + text.length;
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
runs.push({ id: `i:${baseOffset + at}`, text, styles: active, ...{}, _end: at + text.length });
|
|
3113
|
+
};
|
|
3114
|
+
while (i < source.length) {
|
|
3115
|
+
const character = source[i];
|
|
3116
|
+
if (character === "\\") {
|
|
3117
|
+
if (i + 1 < source.length && isPunctuation(source[i + 1])) {
|
|
3118
|
+
append(source[i + 1], i + 1);
|
|
3119
|
+
i += 2;
|
|
3120
|
+
continue;
|
|
3121
|
+
}
|
|
3122
|
+
if (i + 1 === source.length) {
|
|
3123
|
+
i++;
|
|
3124
|
+
continue;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
if (inlineTicks > 0) {
|
|
3128
|
+
if (character === "`") {
|
|
3129
|
+
const length = runLength(source, i, "`");
|
|
3130
|
+
if (length === inlineTicks) {
|
|
3131
|
+
inlineTicks = 0;
|
|
3132
|
+
i += length;
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3135
|
+
if (i + length === source.length) {
|
|
3136
|
+
i += length;
|
|
3137
|
+
continue;
|
|
3138
|
+
}
|
|
3139
|
+
append("`".repeat(length), i);
|
|
3140
|
+
i += length;
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
append(character, i);
|
|
3144
|
+
i++;
|
|
3145
|
+
continue;
|
|
3146
|
+
}
|
|
3147
|
+
if (character === "`") {
|
|
3148
|
+
const length = runLength(source, i, "`");
|
|
3149
|
+
if (i + length < source.length) inlineTicks = length;
|
|
3150
|
+
i += length;
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
if (character === "[") {
|
|
3154
|
+
const destinationAt = source.indexOf("](", i + 1);
|
|
3155
|
+
if (destinationAt >= 0) {
|
|
3156
|
+
const destinationEnd = source.indexOf(")", destinationAt + 2);
|
|
3157
|
+
const end = destinationEnd >= 0 ? destinationEnd : source.length;
|
|
3158
|
+
const href = source.slice(destinationAt + 2, end);
|
|
3159
|
+
const labelStart = i + 1;
|
|
3160
|
+
const labelRuns = parseProgressiveInline(source.slice(labelStart, destinationAt), baseOffset + labelStart);
|
|
3161
|
+
for (const run3 of labelRuns) {
|
|
3162
|
+
runs.push({
|
|
3163
|
+
...run3,
|
|
3164
|
+
styles: run3.styles.includes("link") ? run3.styles : [...run3.styles, "link"],
|
|
3165
|
+
...destinationEnd >= 0 && href ? { href } : {},
|
|
3166
|
+
_end: run3.id.startsWith("i:") ? Number(run3.id.slice(2)) - baseOffset + run3.text.length : end
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
i = destinationEnd >= 0 ? destinationEnd + 1 : source.length;
|
|
3170
|
+
continue;
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
if (character === "*" || character === "_" || character === "~") {
|
|
3174
|
+
const run3 = runLength(source, i, character);
|
|
3175
|
+
const usable = character === "~" ? run3 - run3 % 2 : run3;
|
|
3176
|
+
if (usable > 0) {
|
|
3177
|
+
const { canOpen, canClose } = delimiterFlanking(source, i, run3, character);
|
|
3178
|
+
const frames = [];
|
|
3179
|
+
if (character === "~") {
|
|
3180
|
+
for (let n = 0; n < usable; n += 2) frames.push({ marker: "~", length: 2, style: "strike" });
|
|
3181
|
+
} else {
|
|
3182
|
+
let remaining = usable;
|
|
3183
|
+
while (remaining >= 2) {
|
|
3184
|
+
frames.push({ marker: character, length: 2, style: "strong" });
|
|
3185
|
+
remaining -= 2;
|
|
3186
|
+
}
|
|
3187
|
+
if (remaining) frames.push({ marker: character, length: 1, style: "emphasis" });
|
|
3188
|
+
}
|
|
3189
|
+
let consumed = 0;
|
|
3190
|
+
let closed = 0;
|
|
3191
|
+
if (canClose) {
|
|
3192
|
+
for (const frame of [...frames].reverse()) {
|
|
3193
|
+
const top = stack.at(-1);
|
|
3194
|
+
if (top && top.marker === frame.marker && top.length === frame.length) {
|
|
3195
|
+
stack.pop();
|
|
3196
|
+
consumed += frame.length;
|
|
3197
|
+
closed += frame.length;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
if (canOpen) {
|
|
3202
|
+
for (const frame of frames) {
|
|
3203
|
+
if (consumed >= frame.length) consumed -= frame.length;
|
|
3204
|
+
else stack.push(frame);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
if (canOpen || closed > 0) {
|
|
3208
|
+
i += usable;
|
|
3209
|
+
if (run3 > usable) append(character.repeat(run3 - usable), i);
|
|
3210
|
+
i += run3 - usable;
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
if (i + run3 === source.length) {
|
|
3214
|
+
i += run3;
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (usable === 0 && i + run3 === source.length) {
|
|
3219
|
+
i += run3;
|
|
3220
|
+
continue;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
append(character, i);
|
|
3224
|
+
i++;
|
|
3225
|
+
}
|
|
3226
|
+
return runs.map(({ _end: _, ...run3 }) => run3);
|
|
3227
|
+
}
|
|
3228
|
+
function sourceLines(source) {
|
|
3229
|
+
if (!source) return [];
|
|
3230
|
+
const lines = [];
|
|
3231
|
+
let start = 0;
|
|
3232
|
+
while (start <= source.length) {
|
|
3233
|
+
const newline = source.indexOf("\n", start);
|
|
3234
|
+
if (newline < 0) {
|
|
3235
|
+
lines.push({ text: source.slice(start).replace(/\r$/, ""), start, end: source.length, next: source.length, terminated: false });
|
|
3236
|
+
break;
|
|
3237
|
+
}
|
|
3238
|
+
lines.push({ text: source.slice(start, newline).replace(/\r$/, ""), start, end: newline, next: newline + 1, terminated: true });
|
|
3239
|
+
start = newline + 1;
|
|
3240
|
+
if (start === source.length) {
|
|
3241
|
+
lines.push({ text: "", start, end: start, next: start, terminated: false });
|
|
3242
|
+
break;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
return lines;
|
|
3246
|
+
}
|
|
3247
|
+
function blockFence(line) {
|
|
3248
|
+
const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
3249
|
+
if (!match) return null;
|
|
3250
|
+
const marker = match[1][0];
|
|
3251
|
+
const info = match[2];
|
|
3252
|
+
if (marker === "`" && info.includes("`")) return null;
|
|
3253
|
+
return { marker, length: match[1].length, info: info.trim() };
|
|
3254
|
+
}
|
|
3255
|
+
var tableSeparator = (line) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(line) && line.includes("-");
|
|
3256
|
+
var partialTableSeparator = (line) => /^\s*\|?\s*:?-*:?\s*(\|\s*:?-*:?\s*)*\|?\s*$/.test(line) && /[-|:]/.test(line);
|
|
3257
|
+
function tableCells(line) {
|
|
3258
|
+
let start = 0;
|
|
3259
|
+
let end = line.length;
|
|
3260
|
+
while (start < end && /\s/.test(line[start])) start++;
|
|
3261
|
+
while (end > start && /\s/.test(line[end - 1])) end--;
|
|
3262
|
+
if (line[start] === "|") start++;
|
|
3263
|
+
if (line[end - 1] === "|") end--;
|
|
3264
|
+
const cells = [];
|
|
3265
|
+
let cellStart = start;
|
|
3266
|
+
for (let i = start; i <= end; i++) {
|
|
3267
|
+
if (i === end || line[i] === "|" && (i === 0 || line[i - 1] !== "\\")) {
|
|
3268
|
+
const raw = line.slice(cellStart, i);
|
|
3269
|
+
const leading = raw.match(/^\s*/)?.[0].length ?? 0;
|
|
3270
|
+
cells.push({ text: raw.trim().replace(/\\\|/g, "|"), offset: cellStart + leading });
|
|
3271
|
+
cellStart = i + 1;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
return cells;
|
|
3275
|
+
}
|
|
3276
|
+
function cell(text, offset) {
|
|
3277
|
+
return { id: `cell:${offset}`, runs: parseProgressiveInline(text, offset) };
|
|
3278
|
+
}
|
|
3279
|
+
function startsSpecialBlock(lines, index) {
|
|
3280
|
+
const line = lines[index]?.text ?? "";
|
|
3281
|
+
const next = lines[index + 1]?.text;
|
|
3282
|
+
return !!blockFence(line) || /^#{1,6}\s+/.test(line) || /^\s*>\s?/.test(line) || /^\s*[-*+]\s+/.test(line) || /^\s*\d+[.)]\s+/.test(line) || /^\s*([-*_])(\s*\1){2,}\s*$/.test(line) || /^ {0,3}\|/.test(line) || line.includes("|") && next !== void 0 && tableSeparator(next);
|
|
3283
|
+
}
|
|
3284
|
+
function parseStreamingMarkdown(source) {
|
|
3285
|
+
const lines = sourceLines(source);
|
|
3286
|
+
const blocks = [];
|
|
3287
|
+
let i = 0;
|
|
3288
|
+
while (i < lines.length) {
|
|
3289
|
+
const line = lines[i];
|
|
3290
|
+
if (!line.text.trim()) {
|
|
3291
|
+
i++;
|
|
3292
|
+
continue;
|
|
3293
|
+
}
|
|
3294
|
+
const id = `b:${line.start}`;
|
|
3295
|
+
const opening = blockFence(line.text);
|
|
3296
|
+
if (opening) {
|
|
3297
|
+
const codeStart = line.next;
|
|
3298
|
+
let j2 = i + 1;
|
|
3299
|
+
let closing;
|
|
3300
|
+
while (j2 < lines.length) {
|
|
3301
|
+
const candidate = blockFence(lines[j2].text);
|
|
3302
|
+
if (candidate && candidate.marker === opening.marker && candidate.length >= opening.length && !candidate.info) {
|
|
3303
|
+
closing = lines[j2];
|
|
3304
|
+
break;
|
|
3305
|
+
}
|
|
3306
|
+
j2++;
|
|
3307
|
+
}
|
|
3308
|
+
let codeEnd = closing?.start ?? source.length;
|
|
3309
|
+
if (!closing) {
|
|
3310
|
+
const tailStart = Math.max(codeStart, source.lastIndexOf("\n") + 1);
|
|
3311
|
+
const tail = source.slice(tailStart);
|
|
3312
|
+
const pending = /^ {0,3}(`+|~+)[ \t]*$/.exec(tail);
|
|
3313
|
+
if (pending && pending[1][0] === opening.marker && pending[1].length < opening.length) codeEnd = tailStart;
|
|
3314
|
+
}
|
|
3315
|
+
if (codeEnd > codeStart && source[codeEnd - 1] === "\n") codeEnd--;
|
|
3316
|
+
blocks.push({
|
|
3317
|
+
id,
|
|
3318
|
+
kind: "code",
|
|
3319
|
+
start: line.start,
|
|
3320
|
+
end: closing?.next ?? source.length,
|
|
3321
|
+
complete: !!closing && closing.terminated,
|
|
3322
|
+
language: opening.info || "code",
|
|
3323
|
+
text: source.slice(codeStart, codeEnd)
|
|
3324
|
+
});
|
|
3325
|
+
i = closing ? j2 + 1 : lines.length;
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line.text);
|
|
3329
|
+
if (heading) {
|
|
3330
|
+
const contentAt = line.start + heading[1].length + 1;
|
|
3331
|
+
blocks.push({
|
|
3332
|
+
id,
|
|
3333
|
+
kind: "heading",
|
|
3334
|
+
start: line.start,
|
|
3335
|
+
end: line.end,
|
|
3336
|
+
complete: line.terminated,
|
|
3337
|
+
level: heading[1].length,
|
|
3338
|
+
runs: parseProgressiveInline(heading[2], contentAt)
|
|
3339
|
+
});
|
|
3340
|
+
i++;
|
|
3341
|
+
continue;
|
|
3342
|
+
}
|
|
3343
|
+
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line.text)) {
|
|
3344
|
+
blocks.push({ id, kind: "rule", start: line.start, end: line.end, complete: line.terminated });
|
|
3345
|
+
i++;
|
|
3346
|
+
continue;
|
|
3347
|
+
}
|
|
3348
|
+
const separatorNext = line.text.includes("|") && i + 1 < lines.length && tableSeparator(lines[i + 1].text);
|
|
3349
|
+
if (separatorNext) {
|
|
3350
|
+
const separator = tableCells(lines[i + 1].text);
|
|
3351
|
+
const align = separator.map(({ text: text2 }) => text2.startsWith(":") && text2.endsWith(":") ? "center" : text2.endsWith(":") ? "right" : text2.startsWith(":") ? "left" : "");
|
|
3352
|
+
const header = tableCells(line.text).map((entry) => cell(entry.text, line.start + entry.offset));
|
|
3353
|
+
const rows = [];
|
|
3354
|
+
let j2 = i + 2;
|
|
3355
|
+
while (j2 < lines.length && lines[j2].text.trim() && lines[j2].text.includes("|")) {
|
|
3356
|
+
rows.push(tableCells(lines[j2].text).map((entry) => cell(entry.text, lines[j2].start + entry.offset)));
|
|
3357
|
+
j2++;
|
|
3358
|
+
}
|
|
3359
|
+
blocks.push({
|
|
3360
|
+
id,
|
|
3361
|
+
kind: "table",
|
|
3362
|
+
start: line.start,
|
|
3363
|
+
end: lines[Math.max(i + 1, j2 - 1)].end,
|
|
3364
|
+
complete: j2 < lines.length && lines[j2].terminated,
|
|
3365
|
+
header,
|
|
3366
|
+
rows,
|
|
3367
|
+
align
|
|
3368
|
+
});
|
|
3369
|
+
i = j2;
|
|
3370
|
+
continue;
|
|
3371
|
+
}
|
|
3372
|
+
if (/^ {0,3}\|/.test(line.text)) {
|
|
3373
|
+
const streamingSeparator = i + 1 >= lines.length || !lines[i + 1].terminated && (!lines[i + 1].text || partialTableSeparator(lines[i + 1].text));
|
|
3374
|
+
if (streamingSeparator) {
|
|
3375
|
+
const header = tableCells(line.text).map((entry) => cell(entry.text, line.start + entry.offset));
|
|
3376
|
+
if (header.some((entry) => entry.runs.length)) {
|
|
3377
|
+
blocks.push({
|
|
3378
|
+
id,
|
|
3379
|
+
kind: "table",
|
|
3380
|
+
start: line.start,
|
|
3381
|
+
end: lines[Math.min(i + 1, lines.length - 1)].end,
|
|
3382
|
+
complete: false,
|
|
3383
|
+
header,
|
|
3384
|
+
rows: []
|
|
3385
|
+
});
|
|
3386
|
+
} else {
|
|
3387
|
+
blocks.push({ id, kind: "paragraph", start: line.start, end: line.end, complete: false, runs: [] });
|
|
3388
|
+
}
|
|
3389
|
+
i = lines.length;
|
|
3390
|
+
continue;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
const quote = /^\s*>\s?(.*)$/.exec(line.text);
|
|
3394
|
+
if (quote) {
|
|
3395
|
+
const parts = [];
|
|
3396
|
+
let j2 = i;
|
|
3397
|
+
let firstContentAt = line.start + line.text.indexOf(quote[1]);
|
|
3398
|
+
while (j2 < lines.length) {
|
|
3399
|
+
const match = /^\s*>\s?(.*)$/.exec(lines[j2].text);
|
|
3400
|
+
if (!match) break;
|
|
3401
|
+
if (!parts.length) firstContentAt = lines[j2].start + lines[j2].text.indexOf(match[1]);
|
|
3402
|
+
parts.push(match[1]);
|
|
3403
|
+
j2++;
|
|
3404
|
+
}
|
|
3405
|
+
blocks.push({
|
|
3406
|
+
id,
|
|
3407
|
+
kind: "quote",
|
|
3408
|
+
start: line.start,
|
|
3409
|
+
end: lines[j2 - 1].end,
|
|
3410
|
+
complete: j2 < lines.length && lines[j2].terminated,
|
|
3411
|
+
runs: parseProgressiveInline(parts.join("\n"), firstContentAt)
|
|
3412
|
+
});
|
|
3413
|
+
i = j2;
|
|
3414
|
+
continue;
|
|
3415
|
+
}
|
|
3416
|
+
const list = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line.text);
|
|
3417
|
+
if (list) {
|
|
3418
|
+
const ordered = /^\d/.test(list[2]);
|
|
3419
|
+
const items = [];
|
|
3420
|
+
let j2 = i;
|
|
3421
|
+
while (j2 < lines.length) {
|
|
3422
|
+
const match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(lines[j2].text);
|
|
3423
|
+
if (!match || /^\d/.test(match[2]) !== ordered) break;
|
|
3424
|
+
const contentAt = lines[j2].start + match[1].length + match[2].length + 1;
|
|
3425
|
+
items.push(cell(match[3], contentAt));
|
|
3426
|
+
j2++;
|
|
3427
|
+
}
|
|
3428
|
+
blocks.push({
|
|
3429
|
+
id,
|
|
3430
|
+
kind: "list",
|
|
3431
|
+
start: line.start,
|
|
3432
|
+
end: lines[j2 - 1].end,
|
|
3433
|
+
complete: j2 < lines.length && lines[j2].terminated,
|
|
3434
|
+
ordered,
|
|
3435
|
+
items
|
|
3436
|
+
});
|
|
3437
|
+
i = j2;
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
if (!line.terminated && (/^ {0,3}#{1,6}\s*$/.test(line.text) || /^\s*[-*_]{1,2}\s*$/.test(line.text))) {
|
|
3441
|
+
blocks.push({ id, kind: "paragraph", start: line.start, end: line.end, complete: false, runs: [] });
|
|
3442
|
+
i++;
|
|
3443
|
+
continue;
|
|
3444
|
+
}
|
|
3445
|
+
const paragraph = [line];
|
|
3446
|
+
let j = i + 1;
|
|
3447
|
+
while (j < lines.length && lines[j].text.trim() && !startsSpecialBlock(lines, j)) {
|
|
3448
|
+
paragraph.push(lines[j]);
|
|
3449
|
+
j++;
|
|
3450
|
+
}
|
|
3451
|
+
const text = paragraph.map((part) => part.text).join("\n");
|
|
3452
|
+
blocks.push({
|
|
3453
|
+
id,
|
|
3454
|
+
kind: "paragraph",
|
|
3455
|
+
start: line.start,
|
|
3456
|
+
end: paragraph.at(-1).end,
|
|
3457
|
+
complete: j < lines.length && lines[j].terminated,
|
|
3458
|
+
runs: parseProgressiveInline(text, line.start)
|
|
3459
|
+
});
|
|
3460
|
+
i = j;
|
|
3461
|
+
}
|
|
3462
|
+
return { version: 3, sourceLength: source.length, blocks };
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
// src/markdown.ts
|
|
3466
|
+
var ESC = "\x1B[";
|
|
3467
|
+
var R = ESC + "0m";
|
|
3468
|
+
var BOLD = ESC + "1m";
|
|
3469
|
+
var DIM3 = ESC + "2m";
|
|
3470
|
+
var ITAL = ESC + "3m";
|
|
3471
|
+
var UNDER = ESC + "4m";
|
|
3472
|
+
var TEAL = ESC + "38;5;37m";
|
|
3473
|
+
var CYAN = ESC + "36m";
|
|
3474
|
+
var GRAY = ESC + "90m";
|
|
3475
|
+
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
3476
|
+
function visibleWidth(s) {
|
|
3477
|
+
return s.replace(ANSI, "").length;
|
|
3478
|
+
}
|
|
3479
|
+
function padEndVisible(s, width) {
|
|
3480
|
+
const pad = width - visibleWidth(s);
|
|
3481
|
+
return pad > 0 ? s + " ".repeat(pad) : s;
|
|
3482
|
+
}
|
|
3483
|
+
function inline(s) {
|
|
3484
|
+
const codes = [];
|
|
3485
|
+
s = s.replace(/`([^`]+)`/g, (_, code) => {
|
|
3486
|
+
codes.push(code);
|
|
3487
|
+
return "\0" + (codes.length - 1) + "\0";
|
|
3488
|
+
});
|
|
3489
|
+
s = s.replace(
|
|
3490
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
3491
|
+
(_, text, url) => `${CYAN}${UNDER}${text}${R} ${DIM3}${url}${R}`
|
|
3492
|
+
);
|
|
3493
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => `${BOLD}${t}${R}`);
|
|
3494
|
+
s = s.replace(/\*([^*\n]+)\*/g, (_, t) => `${ITAL}${t}${R}`);
|
|
3495
|
+
s = s.replace(/__([^_]+)__/g, (_, t) => `${BOLD}${t}${R}`);
|
|
3496
|
+
s = s.replace(/(^|[^\w])_([^_\n]+)_($|[^\w])/g, (_, a, t, b) => `${a}${ITAL}${t}${R}${b}`);
|
|
3497
|
+
s = s.replace(/~~([^~]+)~~/g, (_, t) => `${DIM3}${t}${R}`);
|
|
3498
|
+
s = s.replace(/\x00(\d+)\x00/g, (_, i) => `${TEAL}${codes[+i].replace(/ /g, String.fromCharCode(160))}${R}`);
|
|
3499
|
+
return s;
|
|
3500
|
+
}
|
|
3501
|
+
function wrapStyled(text, width) {
|
|
3502
|
+
if (width < 4 || visibleWidth(text) <= width) return [text];
|
|
3503
|
+
const words = text.split(" ");
|
|
3504
|
+
const lines = [];
|
|
3505
|
+
let cur = "";
|
|
3506
|
+
let curLen = 0;
|
|
3507
|
+
for (const w of words) {
|
|
3508
|
+
const wLen = visibleWidth(w);
|
|
3509
|
+
if (cur === "") {
|
|
3510
|
+
cur = w;
|
|
3511
|
+
curLen = wLen;
|
|
3512
|
+
} else if (curLen + 1 + wLen <= width) {
|
|
3513
|
+
cur += " " + w;
|
|
3514
|
+
curLen += 1 + wLen;
|
|
3515
|
+
} else {
|
|
3516
|
+
lines.push(cur);
|
|
3517
|
+
cur = w;
|
|
3518
|
+
curLen = wLen;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
if (cur !== "" || lines.length === 0) lines.push(cur);
|
|
3522
|
+
return lines;
|
|
3523
|
+
}
|
|
3524
|
+
function wrapBlock(out, cols2, leadFirst, leadRest, leadWidth, text) {
|
|
3525
|
+
const wrapped = wrapStyled(text, Math.max(8, cols2 - leadWidth));
|
|
3526
|
+
wrapped.forEach((ln, idx) => out.push((idx === 0 ? leadFirst : leadRest) + ln));
|
|
3527
|
+
}
|
|
3528
|
+
function renderTable(rows) {
|
|
3529
|
+
const cols2 = Math.max(...rows.map((r) => r.length));
|
|
3530
|
+
const widths = [];
|
|
3531
|
+
for (let c4 = 0; c4 < cols2; c4++) {
|
|
3532
|
+
widths[c4] = Math.max(...rows.map((r) => visibleWidth(inline(r[c4] ?? ""))));
|
|
3533
|
+
}
|
|
3534
|
+
const sep = `${GRAY} \u2502 ${R}`;
|
|
3535
|
+
const out = [];
|
|
3536
|
+
rows.forEach((r, ri) => {
|
|
3537
|
+
const cells = [];
|
|
3538
|
+
for (let c4 = 0; c4 < cols2; c4++) {
|
|
3539
|
+
const raw = r[c4] ?? "";
|
|
3540
|
+
const styled = ri === 0 ? `${BOLD}${inline(raw)}${R}` : inline(raw);
|
|
3541
|
+
cells.push(padEndVisible(styled, widths[c4]));
|
|
3542
|
+
}
|
|
3543
|
+
out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
|
|
3544
|
+
if (ri === 0) {
|
|
3545
|
+
const rule = widths.map((w) => `${GRAY}${"\u2500".repeat(w)}${R}`).join(`${GRAY}\u2500\u253C\u2500${R}`);
|
|
3546
|
+
out.push(" " + rule);
|
|
3547
|
+
}
|
|
3548
|
+
});
|
|
3549
|
+
return out;
|
|
3550
|
+
}
|
|
3551
|
+
function streamingRun(run3) {
|
|
3552
|
+
let text = run3.styles.includes("code") ? run3.text.replace(/ /g, String.fromCharCode(160)) : run3.text;
|
|
3553
|
+
if (run3.styles.includes("code")) text = `${TEAL}${text}${R}`;
|
|
3554
|
+
if (run3.styles.includes("strike")) text = `${DIM3}${text}${R}`;
|
|
3555
|
+
if (run3.styles.includes("strong")) text = `${BOLD}${text}${R}`;
|
|
3556
|
+
if (run3.styles.includes("emphasis")) text = `${ITAL}${text}${R}`;
|
|
3557
|
+
if (run3.href || run3.styles.includes("link")) text = `${CYAN}${UNDER}${text}${R}`;
|
|
3558
|
+
return text;
|
|
3559
|
+
}
|
|
3560
|
+
function streamingInline(runs) {
|
|
3561
|
+
return runs.map(streamingRun).join("");
|
|
3562
|
+
}
|
|
3563
|
+
function plainCell(cell2) {
|
|
3564
|
+
return cell2.runs.map((run3) => run3.text).join("");
|
|
3565
|
+
}
|
|
3566
|
+
function renderStreamingMarkdown(src, cols2 = 80) {
|
|
3567
|
+
const document = parseStreamingMarkdown(src);
|
|
3568
|
+
const out = [];
|
|
3569
|
+
for (const block of document.blocks) {
|
|
3570
|
+
if (out.length && out.at(-1) !== "") out.push("");
|
|
3571
|
+
switch (block.kind) {
|
|
3572
|
+
case "code": {
|
|
3573
|
+
const lines = (block.text ?? "").split("\n");
|
|
3574
|
+
if (lines.length === 1 && !lines[0]) out.push(`${GRAY}\u2502${R} `);
|
|
3575
|
+
else for (const line of lines) out.push(`${GRAY}\u2502${R} ${line}`);
|
|
3576
|
+
break;
|
|
3577
|
+
}
|
|
3578
|
+
case "heading": {
|
|
3579
|
+
const text = streamingInline(block.runs ?? []);
|
|
3580
|
+
for (const line of wrapStyled(text, cols2)) out.push(`${BOLD}${TEAL}${line}${R}`);
|
|
3581
|
+
break;
|
|
3582
|
+
}
|
|
3583
|
+
case "rule":
|
|
3584
|
+
out.push(`${GRAY}\u2500\u2500\u2500\u2500\u2500\u2500${R}`);
|
|
3585
|
+
break;
|
|
3586
|
+
case "quote": {
|
|
3587
|
+
const text = streamingInline(block.runs ?? []);
|
|
3588
|
+
for (const logical of text.split("\n")) {
|
|
3589
|
+
for (const line of wrapStyled(logical, Math.max(8, cols2 - 2))) out.push(`${GRAY}\u2502${R} ${DIM3}${line}${R}`);
|
|
3590
|
+
}
|
|
3591
|
+
break;
|
|
3592
|
+
}
|
|
3593
|
+
case "list": {
|
|
3594
|
+
for (let index = 0; index < (block.items ?? []).length; index++) {
|
|
3595
|
+
const marker = block.ordered ? `${index + 1}.` : "\u2022";
|
|
3596
|
+
const lead = block.ordered ? `${BOLD}${marker}${R} ` : `${TEAL}${marker}${R} `;
|
|
3597
|
+
wrapBlock(
|
|
3598
|
+
out,
|
|
3599
|
+
cols2,
|
|
3600
|
+
lead,
|
|
3601
|
+
" ".repeat(marker.length + 1),
|
|
3602
|
+
marker.length + 1,
|
|
3603
|
+
streamingInline(block.items[index].runs)
|
|
3604
|
+
);
|
|
3605
|
+
}
|
|
3606
|
+
break;
|
|
3607
|
+
}
|
|
3608
|
+
case "table": {
|
|
3609
|
+
const rows = [
|
|
3610
|
+
(block.header ?? []).map(plainCell),
|
|
3611
|
+
...(block.rows ?? []).map((row) => row.map(plainCell))
|
|
3612
|
+
];
|
|
3613
|
+
if (rows[0].length) out.push(...renderTable(rows));
|
|
3614
|
+
break;
|
|
3615
|
+
}
|
|
3616
|
+
case "paragraph": {
|
|
3617
|
+
const text = streamingInline(block.runs ?? []);
|
|
3618
|
+
for (const logical of text.split("\n")) wrapBlock(out, cols2, "", "", 0, logical);
|
|
3619
|
+
break;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
return out;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
2673
3626
|
// src/wordmill.ts
|
|
2674
3627
|
var MILL_WORDS = [
|
|
2675
3628
|
"Working",
|
|
@@ -2711,8 +3664,8 @@ var HOLD_MS = 2600;
|
|
|
2711
3664
|
var LAZY_MS = 320;
|
|
2712
3665
|
var LAZY_PERIOD = 200;
|
|
2713
3666
|
var FAST_PERIOD = 70;
|
|
2714
|
-
var
|
|
2715
|
-
var
|
|
3667
|
+
var BOLD2 = "\x1B[1m";
|
|
3668
|
+
var DIM4 = "\x1B[2m";
|
|
2716
3669
|
var OFF = "\x1B[22m";
|
|
2717
3670
|
function glyphAt(slot, bucket) {
|
|
2718
3671
|
let h = (slot + 1) * 2654435761 ^ (bucket + 1) * 40503;
|
|
@@ -2747,7 +3700,7 @@ var WordMill = class {
|
|
|
2747
3700
|
if (this.phaseAt === 0) this.phaseAt = now;
|
|
2748
3701
|
if (!this.target) {
|
|
2749
3702
|
if (now - this.phaseAt >= HOLD_MS) this.beginMorph(now);
|
|
2750
|
-
else return `${
|
|
3703
|
+
else return `${BOLD2}${this.word}${OFF}`;
|
|
2751
3704
|
}
|
|
2752
3705
|
return this.morphFrame(now);
|
|
2753
3706
|
}
|
|
@@ -2773,21 +3726,21 @@ var WordMill = class {
|
|
|
2773
3726
|
this.word = target;
|
|
2774
3727
|
this.target = null;
|
|
2775
3728
|
this.phaseAt = now;
|
|
2776
|
-
return `${
|
|
3729
|
+
return `${BOLD2}${this.word}${OFF}`;
|
|
2777
3730
|
}
|
|
2778
3731
|
let out = "";
|
|
2779
3732
|
for (let i = 0; i < this.slots.length; i++) {
|
|
2780
3733
|
const s = this.slots[i];
|
|
2781
3734
|
if (t < s.start) {
|
|
2782
3735
|
const ch = this.word[i];
|
|
2783
|
-
if (ch) out += `${
|
|
3736
|
+
if (ch) out += `${BOLD2}${ch}${OFF}`;
|
|
2784
3737
|
} else if (t < s.land) {
|
|
2785
3738
|
const age = t - s.start;
|
|
2786
3739
|
const period = age < LAZY_MS ? LAZY_PERIOD : FAST_PERIOD;
|
|
2787
|
-
out += `${
|
|
3740
|
+
out += `${DIM4}${glyphAt(i, Math.floor(t / period))}${OFF}`;
|
|
2788
3741
|
} else {
|
|
2789
3742
|
const ch = target[i];
|
|
2790
|
-
if (ch) out += `${
|
|
3743
|
+
if (ch) out += `${BOLD2}${ch}${OFF}`;
|
|
2791
3744
|
}
|
|
2792
3745
|
}
|
|
2793
3746
|
return out;
|
|
@@ -2833,7 +3786,7 @@ function fromFile(filePath) {
|
|
|
2833
3786
|
}
|
|
2834
3787
|
}
|
|
2835
3788
|
async function readDarwin() {
|
|
2836
|
-
const tmp = path3.join(
|
|
3789
|
+
const tmp = path3.join(os7.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
|
|
2837
3790
|
const script = [
|
|
2838
3791
|
`set d to the clipboard as \xABclass PNGf\xBB`,
|
|
2839
3792
|
`set f to open for access POSIX file "${tmp}" with write permission`,
|
|
@@ -2870,7 +3823,7 @@ async function readLinux() {
|
|
|
2870
3823
|
return null;
|
|
2871
3824
|
}
|
|
2872
3825
|
async function readWindows() {
|
|
2873
|
-
const tmp = path3.join(
|
|
3826
|
+
const tmp = path3.join(os7.tmpdir(), `sc-clip-${process.pid}-${Date.now()}.png`);
|
|
2874
3827
|
const ps = [
|
|
2875
3828
|
"Add-Type -AssemblyName System.Windows.Forms;",
|
|
2876
3829
|
"$img = [System.Windows.Forms.Clipboard]::GetImage();",
|
|
@@ -2898,6 +3851,11 @@ async function readClipboardImage() {
|
|
|
2898
3851
|
function imagePlaceholder(seq) {
|
|
2899
3852
|
return `[#Image ${seq}]`;
|
|
2900
3853
|
}
|
|
3854
|
+
function ensureImagePlaceholders(text, images) {
|
|
3855
|
+
const missing = images.map((image) => imagePlaceholder(image.seq)).filter((placeholder) => !text.includes(placeholder));
|
|
3856
|
+
if (!missing.length) return text;
|
|
3857
|
+
return [text.trimEnd(), ...missing].filter(Boolean).join(" ");
|
|
3858
|
+
}
|
|
2901
3859
|
var INPUT_BOX_MARGIN = 1;
|
|
2902
3860
|
function inputBoxBorderColor() {
|
|
2903
3861
|
return "\x1B[38;5;240m";
|
|
@@ -3091,8 +4049,13 @@ var C = {
|
|
|
3091
4049
|
gray: themeGray,
|
|
3092
4050
|
teal: "\x1B[38;5;37m"
|
|
3093
4051
|
};
|
|
4052
|
+
var SYNC_OUTPUT_BEGIN = "\x1B[?2026h";
|
|
4053
|
+
var SYNC_OUTPUT_END = "\x1B[?2026l";
|
|
4054
|
+
var CURSOR_HIDE = "\x1B[?25l";
|
|
4055
|
+
var CURSOR_SHOW = "\x1B[?25h";
|
|
3094
4056
|
var FRAMES = ["\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
|
|
3095
|
-
var
|
|
4057
|
+
var SPINNER_FRAME_MS = 70;
|
|
4058
|
+
var ANIMATION_TICK_MS = 1e3 / 60;
|
|
3096
4059
|
var SUBAGENT_COLORS = [
|
|
3097
4060
|
"\x1B[35m",
|
|
3098
4061
|
// magenta
|
|
@@ -3165,6 +4128,7 @@ var Tui = class _Tui {
|
|
|
3165
4128
|
// answer text (plain). Bounded to a tail; cleared when the message commits.
|
|
3166
4129
|
streamThinking = "";
|
|
3167
4130
|
streamResponse = "";
|
|
4131
|
+
streamDraft = emptyLiveDraft();
|
|
3168
4132
|
streamMessageId = null;
|
|
3169
4133
|
// the message currently previewing
|
|
3170
4134
|
streamRedrawTimer = null;
|
|
@@ -3194,7 +4158,7 @@ var Tui = class _Tui {
|
|
|
3194
4158
|
// rows re-wrap (a full-width ruler becomes 2+ physical rows when narrowed),
|
|
3195
4159
|
// so the caret-relative move-up from the last paint is stale. We store the
|
|
3196
4160
|
// visible width of EVERY region row (not just above the body) plus the caret
|
|
3197
|
-
// row index so
|
|
4161
|
+
// row index so regionTopSequence can recompute physical height under the new
|
|
3198
4162
|
// wrap. Resize events are debounced — drag-resizing fires dozens of events
|
|
3199
4163
|
// and redrawing each one desyncs and leaves ghost chrome.
|
|
3200
4164
|
lastDrawnCols = 0;
|
|
@@ -3221,6 +4185,9 @@ var Tui = class _Tui {
|
|
|
3221
4185
|
// placeholder at the caret; on submit only images whose placeholder is still
|
|
3222
4186
|
// present in the text are handed to onSubmit. Cleared with the input.
|
|
3223
4187
|
pendingImages = [];
|
|
4188
|
+
/** Portable file refs mirrored from another client. Their bytes remain in
|
|
4189
|
+
* the thread filesystem, so the TUI shows names without decoding them. */
|
|
4190
|
+
externalAttachmentNames = [];
|
|
3224
4191
|
imagePasteBusy = false;
|
|
3225
4192
|
// one clipboard read at a time
|
|
3226
4193
|
// Sent-message history for ↑/↓ recall (oldest → newest). `historyIdx` is the
|
|
@@ -3234,6 +4201,9 @@ var Tui = class _Tui {
|
|
|
3234
4201
|
// event hooks (wired by index.ts)
|
|
3235
4202
|
onSubmit = () => {
|
|
3236
4203
|
};
|
|
4204
|
+
/** Shift+Return sends a steering input through the portable messaging endpoint. */
|
|
4205
|
+
onSteer = () => {
|
|
4206
|
+
};
|
|
3237
4207
|
onInterrupt = () => {
|
|
3238
4208
|
};
|
|
3239
4209
|
/** Up on the top row: return true to consume it (e.g. pull a queued message)
|
|
@@ -3242,8 +4212,8 @@ var Tui = class _Tui {
|
|
|
3242
4212
|
/** Enter while the `[⚙ n bg]` badge is selected — opens the bg process panel. */
|
|
3243
4213
|
onBgBadge = () => {
|
|
3244
4214
|
};
|
|
3245
|
-
/** Fired (deduped) when the
|
|
3246
|
-
*
|
|
4215
|
+
/** Fired (deduped) when the composer changes so index.ts can persist the
|
|
4216
|
+
* shared packed-endpoint draft, including attachments. */
|
|
3247
4217
|
onDraftChange = () => {
|
|
3248
4218
|
};
|
|
3249
4219
|
lastDraftSeen = "";
|
|
@@ -3329,7 +4299,7 @@ var Tui = class _Tui {
|
|
|
3329
4299
|
dispatch(str, key) {
|
|
3330
4300
|
const seq = key && key.sequence || str || "";
|
|
3331
4301
|
if (seq === "\n" || seq === "\x1B[13;2u" || seq === "\x1B[27;2;13~") {
|
|
3332
|
-
this.
|
|
4302
|
+
if (!this.takeoverHandler && !this.pasting && !this.paletteOpen()) this.submitInput(true);
|
|
3333
4303
|
return;
|
|
3334
4304
|
}
|
|
3335
4305
|
if (key && key.ctrl && key.name === "c") {
|
|
@@ -3398,10 +4368,7 @@ var Tui = class _Tui {
|
|
|
3398
4368
|
return;
|
|
3399
4369
|
}
|
|
3400
4370
|
if (key.name === "return" || key.name === "enter") {
|
|
3401
|
-
if (key.shift)
|
|
3402
|
-
this.insertAtCursor("\n");
|
|
3403
|
-
return;
|
|
3404
|
-
}
|
|
4371
|
+
if (key.shift) return;
|
|
3405
4372
|
if (matches.length) this.runCommand(matches[cur]);
|
|
3406
4373
|
return;
|
|
3407
4374
|
}
|
|
@@ -3464,23 +4431,15 @@ var Tui = class _Tui {
|
|
|
3464
4431
|
return;
|
|
3465
4432
|
}
|
|
3466
4433
|
if (key.name === "return" || key.name === "enter") {
|
|
3467
|
-
if (key.
|
|
4434
|
+
if (key.meta) {
|
|
3468
4435
|
this.insertAtCursor("\n");
|
|
3469
4436
|
return;
|
|
3470
4437
|
}
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
this.cursorPos = 0;
|
|
3475
|
-
this.pendingImages = [];
|
|
3476
|
-
this.historyIdx = null;
|
|
3477
|
-
this.historyDraft = "";
|
|
3478
|
-
this.historyDraftImages = [];
|
|
3479
|
-
this.renderBottom();
|
|
3480
|
-
if (text.trim()) {
|
|
3481
|
-
this.addHistoryEntry(text.trim());
|
|
3482
|
-
this.onSubmit(text.trim(), images);
|
|
4438
|
+
if (key.shift) {
|
|
4439
|
+
this.submitInput(true);
|
|
4440
|
+
return;
|
|
3483
4441
|
}
|
|
4442
|
+
this.submitInput(false);
|
|
3484
4443
|
return;
|
|
3485
4444
|
}
|
|
3486
4445
|
if (key.ctrl && key.name === "v") {
|
|
@@ -3511,6 +4470,23 @@ var Tui = class _Tui {
|
|
|
3511
4470
|
this.insertAtCursor(str);
|
|
3512
4471
|
}
|
|
3513
4472
|
}
|
|
4473
|
+
submitInput(steer) {
|
|
4474
|
+
const text = this.inputBuffer;
|
|
4475
|
+
const images = this.pendingImages.filter((img) => text.includes(imagePlaceholder(img.seq)));
|
|
4476
|
+
const hasExternalAttachments = this.externalAttachmentNames.length > 0;
|
|
4477
|
+
this.inputBuffer = "";
|
|
4478
|
+
this.cursorPos = 0;
|
|
4479
|
+
this.pendingImages = [];
|
|
4480
|
+
this.externalAttachmentNames = [];
|
|
4481
|
+
this.historyIdx = null;
|
|
4482
|
+
this.historyDraft = "";
|
|
4483
|
+
this.historyDraftImages = [];
|
|
4484
|
+
this.renderBottom();
|
|
4485
|
+
if (!text.trim() && !hasExternalAttachments && images.length === 0) return;
|
|
4486
|
+
if (text.trim()) this.addHistoryEntry(text.trim());
|
|
4487
|
+
if (steer) this.onSteer(text.trim(), images);
|
|
4488
|
+
else this.onSubmit(text.trim(), images);
|
|
4489
|
+
}
|
|
3514
4490
|
insertAtCursor(text) {
|
|
3515
4491
|
this.inputBuffer = this.inputBuffer.slice(0, this.cursorPos) + text + this.inputBuffer.slice(this.cursorPos);
|
|
3516
4492
|
this.cursorPos += text.length;
|
|
@@ -3713,7 +4689,7 @@ var Tui = class _Tui {
|
|
|
3713
4689
|
}
|
|
3714
4690
|
spinnerFrame() {
|
|
3715
4691
|
const now = Date.now();
|
|
3716
|
-
return `${C.bold}${brandCycleColor(now)}${FRAMES[Math.floor(now /
|
|
4692
|
+
return `${C.bold}${brandCycleColor(now)}${FRAMES[Math.floor(now / SPINNER_FRAME_MS) % FRAMES.length]}${C.reset}`;
|
|
3717
4693
|
}
|
|
3718
4694
|
/** "↑X ↓Y" cumulative token totals (greyed — low-priority). */
|
|
3719
4695
|
tokensText() {
|
|
@@ -3753,7 +4729,7 @@ var Tui = class _Tui {
|
|
|
3753
4729
|
}
|
|
3754
4730
|
/** Is the `/` command palette currently showing? (input starts with "/".) */
|
|
3755
4731
|
paletteOpen() {
|
|
3756
|
-
return this.started && !this.takeoverHandler && this.commands.length > 0 && this.inputBuffer.startsWith("/");
|
|
4732
|
+
return this.started && !this.takeoverHandler && this.commands.length > 0 && this.externalAttachmentNames.length === 0 && this.inputBuffer.startsWith("/");
|
|
3757
4733
|
}
|
|
3758
4734
|
/** Commands matching the text typed after "/", in declared order. */
|
|
3759
4735
|
filteredCommands() {
|
|
@@ -3812,9 +4788,10 @@ var Tui = class _Tui {
|
|
|
3812
4788
|
/** The prompt line prefix (with ANSI colour) that precedes the typed text. */
|
|
3813
4789
|
promptPrefix() {
|
|
3814
4790
|
const q = this.queuedCount > 0 ? `${C.yellow}[\u23F3 ${this.queuedCount} queued]${C.reset} ` : "";
|
|
4791
|
+
const attachments2 = this.externalAttachmentNames.length > 0 ? `${C.gray}[\u{1F4CE} ${this.externalAttachmentNames.length}]${C.reset} ` : "";
|
|
3815
4792
|
const bgText = `[\u2699 ${this.bgCount} bg]`;
|
|
3816
4793
|
const bg = this.bgCount > 0 ? this.bgBadgeSelected ? `${C.cyan}\x1B[7m${bgText}\x1B[27m${C.reset} ` : `${C.cyan}${bgText}${C.reset} ` : "";
|
|
3817
|
-
return `${q}${bg}${this.levelColor()}\u276F${C.reset} `;
|
|
4794
|
+
return `${q}${attachments2}${bg}${this.levelColor()}\u276F${C.reset} `;
|
|
3818
4795
|
}
|
|
3819
4796
|
visibleWidth(s) {
|
|
3820
4797
|
let w = 0;
|
|
@@ -3889,20 +4866,21 @@ var Tui = class _Tui {
|
|
|
3889
4866
|
if (matches.length === 0) return [` ${C.gray}no matching command${C.reset}`];
|
|
3890
4867
|
const cur = Math.min(this.slashIdx, matches.length - 1);
|
|
3891
4868
|
const pointerW = 2;
|
|
4869
|
+
const rowCols = Math.max(1, cols2 - 1);
|
|
3892
4870
|
return matches.map((cmd, i) => {
|
|
3893
4871
|
const sel = i === cur;
|
|
3894
4872
|
const hint = (typeof cmd.hint === "function" ? cmd.hint() : cmd.hint) ?? "";
|
|
3895
4873
|
const hintW = hint.length;
|
|
3896
4874
|
const name = `/${cmd.name}`;
|
|
3897
4875
|
let visible = `${name} ${cmd.label}`;
|
|
3898
|
-
const labelMax = Math.max(6,
|
|
4876
|
+
const labelMax = Math.max(6, rowCols - pointerW - (hintW ? hintW + 2 : 0));
|
|
3899
4877
|
if (visible.length > labelMax) visible = visible.slice(0, labelMax - 1) + "\u2026";
|
|
3900
4878
|
const desc = visible.slice(name.length);
|
|
3901
4879
|
const pointer = sel ? `${C.magenta}\u276F${C.reset} ` : " ";
|
|
3902
4880
|
const nameStyled = sel ? `${C.bold}${C.cyan}${name}${C.reset}` : `${C.cyan}${name}${C.reset}`;
|
|
3903
4881
|
let line = `${pointer}${nameStyled}${C.gray}${desc}${C.reset}`;
|
|
3904
4882
|
if (hintW) {
|
|
3905
|
-
const gap = Math.max(2,
|
|
4883
|
+
const gap = Math.max(2, rowCols - pointerW - visible.length - hintW);
|
|
3906
4884
|
line += `${" ".repeat(gap)}${C.gray}${hint}${C.reset}`;
|
|
3907
4885
|
}
|
|
3908
4886
|
return line;
|
|
@@ -3924,9 +4902,9 @@ var Tui = class _Tui {
|
|
|
3924
4902
|
* the NEW wrap: rows above the caret row + (caret row's rewrap − 1) so we
|
|
3925
4903
|
* prefer a slight over-move (clean wipe) over under-move (ghost chrome).
|
|
3926
4904
|
*/
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
if (!this.bottomDrawn) return;
|
|
4905
|
+
regionTopSequence() {
|
|
4906
|
+
let sequence = "\r";
|
|
4907
|
+
if (!this.bottomDrawn) return sequence;
|
|
3930
4908
|
const cols2 = process.stdout.columns || 80;
|
|
3931
4909
|
let up;
|
|
3932
4910
|
if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0 && this.drawnRegionWidths.length) {
|
|
@@ -3943,7 +4921,8 @@ var Tui = class _Tui {
|
|
|
3943
4921
|
} else {
|
|
3944
4922
|
up = this.lastCursorRow;
|
|
3945
4923
|
}
|
|
3946
|
-
if (up > 0)
|
|
4924
|
+
if (up > 0) sequence += `\x1B[${up}A`;
|
|
4925
|
+
return sequence;
|
|
3947
4926
|
}
|
|
3948
4927
|
/**
|
|
3949
4928
|
* Render the bottom region: status + subagents above a side-margined expanding
|
|
@@ -3959,14 +4938,15 @@ var Tui = class _Tui {
|
|
|
3959
4938
|
if (!this.started || this.takeoverHandler) return;
|
|
3960
4939
|
if (this.inputBuffer !== this.lastDraftSeen) {
|
|
3961
4940
|
this.lastDraftSeen = this.inputBuffer;
|
|
4941
|
+
const images = this.pendingImages.filter((img) => this.inputBuffer.includes(imagePlaceholder(img.seq)));
|
|
3962
4942
|
try {
|
|
3963
|
-
this.onDraftChange(this.inputBuffer);
|
|
4943
|
+
this.onDraftChange(this.inputBuffer, images);
|
|
3964
4944
|
} catch {
|
|
3965
4945
|
}
|
|
3966
4946
|
}
|
|
3967
4947
|
if (this.resizePending) return;
|
|
3968
4948
|
const cols2 = process.stdout.columns || 80;
|
|
3969
|
-
this.
|
|
4949
|
+
const moveToTop = this.regionTopSequence();
|
|
3970
4950
|
const hudWidths = [];
|
|
3971
4951
|
const hudRows = [];
|
|
3972
4952
|
const rowCap = Math.max(1, cols2 - 1);
|
|
@@ -3987,7 +4967,7 @@ var Tui = class _Tui {
|
|
|
3987
4967
|
if (quitLine) writeHudRow(workPad + quitLine);
|
|
3988
4968
|
const statusLine = this.statusLineText(workCols);
|
|
3989
4969
|
if (statusLine) writeHudRow(workPad + statusLine);
|
|
3990
|
-
const frame = FRAMES[Math.floor(Date.now() /
|
|
4970
|
+
const frame = FRAMES[Math.floor(Date.now() / SPINNER_FRAME_MS) % FRAMES.length];
|
|
3991
4971
|
for (const sub of this.subagents) {
|
|
3992
4972
|
const color = sub.agentName === COMPACTION_AGENT ? COMPACTION_COLOR : SUBAGENT_COLORS[this.subagentColorByID.get(sub.id) ?? 0];
|
|
3993
4973
|
const budget = workCols - 10;
|
|
@@ -4035,18 +5015,21 @@ var Tui = class _Tui {
|
|
|
4035
5015
|
this.bottomDrawn = true;
|
|
4036
5016
|
const totalRows = hudRows.length;
|
|
4037
5017
|
const up = Math.max(0, totalRows - 1 - caretRegionRow);
|
|
4038
|
-
let out = "\x1B[J" + hudRows.join("\r\n");
|
|
5018
|
+
let out = SYNC_OUTPUT_BEGIN + moveToTop + "\x1B[J" + hudRows.join("\r\n");
|
|
4039
5019
|
if (totalRows > 0) {
|
|
4040
5020
|
out += "\r";
|
|
4041
5021
|
if (up > 0) out += `\x1B[${up}A`;
|
|
4042
5022
|
if (caretScreenCol > 0) out += `\x1B[${caretScreenCol}C`;
|
|
4043
5023
|
}
|
|
5024
|
+
out += SYNC_OUTPUT_END;
|
|
4044
5025
|
process.stdout.write(out);
|
|
4045
5026
|
}
|
|
4046
5027
|
clearBottom() {
|
|
4047
5028
|
if (!this.bottomDrawn) return;
|
|
4048
|
-
this.
|
|
4049
|
-
process.stdout.write(
|
|
5029
|
+
const moveToTop = this.regionTopSequence();
|
|
5030
|
+
process.stdout.write(
|
|
5031
|
+
SYNC_OUTPUT_BEGIN + CURSOR_HIDE + moveToTop + "\x1B[J" + CURSOR_SHOW + SYNC_OUTPUT_END
|
|
5032
|
+
);
|
|
4050
5033
|
this.bottomDrawn = false;
|
|
4051
5034
|
}
|
|
4052
5035
|
// ── live streaming preview ────────────────────────────────────────────────
|
|
@@ -4055,15 +5038,15 @@ var Tui = class _Tui {
|
|
|
4055
5038
|
// wipes it right before the finished message is committed to the transcript
|
|
4056
5039
|
// (which renders full markdown), so there's no double-render.
|
|
4057
5040
|
static STREAM_TAIL = 20;
|
|
4058
|
-
// How long a preview may sit untouched before it's wiped.
|
|
4059
|
-
//
|
|
4060
|
-
//
|
|
4061
|
-
// message) arrives. Re-armed on every delta → fires this long after the last.
|
|
5041
|
+
// How long a reasoning-only preview may sit untouched before it's wiped. A
|
|
5042
|
+
// visible answer must NEVER expire: it is the only copy until polling
|
|
5043
|
+
// promotes the durable message into terminal scrollback.
|
|
4062
5044
|
static STREAM_IDLE_MS = 1e4;
|
|
4063
|
-
/** Append a fragment of streamed answer text (rendered
|
|
5045
|
+
/** Append a fragment of streamed answer text (rendered as progressive Markdown). */
|
|
4064
5046
|
streamResponseDelta(delta, messageId) {
|
|
4065
5047
|
this.beginStreamMessage(messageId);
|
|
4066
|
-
this.
|
|
5048
|
+
this.streamDraft = appendLiveDraft(this.streamDraft, { text: delta, messageId });
|
|
5049
|
+
this.streamResponse = this.streamDraft.text;
|
|
4067
5050
|
this.scheduleStreamRedraw();
|
|
4068
5051
|
this.armStreamIdleExpiry();
|
|
4069
5052
|
}
|
|
@@ -4083,8 +5066,7 @@ var Tui = class _Tui {
|
|
|
4083
5066
|
beginStreamMessage(messageId) {
|
|
4084
5067
|
if (messageId !== void 0 && messageId !== this.streamMessageId) {
|
|
4085
5068
|
this.streamMessageId = messageId;
|
|
4086
|
-
this.streamThinking = "";
|
|
4087
|
-
this.streamResponse = "";
|
|
5069
|
+
if (!this.streamResponse) this.streamThinking = "";
|
|
4088
5070
|
}
|
|
4089
5071
|
}
|
|
4090
5072
|
/** Wipe the live preview — call right before committing the final message. */
|
|
@@ -4098,6 +5080,7 @@ var Tui = class _Tui {
|
|
|
4098
5080
|
this.streamIdleTimer = null;
|
|
4099
5081
|
}
|
|
4100
5082
|
this.streamMessageId = null;
|
|
5083
|
+
this.streamDraft = emptyLiveDraft();
|
|
4101
5084
|
if (!this.streamThinking && !this.streamResponse) return;
|
|
4102
5085
|
this.streamThinking = "";
|
|
4103
5086
|
this.streamResponse = "";
|
|
@@ -4110,17 +5093,16 @@ var Tui = class _Tui {
|
|
|
4110
5093
|
this.renderBottom();
|
|
4111
5094
|
}, 40);
|
|
4112
5095
|
}
|
|
4113
|
-
/** Re-armed
|
|
4114
|
-
*
|
|
4115
|
-
* message. The committed message (if any) still renders in full via
|
|
4116
|
-
* clearStream(), so nothing is lost. */
|
|
5096
|
+
/** Re-armed for reasoning-only deltas. Once answer prose exists it remains
|
|
5097
|
+
* visible until clearStream() runs immediately before durable promotion. */
|
|
4117
5098
|
armStreamIdleExpiry() {
|
|
4118
5099
|
if (this.streamIdleTimer) clearTimeout(this.streamIdleTimer);
|
|
5100
|
+
this.streamIdleTimer = null;
|
|
5101
|
+
if (this.streamResponse) return;
|
|
4119
5102
|
this.streamIdleTimer = setTimeout(() => {
|
|
4120
5103
|
this.streamIdleTimer = null;
|
|
4121
|
-
if (
|
|
5104
|
+
if (this.streamResponse || !this.streamThinking) return;
|
|
4122
5105
|
this.streamThinking = "";
|
|
4123
|
-
this.streamResponse = "";
|
|
4124
5106
|
this.renderBottom();
|
|
4125
5107
|
}, _Tui.STREAM_IDLE_MS);
|
|
4126
5108
|
}
|
|
@@ -4136,23 +5118,28 @@ var Tui = class _Tui {
|
|
|
4136
5118
|
*/
|
|
4137
5119
|
streamPreviewLines(cols2) {
|
|
4138
5120
|
const thinkStyle = "\x1B[3m\x1B[38;5;240m";
|
|
4139
|
-
const
|
|
4140
|
-
|
|
4141
|
-
const
|
|
4142
|
-
|
|
5121
|
+
const leakedMarkup = /<\/?[||]DSML[||]|^\s*\[\/?SESSION\]\s*<?\s*$/;
|
|
5122
|
+
const renderedLines = (text) => {
|
|
5123
|
+
const clean2 = text.replace(/\r/g, "").split("\n").filter((line) => !leakedMarkup.test(line)).join("\n");
|
|
5124
|
+
const rows = [];
|
|
5125
|
+
for (const line of renderStreamingMarkdown(clean2, Math.max(8, cols2 - 2))) {
|
|
5126
|
+
const blank = line.replace(/\x1b\[[0-9;]*m/g, "").trim() === "";
|
|
5127
|
+
if (blank && (!rows.length || rows[rows.length - 1] === "")) continue;
|
|
5128
|
+
rows.push(blank ? "" : line);
|
|
5129
|
+
}
|
|
5130
|
+
while (rows.length && rows[rows.length - 1] === "") rows.pop();
|
|
5131
|
+
return rows;
|
|
4143
5132
|
};
|
|
4144
|
-
const leakedMarkup = /<\/?[||]DSML[||]|^\s*\[\/?SESSION\]\s*<?\s*$/;
|
|
4145
|
-
const realLines = (text) => text.replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "" && !leakedMarkup.test(l));
|
|
4146
5133
|
if (this.streamResponse) {
|
|
4147
|
-
const all =
|
|
5134
|
+
const all = renderedLines(this.streamResponse);
|
|
4148
5135
|
const shown = all.slice(-20);
|
|
4149
5136
|
const firstVisible = all.length <= _Tui.STREAM_TAIL;
|
|
4150
5137
|
return shown.map(
|
|
4151
|
-
(l, i) =>
|
|
5138
|
+
(l, i) => `${i === 0 && firstVisible ? `${C.gray}\u2022${C.reset} ` : " "}${l}`
|
|
4152
5139
|
);
|
|
4153
5140
|
}
|
|
4154
5141
|
if (this.streamThinking) {
|
|
4155
|
-
return
|
|
5142
|
+
return renderedLines(this.streamThinking).slice(-20).map((l) => ` ${thinkStyle}${l.replace(/\x1b\[0m/g, `${C.reset}${thinkStyle}`)}${C.reset}`);
|
|
4156
5143
|
}
|
|
4157
5144
|
return [];
|
|
4158
5145
|
}
|
|
@@ -4280,6 +5267,7 @@ var Tui = class _Tui {
|
|
|
4280
5267
|
}
|
|
4281
5268
|
// ─── working indicator (turn state) ───────────────────────────────────────
|
|
4282
5269
|
setWorking(on) {
|
|
5270
|
+
if (on === this.working) return;
|
|
4283
5271
|
if (on && !this.working) {
|
|
4284
5272
|
this.working = true;
|
|
4285
5273
|
this.workingStart = Date.now();
|
|
@@ -4295,6 +5283,11 @@ var Tui = class _Tui {
|
|
|
4295
5283
|
* stable, distinct colour for as long as it's active; the compaction agent
|
|
4296
5284
|
* is always orange (its colour never comes from the shared pool). */
|
|
4297
5285
|
setSubagents(subagents) {
|
|
5286
|
+
const unchanged = subagents.length === this.subagents.length && subagents.every((sub, i) => {
|
|
5287
|
+
const current = this.subagents[i];
|
|
5288
|
+
return current?.id === sub.id && current.label === sub.label && current.agentName === sub.agentName;
|
|
5289
|
+
});
|
|
5290
|
+
if (unchanged) return;
|
|
4298
5291
|
this.subagents = subagents;
|
|
4299
5292
|
const active = new Set(subagents.map((s) => s.id));
|
|
4300
5293
|
for (const id of [...this.subagentColorByID.keys()]) {
|
|
@@ -4315,7 +5308,7 @@ var Tui = class _Tui {
|
|
|
4315
5308
|
syncSpinner() {
|
|
4316
5309
|
const spinning = this.working || this.subagents.length > 0;
|
|
4317
5310
|
if (spinning && !this.spinnerTimer) {
|
|
4318
|
-
this.spinnerTimer = setInterval(() => this.renderBottom(),
|
|
5311
|
+
this.spinnerTimer = setInterval(() => this.renderBottom(), ANIMATION_TICK_MS);
|
|
4319
5312
|
} else if (!spinning && this.spinnerTimer) {
|
|
4320
5313
|
clearInterval(this.spinnerTimer);
|
|
4321
5314
|
this.spinnerTimer = null;
|
|
@@ -4325,15 +5318,18 @@ var Tui = class _Tui {
|
|
|
4325
5318
|
return this.working;
|
|
4326
5319
|
}
|
|
4327
5320
|
setBackgroundCount(n) {
|
|
5321
|
+
if (n === this.bgCount) return;
|
|
4328
5322
|
this.bgCount = n;
|
|
4329
5323
|
if (n === 0) this.bgBadgeSelected = false;
|
|
4330
5324
|
this.renderBottom();
|
|
4331
5325
|
}
|
|
4332
5326
|
setQueuedCount(n) {
|
|
5327
|
+
if (n === this.queuedCount) return;
|
|
4333
5328
|
this.queuedCount = n;
|
|
4334
5329
|
this.renderBottom();
|
|
4335
5330
|
}
|
|
4336
5331
|
setConnected(connected) {
|
|
5332
|
+
if (connected === this.connected) return;
|
|
4337
5333
|
this.connected = connected;
|
|
4338
5334
|
this.renderBottom();
|
|
4339
5335
|
}
|
|
@@ -4342,15 +5338,25 @@ var Tui = class _Tui {
|
|
|
4342
5338
|
return this.inputBuffer;
|
|
4343
5339
|
}
|
|
4344
5340
|
/** Replace the input (and any pasted images tied to placeholders in it). */
|
|
4345
|
-
setInput(text, images = []) {
|
|
4346
|
-
this.inputBuffer = text;
|
|
4347
|
-
this.cursorPos =
|
|
5341
|
+
setInput(text, images = [], notifyDraft = true, restoreExternalImages = false) {
|
|
5342
|
+
this.inputBuffer = restoreExternalImages ? ensureImagePlaceholders(text, images) : text;
|
|
5343
|
+
this.cursorPos = this.inputBuffer.length;
|
|
4348
5344
|
this.pendingImages = images;
|
|
4349
5345
|
this.historyIdx = null;
|
|
5346
|
+
if (!notifyDraft) this.lastDraftSeen = this.inputBuffer;
|
|
5347
|
+
this.renderBottom();
|
|
5348
|
+
}
|
|
5349
|
+
setExternalAttachmentNames(names) {
|
|
5350
|
+
if (names.length === this.externalAttachmentNames.length && names.every((name, i) => name === this.externalAttachmentNames[i])) return;
|
|
5351
|
+
this.externalAttachmentNames = [...names];
|
|
4350
5352
|
this.renderBottom();
|
|
4351
5353
|
}
|
|
5354
|
+
hasExternalAttachments() {
|
|
5355
|
+
return this.externalAttachmentNames.length > 0;
|
|
5356
|
+
}
|
|
4352
5357
|
/** Cumulative token totals shown on the prompt line (`outTokens` includes live). */
|
|
4353
5358
|
setTokens(inTokens, outTokens) {
|
|
5359
|
+
if (inTokens === this.tokensIn && outTokens === this.tokensOut) return;
|
|
4354
5360
|
this.tokensIn = inTokens;
|
|
4355
5361
|
this.tokensOut = outTokens;
|
|
4356
5362
|
this.renderBottom();
|
|
@@ -4362,6 +5368,7 @@ var Tui = class _Tui {
|
|
|
4362
5368
|
*/
|
|
4363
5369
|
setStep(label, outTokens) {
|
|
4364
5370
|
const next = label && label.trim() ? label.replace(/\s+/g, " ").trim() : null;
|
|
5371
|
+
if (next === this.step && outTokens === this.stepOut) return;
|
|
4365
5372
|
if (next !== this.step) {
|
|
4366
5373
|
this.step = next;
|
|
4367
5374
|
this.stepStart = Date.now();
|
|
@@ -4787,169 +5794,7 @@ function appendHistory(store, threadId, history, text) {
|
|
|
4787
5794
|
void store.kvSet(threadId, HISTORY_KEY, [...history]);
|
|
4788
5795
|
return history;
|
|
4789
5796
|
}
|
|
4790
|
-
|
|
4791
|
-
// src/markdown.ts
|
|
4792
|
-
var ESC = "\x1B[";
|
|
4793
|
-
var R = ESC + "0m";
|
|
4794
|
-
var BOLD2 = ESC + "1m";
|
|
4795
|
-
var DIM4 = ESC + "2m";
|
|
4796
|
-
var ITAL = ESC + "3m";
|
|
4797
|
-
var UNDER = ESC + "4m";
|
|
4798
|
-
var TEAL = ESC + "38;5;37m";
|
|
4799
|
-
var CYAN = ESC + "36m";
|
|
4800
|
-
var GRAY = ESC + "90m";
|
|
4801
|
-
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
4802
|
-
function visibleWidth(s) {
|
|
4803
|
-
return s.replace(ANSI, "").length;
|
|
4804
|
-
}
|
|
4805
|
-
function padEndVisible(s, width) {
|
|
4806
|
-
const pad = width - visibleWidth(s);
|
|
4807
|
-
return pad > 0 ? s + " ".repeat(pad) : s;
|
|
4808
|
-
}
|
|
4809
|
-
function inline(s) {
|
|
4810
|
-
const codes = [];
|
|
4811
|
-
s = s.replace(/`([^`]+)`/g, (_, code) => {
|
|
4812
|
-
codes.push(code);
|
|
4813
|
-
return "\0" + (codes.length - 1) + "\0";
|
|
4814
|
-
});
|
|
4815
|
-
s = s.replace(
|
|
4816
|
-
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
4817
|
-
(_, text, url) => `${CYAN}${UNDER}${text}${R} ${DIM4}${url}${R}`
|
|
4818
|
-
);
|
|
4819
|
-
s = s.replace(/\*\*([^*]+)\*\*/g, (_, t) => `${BOLD2}${t}${R}`);
|
|
4820
|
-
s = s.replace(/\*([^*\n]+)\*/g, (_, t) => `${ITAL}${t}${R}`);
|
|
4821
|
-
s = s.replace(/~~([^~]+)~~/g, (_, t) => `${DIM4}${t}${R}`);
|
|
4822
|
-
s = s.replace(/\x00(\d+)\x00/g, (_, i) => `${TEAL}${codes[+i].replace(/ /g, String.fromCharCode(160))}${R}`);
|
|
4823
|
-
return s;
|
|
4824
|
-
}
|
|
4825
|
-
function wrapStyled(text, width) {
|
|
4826
|
-
if (width < 4 || visibleWidth(text) <= width) return [text];
|
|
4827
|
-
const words = text.split(" ");
|
|
4828
|
-
const lines = [];
|
|
4829
|
-
let cur = "";
|
|
4830
|
-
let curLen = 0;
|
|
4831
|
-
for (const w of words) {
|
|
4832
|
-
const wLen = visibleWidth(w);
|
|
4833
|
-
if (cur === "") {
|
|
4834
|
-
cur = w;
|
|
4835
|
-
curLen = wLen;
|
|
4836
|
-
} else if (curLen + 1 + wLen <= width) {
|
|
4837
|
-
cur += " " + w;
|
|
4838
|
-
curLen += 1 + wLen;
|
|
4839
|
-
} else {
|
|
4840
|
-
lines.push(cur);
|
|
4841
|
-
cur = w;
|
|
4842
|
-
curLen = wLen;
|
|
4843
|
-
}
|
|
4844
|
-
}
|
|
4845
|
-
if (cur !== "" || lines.length === 0) lines.push(cur);
|
|
4846
|
-
return lines;
|
|
4847
|
-
}
|
|
4848
|
-
function wrapBlock(out, cols2, leadFirst, leadRest, leadWidth, text) {
|
|
4849
|
-
const wrapped = wrapStyled(text, Math.max(8, cols2 - leadWidth));
|
|
4850
|
-
wrapped.forEach((ln, idx) => out.push((idx === 0 ? leadFirst : leadRest) + ln));
|
|
4851
|
-
}
|
|
4852
|
-
function tableCells(row) {
|
|
4853
|
-
let r = row.trim();
|
|
4854
|
-
if (r.startsWith("|")) r = r.slice(1);
|
|
4855
|
-
if (r.endsWith("|")) r = r.slice(0, -1);
|
|
4856
|
-
return r.split("|").map((c4) => c4.trim());
|
|
4857
|
-
}
|
|
4858
|
-
var SEPARATOR = /^[\s|:-]+$/;
|
|
4859
|
-
function isTableSeparator(line) {
|
|
4860
|
-
return SEPARATOR.test(line) && line.includes("-") && line.includes("|");
|
|
4861
|
-
}
|
|
4862
|
-
function renderTable(rows) {
|
|
4863
|
-
const cols2 = Math.max(...rows.map((r) => r.length));
|
|
4864
|
-
const widths = [];
|
|
4865
|
-
for (let c4 = 0; c4 < cols2; c4++) {
|
|
4866
|
-
widths[c4] = Math.max(...rows.map((r) => visibleWidth(inline(r[c4] ?? ""))));
|
|
4867
|
-
}
|
|
4868
|
-
const sep = `${GRAY} \u2502 ${R}`;
|
|
4869
|
-
const out = [];
|
|
4870
|
-
rows.forEach((r, ri) => {
|
|
4871
|
-
const cells = [];
|
|
4872
|
-
for (let c4 = 0; c4 < cols2; c4++) {
|
|
4873
|
-
const raw = r[c4] ?? "";
|
|
4874
|
-
const styled = ri === 0 ? `${BOLD2}${inline(raw)}${R}` : inline(raw);
|
|
4875
|
-
cells.push(padEndVisible(styled, widths[c4]));
|
|
4876
|
-
}
|
|
4877
|
-
out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
|
|
4878
|
-
if (ri === 0) {
|
|
4879
|
-
const rule = widths.map((w) => `${GRAY}${"\u2500".repeat(w)}${R}`).join(`${GRAY}\u2500\u253C\u2500${R}`);
|
|
4880
|
-
out.push(" " + rule);
|
|
4881
|
-
}
|
|
4882
|
-
});
|
|
4883
|
-
return out;
|
|
4884
|
-
}
|
|
4885
|
-
function renderMarkdown(src, cols2 = 80) {
|
|
4886
|
-
const lines = src.replace(/\r\n/g, "\n").split("\n");
|
|
4887
|
-
const out = [];
|
|
4888
|
-
let inFence = false;
|
|
4889
|
-
let i = 0;
|
|
4890
|
-
while (i < lines.length) {
|
|
4891
|
-
const line = lines[i];
|
|
4892
|
-
if (/^\s*```/.test(line)) {
|
|
4893
|
-
inFence = !inFence;
|
|
4894
|
-
i++;
|
|
4895
|
-
continue;
|
|
4896
|
-
}
|
|
4897
|
-
if (inFence) {
|
|
4898
|
-
out.push(`${GRAY}\u2502${R} ${line}`);
|
|
4899
|
-
i++;
|
|
4900
|
-
continue;
|
|
4901
|
-
}
|
|
4902
|
-
if (line.includes("|") && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
|
|
4903
|
-
const block = [tableCells(line)];
|
|
4904
|
-
i += 2;
|
|
4905
|
-
while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
|
|
4906
|
-
block.push(tableCells(lines[i]));
|
|
4907
|
-
i++;
|
|
4908
|
-
}
|
|
4909
|
-
out.push(...renderTable(block));
|
|
4910
|
-
continue;
|
|
4911
|
-
}
|
|
4912
|
-
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
4913
|
-
if (heading) {
|
|
4914
|
-
for (const ln of wrapStyled(heading[2].trim(), cols2)) out.push(`${BOLD2}${TEAL}${ln}${R}`);
|
|
4915
|
-
i++;
|
|
4916
|
-
continue;
|
|
4917
|
-
}
|
|
4918
|
-
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
|
4919
|
-
out.push(`${GRAY}\u2500\u2500\u2500\u2500\u2500\u2500${R}`);
|
|
4920
|
-
i++;
|
|
4921
|
-
continue;
|
|
4922
|
-
}
|
|
4923
|
-
const quote = line.match(/^\s*>\s?(.*)$/);
|
|
4924
|
-
if (quote) {
|
|
4925
|
-
for (const ln of wrapStyled(inline(quote[1]), Math.max(8, cols2 - 2))) {
|
|
4926
|
-
out.push(`${GRAY}\u2502${R} ${DIM4}${ln}${R}`);
|
|
4927
|
-
}
|
|
4928
|
-
i++;
|
|
4929
|
-
continue;
|
|
4930
|
-
}
|
|
4931
|
-
const bullet = line.match(/^(\s*)[-*+]\s+(.*)$/);
|
|
4932
|
-
if (bullet) {
|
|
4933
|
-
const leadWidth = bullet[1].length + 2;
|
|
4934
|
-
wrapBlock(out, cols2, `${bullet[1]}${TEAL}\u2022${R} `, " ".repeat(leadWidth), leadWidth, inline(bullet[2]));
|
|
4935
|
-
i++;
|
|
4936
|
-
continue;
|
|
4937
|
-
}
|
|
4938
|
-
const numbered = line.match(/^(\s*)(\d+)([.)])\s+(.*)$/);
|
|
4939
|
-
if (numbered) {
|
|
4940
|
-
const marker = `${numbered[2]}${numbered[3]}`;
|
|
4941
|
-
const leadWidth = numbered[1].length + marker.length + 1;
|
|
4942
|
-
wrapBlock(out, cols2, `${numbered[1]}${BOLD2}${marker}${R} `, " ".repeat(leadWidth), leadWidth, inline(numbered[4]));
|
|
4943
|
-
i++;
|
|
4944
|
-
continue;
|
|
4945
|
-
}
|
|
4946
|
-
if (line.trim()) wrapBlock(out, cols2, "", "", 0, inline(line));
|
|
4947
|
-
else out.push("");
|
|
4948
|
-
i++;
|
|
4949
|
-
}
|
|
4950
|
-
return out;
|
|
4951
|
-
}
|
|
4952
|
-
var DIR = path3.join(os10.homedir(), ".standardagents");
|
|
5797
|
+
var DIR = path3.join(os7.homedir(), ".standardagents");
|
|
4953
5798
|
var FILE = path3.join(DIR, "credentials");
|
|
4954
5799
|
function normalizeEndpoint(endpoint) {
|
|
4955
5800
|
let e = endpoint.trim();
|
|
@@ -5083,7 +5928,7 @@ function relaxTlsForLocalEndpoint(endpoint) {
|
|
|
5083
5928
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
5084
5929
|
return true;
|
|
5085
5930
|
}
|
|
5086
|
-
var dir = () => path3.join(
|
|
5931
|
+
var dir = () => path3.join(os7.homedir(), ".standardagents");
|
|
5087
5932
|
var file = () => path3.join(dir(), "machine.json");
|
|
5088
5933
|
function loadMachineIdentity() {
|
|
5089
5934
|
try {
|
|
@@ -5153,20 +5998,55 @@ function parseFsRequest(value) {
|
|
|
5153
5998
|
requested_at: typeof r.requested_at === "number" ? r.requested_at : Date.now()
|
|
5154
5999
|
};
|
|
5155
6000
|
}
|
|
6001
|
+
async function writeFsRequest(api, machineId, req) {
|
|
6002
|
+
await api.userKvSet(fsRequestKey(machineId), { ...req, requested_at: Date.now() });
|
|
6003
|
+
}
|
|
6004
|
+
async function readFsResponse(api, machineId) {
|
|
6005
|
+
const value = await api.userKvGet(fsResponseKey(machineId));
|
|
6006
|
+
if (!value || typeof value !== "object") return null;
|
|
6007
|
+
const r = value;
|
|
6008
|
+
if (typeof r.nonce !== "string" || typeof r.ok !== "boolean") return null;
|
|
6009
|
+
return {
|
|
6010
|
+
nonce: r.nonce,
|
|
6011
|
+
ok: r.ok,
|
|
6012
|
+
result: r.result,
|
|
6013
|
+
error: typeof r.error === "string" ? r.error : void 0,
|
|
6014
|
+
responded_at: typeof r.responded_at === "number" ? r.responded_at : 0
|
|
6015
|
+
};
|
|
6016
|
+
}
|
|
5156
6017
|
async function readFsRequest(api, machineId) {
|
|
5157
6018
|
return parseFsRequest(await api.userKvGet(fsRequestKey(machineId)));
|
|
5158
6019
|
}
|
|
5159
6020
|
async function writeFsResponse(api, machineId, res) {
|
|
5160
6021
|
await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
|
|
5161
6022
|
}
|
|
5162
|
-
function
|
|
5163
|
-
|
|
5164
|
-
if (
|
|
5165
|
-
|
|
6023
|
+
async function resolveRemoteProjectPath(api, machineId, typedPath, timeoutMs = 12e3) {
|
|
6024
|
+
const trimmed = typedPath.trim();
|
|
6025
|
+
if (!trimmed.startsWith("~")) return trimmed;
|
|
6026
|
+
const nonce = crypto.randomBytes(8).toString("hex");
|
|
6027
|
+
try {
|
|
6028
|
+
await writeFsRequest(api, machineId, { nonce, op: "list", path: trimmed });
|
|
6029
|
+
const deadline = Date.now() + timeoutMs;
|
|
6030
|
+
while (Date.now() < deadline) {
|
|
6031
|
+
await new Promise((r) => setTimeout(r, 700));
|
|
6032
|
+
const res = await readFsResponse(api, machineId).catch(() => null);
|
|
6033
|
+
if (res && res.nonce === nonce) {
|
|
6034
|
+
const resolved = res.result && typeof res.result.path === "string" ? res.result.path : "";
|
|
6035
|
+
return resolved || trimmed;
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
6038
|
+
} catch {
|
|
6039
|
+
}
|
|
6040
|
+
return trimmed;
|
|
6041
|
+
}
|
|
6042
|
+
function machineIcon(record2) {
|
|
6043
|
+
if (record2.icon && record2.icon.trim()) return record2.icon.trim();
|
|
6044
|
+
if (record2.platform === "darwin") return "\u{1F4BB}";
|
|
6045
|
+
if (record2.platform === "win32") return "\u{1F5A5}\uFE0F";
|
|
5166
6046
|
return "\u{1F5B3}";
|
|
5167
6047
|
}
|
|
5168
|
-
function machineDisplayName(
|
|
5169
|
-
return
|
|
6048
|
+
function machineDisplayName(record2) {
|
|
6049
|
+
return record2.name?.trim() || "" || (record2.hostname || "") || (record2.id || "") || "machine";
|
|
5170
6050
|
}
|
|
5171
6051
|
async function getMachineName(api, machineId) {
|
|
5172
6052
|
const v = await api.userKvGet(nameKey(machineId));
|
|
@@ -5241,15 +6121,15 @@ async function loadMachine(api, machineId) {
|
|
|
5241
6121
|
if (icon) rec.icon = icon;
|
|
5242
6122
|
return rec;
|
|
5243
6123
|
}
|
|
5244
|
-
function daemonOnline(
|
|
5245
|
-
return !!
|
|
6124
|
+
function daemonOnline(record2, now = Date.now()) {
|
|
6125
|
+
return !!record2.daemon && now - record2.daemon.last_seen_at < DAEMON_ONLINE_WINDOW_MS;
|
|
5246
6126
|
}
|
|
5247
6127
|
function newRecord(identity) {
|
|
5248
6128
|
const now = Date.now();
|
|
5249
6129
|
return {
|
|
5250
6130
|
id: identity.machine_id,
|
|
5251
|
-
name:
|
|
5252
|
-
hostname:
|
|
6131
|
+
name: os7.hostname(),
|
|
6132
|
+
hostname: os7.hostname(),
|
|
5253
6133
|
platform: process.platform,
|
|
5254
6134
|
arch: process.arch,
|
|
5255
6135
|
version: readVersion() || void 0,
|
|
@@ -5261,15 +6141,15 @@ function newRecord(identity) {
|
|
|
5261
6141
|
}
|
|
5262
6142
|
async function updateOwnMachineRecord(api, identity, mutate) {
|
|
5263
6143
|
const existing = await loadRawMachine(api, identity.machine_id);
|
|
5264
|
-
const
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
mutate?.(
|
|
5270
|
-
|
|
5271
|
-
await api.userKvSet(machineKey(identity.machine_id),
|
|
5272
|
-
return
|
|
6144
|
+
const record2 = existing ?? newRecord(identity);
|
|
6145
|
+
record2.hostname = os7.hostname();
|
|
6146
|
+
record2.platform = process.platform;
|
|
6147
|
+
record2.arch = process.arch;
|
|
6148
|
+
record2.version = readVersion() || record2.version;
|
|
6149
|
+
mutate?.(record2);
|
|
6150
|
+
record2.updated_at = Date.now();
|
|
6151
|
+
await api.userKvSet(machineKey(identity.machine_id), record2);
|
|
6152
|
+
return record2;
|
|
5273
6153
|
}
|
|
5274
6154
|
function projectRepository(projectDir) {
|
|
5275
6155
|
try {
|
|
@@ -5283,35 +6163,54 @@ function projectRepository(projectDir) {
|
|
|
5283
6163
|
return null;
|
|
5284
6164
|
}
|
|
5285
6165
|
}
|
|
6166
|
+
function normalizeProjectDir(projectDir) {
|
|
6167
|
+
let p = projectDir.trim();
|
|
6168
|
+
if (!p) return p;
|
|
6169
|
+
if (p === "~") p = os7.homedir();
|
|
6170
|
+
else if (p.startsWith("~/")) p = path3.join(os7.homedir(), p.slice(2));
|
|
6171
|
+
return path3.resolve(p);
|
|
6172
|
+
}
|
|
5286
6173
|
async function registerProject(api, identity, projectDir) {
|
|
5287
|
-
const
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
6174
|
+
const dir2 = normalizeProjectDir(projectDir);
|
|
6175
|
+
if (!dir2) return;
|
|
6176
|
+
const repository = projectRepository(dir2);
|
|
6177
|
+
await updateOwnMachineRecord(api, identity, (record2) => {
|
|
6178
|
+
for (const existing of Object.keys(record2.projects)) {
|
|
6179
|
+
if (existing !== dir2 && normalizeProjectDir(existing) === dir2) {
|
|
6180
|
+
delete record2.projects[existing];
|
|
6181
|
+
}
|
|
6182
|
+
}
|
|
6183
|
+
record2.projects[dir2] = {
|
|
6184
|
+
name: dir2.split("/").filter(Boolean).pop() || dir2,
|
|
5291
6185
|
last_used_at: Date.now(),
|
|
5292
6186
|
repository
|
|
5293
6187
|
};
|
|
5294
6188
|
});
|
|
5295
6189
|
}
|
|
5296
6190
|
async function unregisterProject(api, identity, projectDir) {
|
|
5297
|
-
|
|
5298
|
-
|
|
6191
|
+
const dir2 = normalizeProjectDir(projectDir);
|
|
6192
|
+
await updateOwnMachineRecord(api, identity, (record2) => {
|
|
6193
|
+
for (const existing of Object.keys(record2.projects)) {
|
|
6194
|
+
if (existing === projectDir || existing === dir2 || normalizeProjectDir(existing) === dir2) {
|
|
6195
|
+
delete record2.projects[existing];
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
5299
6198
|
});
|
|
5300
6199
|
}
|
|
5301
6200
|
async function touchDaemon(api, identity, version) {
|
|
5302
|
-
await updateOwnMachineRecord(api, identity, (
|
|
6201
|
+
await updateOwnMachineRecord(api, identity, (record2) => {
|
|
5303
6202
|
const now = Date.now();
|
|
5304
|
-
|
|
6203
|
+
record2.daemon = {
|
|
5305
6204
|
version,
|
|
5306
|
-
installed_at:
|
|
6205
|
+
installed_at: record2.daemon?.installed_at ?? now,
|
|
5307
6206
|
last_seen_at: now,
|
|
5308
6207
|
pid: process.pid
|
|
5309
6208
|
};
|
|
5310
6209
|
});
|
|
5311
6210
|
}
|
|
5312
6211
|
async function clearDaemon(api, identity) {
|
|
5313
|
-
await updateOwnMachineRecord(api, identity, (
|
|
5314
|
-
|
|
6212
|
+
await updateOwnMachineRecord(api, identity, (record2) => {
|
|
6213
|
+
record2.daemon = null;
|
|
5315
6214
|
});
|
|
5316
6215
|
}
|
|
5317
6216
|
function parseCommands(value) {
|
|
@@ -5347,16 +6246,16 @@ async function clearMachineCommands(api, machineId, appliedIds) {
|
|
|
5347
6246
|
async function applyMachineCommand(api, identity, cmd) {
|
|
5348
6247
|
switch (cmd.kind) {
|
|
5349
6248
|
case "add_project": {
|
|
5350
|
-
const
|
|
5351
|
-
if (!
|
|
5352
|
-
await registerProject(api, identity,
|
|
5353
|
-
return `added project ${
|
|
6249
|
+
const path14 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
|
|
6250
|
+
if (!path14) return "add_project: ignored (no path)";
|
|
6251
|
+
await registerProject(api, identity, path14);
|
|
6252
|
+
return `added project ${path14}`;
|
|
5354
6253
|
}
|
|
5355
6254
|
case "remove_project": {
|
|
5356
|
-
const
|
|
5357
|
-
if (!
|
|
5358
|
-
await unregisterProject(api, identity,
|
|
5359
|
-
return `removed project ${
|
|
6255
|
+
const path14 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
|
|
6256
|
+
if (!path14) return "remove_project: ignored (no path)";
|
|
6257
|
+
await unregisterProject(api, identity, path14);
|
|
6258
|
+
return `removed project ${path14}`;
|
|
5360
6259
|
}
|
|
5361
6260
|
case "update":
|
|
5362
6261
|
return "update requested";
|
|
@@ -5433,7 +6332,7 @@ var PROJECT_MARKERS = [
|
|
|
5433
6332
|
];
|
|
5434
6333
|
var MAX_ENTRIES2 = 500;
|
|
5435
6334
|
function resolveBrowsePath(input3) {
|
|
5436
|
-
const home =
|
|
6335
|
+
const home = os7.homedir();
|
|
5437
6336
|
let p = (input3 ?? "").trim();
|
|
5438
6337
|
if (!p) return home;
|
|
5439
6338
|
if (p === "~") return home;
|
|
@@ -5458,7 +6357,7 @@ function markers(dirPath) {
|
|
|
5458
6357
|
return { project, repo };
|
|
5459
6358
|
}
|
|
5460
6359
|
function browseDirectory(input3, opts = {}) {
|
|
5461
|
-
const home =
|
|
6360
|
+
const home = os7.homedir();
|
|
5462
6361
|
const abs = resolveBrowsePath(input3);
|
|
5463
6362
|
const parent = path3.dirname(abs);
|
|
5464
6363
|
const base = {
|
|
@@ -5712,7 +6611,7 @@ var UPDATE_CHECK_MS = 6 * 60 * 6e4;
|
|
|
5712
6611
|
var SWEEP_MS = 10 * 6e4;
|
|
5713
6612
|
var MAX_WORKERS = 30;
|
|
5714
6613
|
var LOG_MAX_BYTES = 1e6;
|
|
5715
|
-
var LOG_FILE = path3.join(
|
|
6614
|
+
var LOG_FILE = path3.join(os7.homedir(), ".standardagents", "daemon.log");
|
|
5716
6615
|
function daemonLog(line) {
|
|
5717
6616
|
try {
|
|
5718
6617
|
fs4.mkdirSync(path3.dirname(LOG_FILE), { recursive: true });
|
|
@@ -5731,13 +6630,13 @@ function pathFromTags(tags) {
|
|
|
5731
6630
|
const tag = tags.find((t) => t.startsWith("path:"));
|
|
5732
6631
|
if (!tag) return null;
|
|
5733
6632
|
const raw = tag.slice("path:".length);
|
|
5734
|
-
return raw.replace(/^~(?=\/|$)/,
|
|
6633
|
+
return raw.replace(/^~(?=\/|$)/, os7.homedir());
|
|
5735
6634
|
}
|
|
5736
6635
|
var ThreadWorker = class {
|
|
5737
|
-
constructor(api, identity, machineName, threadId, projectDir,
|
|
6636
|
+
constructor(api, identity, machineName, threadId, projectDir, createdAt2) {
|
|
5738
6637
|
this.threadId = threadId;
|
|
5739
6638
|
this.projectDir = projectDir;
|
|
5740
|
-
this.createdAt =
|
|
6639
|
+
this.createdAt = createdAt2;
|
|
5741
6640
|
this.perm = { level: 1, alwaysAllow: /* @__PURE__ */ new Set(), allowRisk: /* @__PURE__ */ new Set() };
|
|
5742
6641
|
this.session = new ExecutionSession({
|
|
5743
6642
|
api,
|
|
@@ -5863,7 +6762,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5863
6762
|
process.exit(1);
|
|
5864
6763
|
}
|
|
5865
6764
|
let displayName = machineDisplayName(await loadMachine(api, identity.machine_id).catch(() => null) ?? {
|
|
5866
|
-
hostname:
|
|
6765
|
+
hostname: os7.hostname(),
|
|
5867
6766
|
id: identity.machine_id
|
|
5868
6767
|
});
|
|
5869
6768
|
const applied = consumeAppliedUpdate(version);
|
|
@@ -5877,8 +6776,19 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5877
6776
|
process.on("unhandledRejection", (err) => {
|
|
5878
6777
|
daemonLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
|
|
5879
6778
|
});
|
|
6779
|
+
await touchDaemon(api, identity, version).catch(
|
|
6780
|
+
(e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
6781
|
+
);
|
|
6782
|
+
const heartbeat = setInterval(() => {
|
|
6783
|
+
void touchDaemon(api, identity, version).catch(() => {
|
|
6784
|
+
});
|
|
6785
|
+
void getMachineName(api, identity.machine_id).then((n) => {
|
|
6786
|
+
if (n) displayName = n;
|
|
6787
|
+
}).catch(() => {
|
|
6788
|
+
});
|
|
6789
|
+
}, HEARTBEAT_MS);
|
|
5880
6790
|
const workers = /* @__PURE__ */ new Map();
|
|
5881
|
-
const attach = async (threadId, tags,
|
|
6791
|
+
const attach = async (threadId, tags, createdAt2 = 0) => {
|
|
5882
6792
|
if (workers.has(threadId)) return;
|
|
5883
6793
|
const projectDir = pathFromTags(tags);
|
|
5884
6794
|
if (!projectDir) return;
|
|
@@ -5895,7 +6805,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5895
6805
|
daemonLog(`cannot prepare project dir ${projectDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5896
6806
|
return;
|
|
5897
6807
|
}
|
|
5898
|
-
const worker = new ThreadWorker(api, identity, displayName, threadId, projectDir,
|
|
6808
|
+
const worker = new ThreadWorker(api, identity, displayName, threadId, projectDir, createdAt2 || Date.now());
|
|
5899
6809
|
worker.onEvicted = (id) => detach(id);
|
|
5900
6810
|
workers.set(threadId, worker);
|
|
5901
6811
|
daemonLog(`attached ${threadId.slice(0, 8)} \u2192 ${projectDir}`);
|
|
@@ -5934,17 +6844,6 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5934
6844
|
});
|
|
5935
6845
|
events.connect();
|
|
5936
6846
|
await sweep();
|
|
5937
|
-
await touchDaemon(api, identity, version).catch(
|
|
5938
|
-
(e) => daemonLog(`heartbeat failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
5939
|
-
);
|
|
5940
|
-
const heartbeat = setInterval(() => {
|
|
5941
|
-
void touchDaemon(api, identity, version).catch(() => {
|
|
5942
|
-
});
|
|
5943
|
-
void getMachineName(api, identity.machine_id).then((n) => {
|
|
5944
|
-
if (n) displayName = n;
|
|
5945
|
-
}).catch(() => {
|
|
5946
|
-
});
|
|
5947
|
-
}, HEARTBEAT_MS);
|
|
5948
6847
|
const reclaim = setInterval(() => {
|
|
5949
6848
|
for (const worker of workers.values()) {
|
|
5950
6849
|
if (!worker.isOwner) {
|
|
@@ -6121,8 +7020,8 @@ function run2(cmd, args) {
|
|
|
6121
7020
|
const output4 = `${res.stdout ?? ""}${res.stderr ?? ""}`.trim();
|
|
6122
7021
|
return { ok: res.status === 0, output: output4 };
|
|
6123
7022
|
}
|
|
6124
|
-
var plistPath = () => path3.join(
|
|
6125
|
-
var unitPath = () => path3.join(
|
|
7023
|
+
var plistPath = () => path3.join(os7.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
|
|
7024
|
+
var unitPath = () => path3.join(os7.homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
6126
7025
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
6127
7026
|
function installService(command, endpoint) {
|
|
6128
7027
|
if (process.platform === "darwin") return installLaunchd(command, endpoint);
|
|
@@ -6134,7 +7033,7 @@ function installService(command, endpoint) {
|
|
|
6134
7033
|
};
|
|
6135
7034
|
}
|
|
6136
7035
|
function installLaunchd(command, endpoint) {
|
|
6137
|
-
const logDir = path3.join(
|
|
7036
|
+
const logDir = path3.join(os7.homedir(), ".standardagents");
|
|
6138
7037
|
fs4.mkdirSync(logDir, { recursive: true });
|
|
6139
7038
|
fs4.mkdirSync(path3.dirname(plistPath()), { recursive: true });
|
|
6140
7039
|
const envEntries = [
|
|
@@ -6207,10 +7106,10 @@ WantedBy=default.target
|
|
|
6207
7106
|
if (!enable.ok) {
|
|
6208
7107
|
return { ok: false, detail: `systemctl enable failed: ${enable.output}` };
|
|
6209
7108
|
}
|
|
6210
|
-
const linger = run2("loginctl", ["enable-linger",
|
|
7109
|
+
const linger = run2("loginctl", ["enable-linger", os7.userInfo().username]);
|
|
6211
7110
|
return {
|
|
6212
7111
|
ok: true,
|
|
6213
|
-
detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${
|
|
7112
|
+
detail: `systemd user unit installed (${unitPath()})` + (linger.ok ? ", lingering enabled" : ` \u2014 enable lingering manually: sudo loginctl enable-linger ${os7.userInfo().username}`)
|
|
6214
7113
|
};
|
|
6215
7114
|
}
|
|
6216
7115
|
function uninstallService() {
|
|
@@ -6338,7 +7237,7 @@ async function installCommand(endpointFlag) {
|
|
|
6338
7237
|
const api = await ensureSignedIn(endpoint);
|
|
6339
7238
|
const identity = loadMachineIdentity();
|
|
6340
7239
|
const existing = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
6341
|
-
const suggested = machineDisplayName(existing ?? { hostname:
|
|
7240
|
+
const suggested = machineDisplayName(existing ?? { hostname: os7.hostname(), id: identity.machine_id });
|
|
6342
7241
|
const rl = readline2.createInterface({ input: stdin, output: stdout });
|
|
6343
7242
|
const answer = (await rl.question(
|
|
6344
7243
|
`${c2.bold}Machine name${c2.reset} ${c2.dim}(shown in the session picker)${c2.reset} [${suggested}]: `
|
|
@@ -6362,12 +7261,12 @@ async function installCommand(endpointFlag) {
|
|
|
6362
7261
|
`);
|
|
6363
7262
|
stdout.write(`${c2.dim}Waiting for the daemon's first heartbeat\u2026${c2.reset}
|
|
6364
7263
|
`);
|
|
6365
|
-
const deadline = Date.now() +
|
|
7264
|
+
const deadline = Date.now() + 6e4;
|
|
6366
7265
|
let alive = false;
|
|
6367
7266
|
while (Date.now() < deadline) {
|
|
6368
7267
|
await new Promise((r) => setTimeout(r, 2e3));
|
|
6369
|
-
const
|
|
6370
|
-
if (
|
|
7268
|
+
const record2 = await loadMachine(api, identity.machine_id);
|
|
7269
|
+
if (record2 && daemonOnline(record2)) {
|
|
6371
7270
|
alive = true;
|
|
6372
7271
|
break;
|
|
6373
7272
|
}
|
|
@@ -6398,7 +7297,7 @@ async function statusCommand() {
|
|
|
6398
7297
|
const cred = getCredential(endpoint);
|
|
6399
7298
|
if (!cred) {
|
|
6400
7299
|
stdout.write(
|
|
6401
|
-
`${c2.bold}Machine:${c2.reset} ${
|
|
7300
|
+
`${c2.bold}Machine:${c2.reset} ${os7.hostname()} ${c2.dim}(${identity.machine_id})${c2.reset}
|
|
6402
7301
|
`
|
|
6403
7302
|
);
|
|
6404
7303
|
stdout.write(`${c2.bold}Account:${c2.reset} ${c2.yellow}not signed in to ${endpoint}${c2.reset}
|
|
@@ -6407,21 +7306,21 @@ async function statusCommand() {
|
|
|
6407
7306
|
}
|
|
6408
7307
|
relaxTlsForLocalEndpoint(endpoint);
|
|
6409
7308
|
const api = new ApiClient(endpoint, cred.access_token);
|
|
6410
|
-
const
|
|
7309
|
+
const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
6411
7310
|
stdout.write(
|
|
6412
|
-
`${c2.bold}Machine:${c2.reset} ${machineDisplayName(
|
|
7311
|
+
`${c2.bold}Machine:${c2.reset} ${machineDisplayName(record2 ?? { hostname: os7.hostname(), id: identity.machine_id })} ${c2.dim}(${identity.machine_id})${c2.reset}
|
|
6413
7312
|
`
|
|
6414
7313
|
);
|
|
6415
|
-
if (!
|
|
7314
|
+
if (!record2) {
|
|
6416
7315
|
stdout.write(`${c2.bold}Registry:${c2.reset} not registered yet
|
|
6417
7316
|
`);
|
|
6418
7317
|
return;
|
|
6419
7318
|
}
|
|
6420
|
-
const online = daemonOnline(
|
|
6421
|
-
const seen =
|
|
7319
|
+
const online = daemonOnline(record2);
|
|
7320
|
+
const seen = record2.daemon ? `${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
|
|
6422
7321
|
stdout.write(`${c2.bold}Registry:${c2.reset} ${online ? `${c2.green}online${c2.reset}` : `${c2.yellow}offline${c2.reset}`} \xB7 last heartbeat ${seen}
|
|
6423
7322
|
`);
|
|
6424
|
-
const projects = Object.keys(
|
|
7323
|
+
const projects = Object.keys(record2.projects);
|
|
6425
7324
|
stdout.write(`${c2.bold}Projects:${c2.reset} ${projects.length ? "" : c2.dim + "none registered" + c2.reset}
|
|
6426
7325
|
`);
|
|
6427
7326
|
for (const p of projects.sort()) stdout.write(` ${c2.dim}${p}${c2.reset}
|
|
@@ -6442,8 +7341,8 @@ async function projectCommand(action, target) {
|
|
|
6442
7341
|
const endpoint = resolveEndpoint();
|
|
6443
7342
|
const api = await ensureSignedIn(endpoint);
|
|
6444
7343
|
const identity = loadMachineIdentity();
|
|
6445
|
-
const
|
|
6446
|
-
const displayName = machineDisplayName(
|
|
7344
|
+
const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
7345
|
+
const displayName = machineDisplayName(record2 ?? { hostname: os7.hostname(), id: identity.machine_id });
|
|
6447
7346
|
if (action === "add") {
|
|
6448
7347
|
await registerProject(api, identity, dir2);
|
|
6449
7348
|
stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir2} for remote sessions on ${displayName}.
|
|
@@ -6601,7 +7500,7 @@ function printAssistant(tui, text) {
|
|
|
6601
7500
|
tui.clearStream();
|
|
6602
7501
|
tui.print("");
|
|
6603
7502
|
let dotted = false;
|
|
6604
|
-
for (const line of
|
|
7503
|
+
for (const line of renderStreamingMarkdown(text, cols2)) {
|
|
6605
7504
|
if (!dotted && line.trim()) {
|
|
6606
7505
|
tui.print(`${c3.gray}\u2022${c3.reset} ${line}`);
|
|
6607
7506
|
dotted = true;
|
|
@@ -6643,18 +7542,21 @@ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.rese
|
|
|
6643
7542
|
`);
|
|
6644
7543
|
}
|
|
6645
7544
|
function printWelcome(endpoint, projectDir) {
|
|
6646
|
-
const home =
|
|
7545
|
+
const home = os7.homedir();
|
|
6647
7546
|
const dir2 = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
6648
7547
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
6649
7548
|
const version = readVersion();
|
|
6650
7549
|
const pad = " ";
|
|
7550
|
+
const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
|
|
7551
|
+
const terminalColumns = Math.max(20, process.stdout.columns || 80);
|
|
7552
|
+
const metaWidth = Math.max(1, terminalColumns - pad.length - markWidth - 3 - 1);
|
|
7553
|
+
const displayDir = truncateMiddle(dir2, metaWidth);
|
|
6651
7554
|
const meta = [
|
|
6652
7555
|
`${c3.bold}${gradientText("Standard Code")}${c3.reset}${version ? ` ${c3.dim}v${version}${c3.reset}` : ""}`,
|
|
6653
7556
|
`${c3.dim}terminal coding agent${c3.reset}`,
|
|
6654
7557
|
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c3.teal}${host}${c3.reset}`],
|
|
6655
|
-
`${c3.dim}${
|
|
7558
|
+
`${c3.dim}${displayDir}${c3.reset}`
|
|
6656
7559
|
];
|
|
6657
|
-
const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
|
|
6658
7560
|
const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
|
|
6659
7561
|
stdout.write("\n");
|
|
6660
7562
|
for (let i = 0; i < LOGO_MARK.length; i++) {
|
|
@@ -6711,7 +7613,7 @@ async function main() {
|
|
|
6711
7613
|
const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
|
|
6712
7614
|
const dirArg = cliArgs.dir;
|
|
6713
7615
|
const projectDir = path3.resolve(dirArg || process.cwd());
|
|
6714
|
-
const machine =
|
|
7616
|
+
const machine = os7.hostname();
|
|
6715
7617
|
const reader = { rl: null };
|
|
6716
7618
|
let handoffClosing = false;
|
|
6717
7619
|
let preflightArmed = false;
|
|
@@ -6896,7 +7798,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
6896
7798
|
void registerProject(api, identity, projectDir).catch(() => {
|
|
6897
7799
|
});
|
|
6898
7800
|
const tui = new Tui(1);
|
|
6899
|
-
const home =
|
|
7801
|
+
const home = os7.homedir();
|
|
6900
7802
|
const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
6901
7803
|
const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
|
|
6902
7804
|
const session = { mode: "local", identity };
|
|
@@ -6919,7 +7821,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
6919
7821
|
]
|
|
6920
7822
|
);
|
|
6921
7823
|
if (where) {
|
|
6922
|
-
const remotePath = await pickRemoteProject(tui, where);
|
|
7824
|
+
const remotePath = await pickRemoteProject(tui, api, where);
|
|
6923
7825
|
if (remotePath) {
|
|
6924
7826
|
session.mode = "remote";
|
|
6925
7827
|
session.runner = where;
|
|
@@ -6955,6 +7857,10 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
6955
7857
|
if (session.mode === "remote" && session.runner && session.remotePath) {
|
|
6956
7858
|
await api.kvSet(id, "session_info", { cwd: session.remotePath, machine: session.runner.name }).catch(() => {
|
|
6957
7859
|
});
|
|
7860
|
+
void enqueueMachineCommand(api, session.runner.id, "add_project", {
|
|
7861
|
+
path: session.remotePath
|
|
7862
|
+
}).catch(() => {
|
|
7863
|
+
});
|
|
6958
7864
|
}
|
|
6959
7865
|
return id;
|
|
6960
7866
|
};
|
|
@@ -6994,7 +7900,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
6994
7900
|
function shortenPath(p, max = 38) {
|
|
6995
7901
|
return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
|
|
6996
7902
|
}
|
|
6997
|
-
async function pickRemoteProject(tui, runner) {
|
|
7903
|
+
async function pickRemoteProject(tui, api, runner) {
|
|
6998
7904
|
const ENTER_PATH = "__enter_path__";
|
|
6999
7905
|
const projects = Object.entries(runner.projects).sort(
|
|
7000
7906
|
(a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
|
|
@@ -7021,7 +7927,7 @@ async function pickRemoteProject(tui, runner) {
|
|
|
7021
7927
|
tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
|
|
7022
7928
|
return null;
|
|
7023
7929
|
}
|
|
7024
|
-
return trimmed;
|
|
7930
|
+
return await resolveRemoteProjectPath(api, runner.id, trimmed);
|
|
7025
7931
|
}
|
|
7026
7932
|
function isSilentMessage(m) {
|
|
7027
7933
|
return m?.silent === true || m?.metadata?.silent === true;
|
|
@@ -7056,31 +7962,6 @@ function relativeTime(unixSeconds) {
|
|
|
7056
7962
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
7057
7963
|
return `${Math.floor(diff / 86400)}d ago`;
|
|
7058
7964
|
}
|
|
7059
|
-
function hasToolCalls(m) {
|
|
7060
|
-
const tc = m?.tool_calls;
|
|
7061
|
-
if (Array.isArray(tc)) return tc.length > 0;
|
|
7062
|
-
if (typeof tc === "string") {
|
|
7063
|
-
const s = tc.trim();
|
|
7064
|
-
return s.length > 0 && s !== "null" && s !== "[]";
|
|
7065
|
-
}
|
|
7066
|
-
return false;
|
|
7067
|
-
}
|
|
7068
|
-
function messageText(content) {
|
|
7069
|
-
if (typeof content === "string") return content;
|
|
7070
|
-
if (Array.isArray(content)) {
|
|
7071
|
-
return content.map((b) => typeof b === "string" ? b : typeof b?.text === "string" ? b.text : "").join("");
|
|
7072
|
-
}
|
|
7073
|
-
return "";
|
|
7074
|
-
}
|
|
7075
|
-
function threadBusy(msgs) {
|
|
7076
|
-
if (!msgs.length) return false;
|
|
7077
|
-
if (msgs.some((m) => m.status === "pending")) return true;
|
|
7078
|
-
const last = [...msgs].sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0))[0];
|
|
7079
|
-
if (!last) return false;
|
|
7080
|
-
if (last.role === "user" || last.role === "tool") return true;
|
|
7081
|
-
if (last.role === "assistant") return hasToolCalls(last);
|
|
7082
|
-
return false;
|
|
7083
|
-
}
|
|
7084
7965
|
async function printHistory(api, threadId, tui) {
|
|
7085
7966
|
let msgs;
|
|
7086
7967
|
try {
|
|
@@ -7139,9 +8020,19 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
7139
8020
|
saveApprovals(api, threadId, perm);
|
|
7140
8021
|
const attaching = startLoader("Attaching to thread");
|
|
7141
8022
|
let busy = false;
|
|
7142
|
-
let
|
|
7143
|
-
|
|
7144
|
-
let
|
|
8023
|
+
let sharedMessaging = emptySharedMessagingSnapshot();
|
|
8024
|
+
let mirroredDraftRefs = [];
|
|
8025
|
+
let sharedMessagingReady = false;
|
|
8026
|
+
let sharedMessagingUnavailableShown = false;
|
|
8027
|
+
const messagingOrigin = {
|
|
8028
|
+
originClientId: `tui:${Math.random().toString(36).slice(2, 10)}`,
|
|
8029
|
+
originClientKind: "tui"
|
|
8030
|
+
};
|
|
8031
|
+
let editingPendingId = null;
|
|
8032
|
+
let reconcileSharedMessaging = async () => {
|
|
8033
|
+
};
|
|
8034
|
+
let refreshSessionProjection = async () => {
|
|
8035
|
+
};
|
|
7145
8036
|
const shownIds = /* @__PURE__ */ new Set();
|
|
7146
8037
|
const pendingSent = /* @__PURE__ */ new Map();
|
|
7147
8038
|
let lastSent = null;
|
|
@@ -7239,6 +8130,10 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
7239
8130
|
bridge = exec.bridge;
|
|
7240
8131
|
}
|
|
7241
8132
|
const stream = new MessageStream(api, threadId, {
|
|
8133
|
+
onOpen: () => {
|
|
8134
|
+
void reconcileSharedMessaging(true);
|
|
8135
|
+
void refreshSessionProjection();
|
|
8136
|
+
},
|
|
7242
8137
|
// Live streaming preview: answer text and (opt-in) internal reasoning feed
|
|
7243
8138
|
// the TUI's ephemeral preview; the committed message still renders from
|
|
7244
8139
|
// polling, which calls tui.clearStream() first so there's no double-render.
|
|
@@ -7246,6 +8141,12 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
7246
8141
|
onReasoningChunk: (text, mid) => tui.streamThinkingDelta(text, mid),
|
|
7247
8142
|
onAssistantText: () => {
|
|
7248
8143
|
},
|
|
8144
|
+
onMessage: (message) => {
|
|
8145
|
+
if (message?.role === "user" || message?.status === "pending") {
|
|
8146
|
+
busy = true;
|
|
8147
|
+
tui.setWorking(true);
|
|
8148
|
+
}
|
|
8149
|
+
},
|
|
7249
8150
|
onEvent: (eventType, data) => {
|
|
7250
8151
|
if (eventType === "generation" && typeof data?.outputTokens === "number") {
|
|
7251
8152
|
liveOut = data.outputTokens;
|
|
@@ -7258,6 +8159,8 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
7258
8159
|
refreshStatus();
|
|
7259
8160
|
} else if (eventType === "goal_updated" && data) {
|
|
7260
8161
|
tui.setGoal(data);
|
|
8162
|
+
} else if (eventType === SHARED_MESSAGING_EVENT) {
|
|
8163
|
+
void reconcileSharedMessaging(true);
|
|
7261
8164
|
}
|
|
7262
8165
|
},
|
|
7263
8166
|
// A failed turn whose message is the lease service's at-limit denial → offer
|
|
@@ -7334,7 +8237,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
7334
8237
|
const sessionEnded = new Promise((r) => endSession = r);
|
|
7335
8238
|
const quit = async () => {
|
|
7336
8239
|
tui.end();
|
|
7337
|
-
const stopped2 = bridge?.isOwner ?? false ? api.
|
|
8240
|
+
const stopped2 = bridge?.isOwner ?? false ? api.requestSharedStop(threadId, messagingOrigin).catch(() => {
|
|
7338
8241
|
}) : Promise.resolve();
|
|
7339
8242
|
const procsStopped2 = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
|
|
7340
8243
|
bridge?.close();
|
|
@@ -7409,23 +8312,89 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
7409
8312
|
};
|
|
7410
8313
|
const extFor = (mime) => ({ "image/png": "png", "image/jpeg": "jpg", "image/gif": "gif", "image/webp": "webp" })[mime] ?? "bin";
|
|
7411
8314
|
const toAttachments = (images) => images.map((img) => ({ name: `image-${img.seq}.${extFor(img.mime)}`, mimeType: img.mime, data: img.data }));
|
|
7412
|
-
const
|
|
7413
|
-
|
|
7414
|
-
|
|
8315
|
+
const toSharedAttachments = (images) => toAttachments(images);
|
|
8316
|
+
const fromSharedAttachments = (attachments2) => attachments2.filter(isInlineSharedAttachment).map((attachment, index) => {
|
|
8317
|
+
const namedSequence = /(?:image-|Image\s+)(\d+)/i.exec(attachment.name)?.[1];
|
|
8318
|
+
return {
|
|
8319
|
+
seq: namedSequence ? Number(namedSequence) : index + 1,
|
|
8320
|
+
data: attachment.data,
|
|
8321
|
+
mime: attachment.mimeType
|
|
8322
|
+
};
|
|
8323
|
+
});
|
|
8324
|
+
const applySharedMessaging = (snapshot, mirrorDraft) => {
|
|
8325
|
+
const firstSnapshot = !sharedMessagingReady;
|
|
8326
|
+
const previousDraftRevision = sharedMessaging.draft.revision;
|
|
8327
|
+
sharedMessaging = firstSnapshot ? snapshot : mergeSharedMessagingSnapshot(sharedMessaging, snapshot);
|
|
8328
|
+
sharedMessagingReady = true;
|
|
8329
|
+
sharedMessagingUnavailableShown = false;
|
|
8330
|
+
tui.setQueuedCount(sharedMessaging.pending.items.length);
|
|
8331
|
+
if (mirrorDraft && (firstSnapshot || sharedMessaging.draft.revision > previousDraftRevision) && (firstSnapshot || sharedMessaging.draft.originClientId !== messagingOrigin.originClientId)) {
|
|
8332
|
+
mirroredDraftRefs = sharedMessaging.draft.attachments.filter(isSharedAttachmentRef);
|
|
8333
|
+
tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
|
|
8334
|
+
tui.setInput(
|
|
8335
|
+
sharedMessaging.draft.content,
|
|
8336
|
+
fromSharedAttachments(sharedMessaging.draft.attachments),
|
|
8337
|
+
false,
|
|
8338
|
+
true
|
|
8339
|
+
);
|
|
8340
|
+
}
|
|
8341
|
+
};
|
|
8342
|
+
reconcileSharedMessaging = async (mirrorDraft = false) => {
|
|
8343
|
+
try {
|
|
8344
|
+
applySharedMessaging(await api.getSharedMessaging(threadId), mirrorDraft);
|
|
8345
|
+
} catch (error) {
|
|
8346
|
+
if (!sharedMessagingReady && !sharedMessagingUnavailableShown) {
|
|
8347
|
+
sharedMessagingUnavailableShown = true;
|
|
8348
|
+
tui.print(`${c3.dim}shared messaging unavailable: ${error instanceof Error ? error.message : String(error)}${c3.reset}`);
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
8351
|
+
};
|
|
8352
|
+
const onTerminalResume = () => void reconcileSharedMessaging(true);
|
|
8353
|
+
process.on("SIGCONT", onTerminalResume);
|
|
8354
|
+
const applySharedMutation = (promise) => promise.then((snapshot) => {
|
|
8355
|
+
applySharedMessaging(snapshot, false);
|
|
8356
|
+
return true;
|
|
8357
|
+
}).catch((error) => {
|
|
8358
|
+
tui.print(`${c3.dim}shared messaging failed: ${error instanceof Error ? error.message : String(error)}${c3.reset}`);
|
|
8359
|
+
return false;
|
|
8360
|
+
});
|
|
8361
|
+
const appendSharedPending = (text, images, refs = []) => applySharedMutation(api.appendPendingInput(threadId, {
|
|
8362
|
+
content: text,
|
|
8363
|
+
attachments: [...refs, ...toSharedAttachments(images)],
|
|
8364
|
+
...messagingOrigin
|
|
8365
|
+
}));
|
|
8366
|
+
const steerSharedInput = (text, images, refs = []) => applySharedMutation(api.steerInput(threadId, {
|
|
8367
|
+
content: text,
|
|
8368
|
+
attachments: [...refs, ...toSharedAttachments(images)],
|
|
8369
|
+
...messagingOrigin
|
|
8370
|
+
}));
|
|
8371
|
+
const editSharedPending = (item, text, images, refs) => applySharedMutation(api.updatePendingInput(threadId, item.id, {
|
|
8372
|
+
content: text,
|
|
8373
|
+
attachments: [
|
|
8374
|
+
...refs,
|
|
8375
|
+
...toSharedAttachments(images)
|
|
8376
|
+
],
|
|
8377
|
+
...messagingOrigin
|
|
8378
|
+
}));
|
|
8379
|
+
const dismissSharedPending = (item) => applySharedMutation(api.dismissPendingInput(threadId, item.id, messagingOrigin));
|
|
8380
|
+
const promoteSharedPending = (item) => applySharedMutation(api.steerPendingInput(threadId, item.id, messagingOrigin));
|
|
8381
|
+
const sendNow = async (text, images = [], refs = []) => {
|
|
8382
|
+
lastSent = { text, images, refs };
|
|
8383
|
+
tui.printUserMessage(text || `\u{1F4CE} ${refs.length + images.length} attachment(s)`);
|
|
7415
8384
|
const key = text.trim();
|
|
7416
8385
|
pendingSent.set(key, (pendingSent.get(key) ?? 0) + 1);
|
|
7417
8386
|
try {
|
|
7418
|
-
await api.sendMessage(threadId, text, toAttachments(images));
|
|
8387
|
+
await api.sendMessage(threadId, text, [...refs, ...toAttachments(images)]);
|
|
7419
8388
|
} catch (e) {
|
|
7420
8389
|
const n = (pendingSent.get(key) ?? 1) - 1;
|
|
7421
8390
|
if (n > 0) pendingSent.set(key, n);
|
|
7422
8391
|
else pendingSent.delete(key);
|
|
7423
8392
|
tui.print(`${c3.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c3.reset}`);
|
|
7424
|
-
return;
|
|
8393
|
+
return false;
|
|
7425
8394
|
}
|
|
7426
|
-
interrupting = false;
|
|
7427
8395
|
busy = true;
|
|
7428
8396
|
tui.setWorking(true);
|
|
8397
|
+
return true;
|
|
7429
8398
|
};
|
|
7430
8399
|
const whereLabel = remote ? runnerName : "this machine";
|
|
7431
8400
|
let bangRunning = false;
|
|
@@ -7446,11 +8415,34 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
7446
8415
|
bangRunning = false;
|
|
7447
8416
|
}
|
|
7448
8417
|
};
|
|
7449
|
-
const
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
8418
|
+
const openPendingMenu = async () => {
|
|
8419
|
+
const items = sharedMessaging.pending.items;
|
|
8420
|
+
if (!items.length) {
|
|
8421
|
+
tui.print(`${c3.dim}No pending messages.${c3.reset}`);
|
|
8422
|
+
return;
|
|
8423
|
+
}
|
|
8424
|
+
const picked = await tui.select("Pending messages", items.map((item, index) => ({
|
|
8425
|
+
label: item.content.replace(/\s+/g, " "),
|
|
8426
|
+
hint: `${index + 1} of ${items.length}`,
|
|
8427
|
+
value: item
|
|
8428
|
+
})));
|
|
8429
|
+
if (!picked) return;
|
|
8430
|
+
const action = await tui.select("Pending message", [
|
|
8431
|
+
{ label: "Edit", value: "edit" },
|
|
8432
|
+
{ label: "Steer next", value: "steer" },
|
|
8433
|
+
{ label: "Dismiss", value: "dismiss" }
|
|
8434
|
+
]);
|
|
8435
|
+
if (!action) return;
|
|
8436
|
+
if (action === "edit") {
|
|
8437
|
+
editingPendingId = picked.id;
|
|
8438
|
+
mirroredDraftRefs = picked.attachments.filter(isSharedAttachmentRef);
|
|
8439
|
+
tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
|
|
8440
|
+
tui.setInput(picked.content, fromSharedAttachments(picked.attachments), true, true);
|
|
8441
|
+
} else if (action === "steer") {
|
|
8442
|
+
await promoteSharedPending(picked);
|
|
8443
|
+
} else {
|
|
8444
|
+
await dismissSharedPending(picked);
|
|
8445
|
+
}
|
|
7454
8446
|
};
|
|
7455
8447
|
const requestCompaction = async () => {
|
|
7456
8448
|
try {
|
|
@@ -7554,7 +8546,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
7554
8546
|
tui.print(`${c3.green}\u2713${c3.reset} ${c3.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c3.reset}`);
|
|
7555
8547
|
if (opts.auto && lastSent) {
|
|
7556
8548
|
tui.print(`${c3.gray}Continuing\u2026${c3.reset}`);
|
|
7557
|
-
await sendNow(lastSent.text, lastSent.images);
|
|
8549
|
+
await sendNow(lastSent.text, lastSent.images, lastSent.refs);
|
|
7558
8550
|
}
|
|
7559
8551
|
} finally {
|
|
7560
8552
|
upgradeInFlight = false;
|
|
@@ -7587,6 +8579,15 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
7587
8579
|
hint: () => tui.contextPctLabel() || "free up context",
|
|
7588
8580
|
run: requestCompaction
|
|
7589
8581
|
},
|
|
8582
|
+
{
|
|
8583
|
+
name: "queue",
|
|
8584
|
+
label: "Pending messages",
|
|
8585
|
+
hint: () => {
|
|
8586
|
+
const count = sharedMessaging.pending.items.length;
|
|
8587
|
+
return count ? `${count} pending` : "none";
|
|
8588
|
+
},
|
|
8589
|
+
run: openPendingMenu
|
|
8590
|
+
},
|
|
7590
8591
|
{ name: "level", label: "Auto-accept level", hint: () => `level ${tui.level}`, run: () => runLevelMenu(tui, perm) },
|
|
7591
8592
|
{
|
|
7592
8593
|
name: "permissions",
|
|
@@ -7648,26 +8649,46 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
7648
8649
|
]);
|
|
7649
8650
|
const history = await loadHistory(api, threadId, historySeedThreadId);
|
|
7650
8651
|
tui.setHistory(history);
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
8652
|
+
await reconcileSharedMessaging(true);
|
|
8653
|
+
let draftTimer;
|
|
8654
|
+
const clearComposerDraft = () => {
|
|
8655
|
+
if (draftTimer) clearTimeout(draftTimer);
|
|
8656
|
+
draftTimer = void 0;
|
|
8657
|
+
mirroredDraftRefs = [];
|
|
8658
|
+
tui.setExternalAttachmentNames([]);
|
|
8659
|
+
if (sharedMessagingReady) void applySharedMutation(api.clearSharedDraft(threadId, messagingOrigin));
|
|
7658
8660
|
};
|
|
7659
|
-
tui.onDraftChange = (textVal) => {
|
|
7660
|
-
if (
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
8661
|
+
tui.onDraftChange = (textVal, images) => {
|
|
8662
|
+
if (draftTimer) clearTimeout(draftTimer);
|
|
8663
|
+
draftTimer = setTimeout(() => {
|
|
8664
|
+
draftTimer = void 0;
|
|
8665
|
+
if (sharedMessagingReady) {
|
|
8666
|
+
const hasDraft = !!textVal.trim() || images.length > 0 || mirroredDraftRefs.length > 0;
|
|
8667
|
+
const mutation = {
|
|
8668
|
+
content: textVal,
|
|
8669
|
+
attachments: [
|
|
8670
|
+
...hasDraft ? mirroredDraftRefs : [],
|
|
8671
|
+
...toSharedAttachments(images)
|
|
8672
|
+
],
|
|
8673
|
+
...messagingOrigin
|
|
8674
|
+
};
|
|
8675
|
+
void applySharedMutation(
|
|
8676
|
+
hasDraft ? api.putSharedDraft(threadId, mutation) : api.clearSharedDraft(threadId, messagingOrigin)
|
|
8677
|
+
);
|
|
8678
|
+
}
|
|
8679
|
+
}, 150);
|
|
7666
8680
|
};
|
|
7667
|
-
|
|
7668
|
-
|
|
8681
|
+
const submitComposer = async (text, images, steer) => {
|
|
8682
|
+
const draftRefs = mirroredDraftRefs;
|
|
8683
|
+
if (!sharedMessagingReady && (busy || steer || editingPendingId !== null)) {
|
|
8684
|
+
tui.print(`${c3.dim}Restoring shared message state \u2014 try again in a moment.${c3.reset}`);
|
|
8685
|
+
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
8686
|
+
tui.setInput(text, images);
|
|
8687
|
+
return;
|
|
8688
|
+
}
|
|
8689
|
+
clearComposerDraft();
|
|
7669
8690
|
const trimmed = text.trimStart();
|
|
7670
|
-
if (trimmed.startsWith("!") && !trimmed.startsWith("!!")) {
|
|
8691
|
+
if (trimmed.startsWith("!") && !trimmed.startsWith("!!") && images.length === 0 && draftRefs.length === 0) {
|
|
7671
8692
|
const command = trimmed.slice(1).trim();
|
|
7672
8693
|
if (command) {
|
|
7673
8694
|
appendHistory(api, threadId, history, text);
|
|
@@ -7676,38 +8697,71 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
7676
8697
|
return;
|
|
7677
8698
|
}
|
|
7678
8699
|
const outgoing = trimmed.startsWith("!!") ? text.replace("!!", "!") : text;
|
|
7679
|
-
appendHistory(api, threadId, history, outgoing);
|
|
8700
|
+
if (outgoing.trim()) appendHistory(api, threadId, history, outgoing);
|
|
7680
8701
|
text = outgoing;
|
|
7681
|
-
if (
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
8702
|
+
if (editingPendingId) {
|
|
8703
|
+
const pendingId = editingPendingId;
|
|
8704
|
+
const item = sharedMessaging.pending.items.find((candidate) => candidate.id === pendingId);
|
|
8705
|
+
editingPendingId = null;
|
|
8706
|
+
if (!item) {
|
|
8707
|
+
tui.print(`${c3.dim}That pending message was already dispatched or dismissed.${c3.reset}`);
|
|
8708
|
+
return;
|
|
8709
|
+
}
|
|
8710
|
+
const updated = await editSharedPending(item, text, images, draftRefs);
|
|
8711
|
+
const promoted = !steer || !updated ? updated : await applySharedMutation(api.steerPendingInput(threadId, pendingId, messagingOrigin));
|
|
8712
|
+
if (!promoted) {
|
|
8713
|
+
mirroredDraftRefs = item.attachments.filter(isSharedAttachmentRef);
|
|
8714
|
+
tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
|
|
8715
|
+
tui.setInput(text, images);
|
|
8716
|
+
}
|
|
8717
|
+
return;
|
|
8718
|
+
}
|
|
8719
|
+
if (steer) {
|
|
8720
|
+
tui.print(`${c3.yellow}\u21AA steering at the next safe model boundary:${c3.reset} ${text}`);
|
|
8721
|
+
if (!await steerSharedInput(text, images, draftRefs)) {
|
|
8722
|
+
mirroredDraftRefs = draftRefs;
|
|
8723
|
+
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
8724
|
+
tui.setInput(text, images);
|
|
8725
|
+
}
|
|
7686
8726
|
return;
|
|
7687
8727
|
}
|
|
7688
8728
|
if (busy) {
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
8729
|
+
if (await appendSharedPending(text, images, draftRefs)) {
|
|
8730
|
+
tui.print(`${c3.gray}\u23F3 pending:${c3.reset} ${text} ${c3.dim}(/queue to edit, steer, or dismiss)${c3.reset}`);
|
|
8731
|
+
} else {
|
|
8732
|
+
mirroredDraftRefs = draftRefs;
|
|
8733
|
+
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
8734
|
+
tui.setInput(text, images);
|
|
8735
|
+
}
|
|
7692
8736
|
} else {
|
|
7693
|
-
|
|
8737
|
+
if (!await sendNow(text, images, draftRefs)) {
|
|
8738
|
+
mirroredDraftRefs = draftRefs;
|
|
8739
|
+
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
8740
|
+
tui.setInput(text, images);
|
|
8741
|
+
}
|
|
7694
8742
|
}
|
|
7695
8743
|
};
|
|
8744
|
+
tui.onSubmit = (text, images) => {
|
|
8745
|
+
void submitComposer(text, images, false);
|
|
8746
|
+
};
|
|
8747
|
+
tui.onSteer = (text, images) => {
|
|
8748
|
+
void submitComposer(text, images, true);
|
|
8749
|
+
};
|
|
7696
8750
|
tui.onInterrupt = () => {
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
8751
|
+
const firstPending = sharedMessaging.pending.items[0];
|
|
8752
|
+
if (!busy && firstPending) {
|
|
8753
|
+
tui.print(`${c3.yellow}\u21AA steering the first pending message\u2026${c3.reset}`);
|
|
8754
|
+
void promoteSharedPending(firstPending);
|
|
8755
|
+
return;
|
|
8756
|
+
}
|
|
8757
|
+
if (busy) {
|
|
8758
|
+
if (!sharedMessagingReady) {
|
|
8759
|
+
tui.print(`${c3.dim}Shared messaging is not connected; the session was not stopped.${c3.reset}`);
|
|
8760
|
+
return;
|
|
8761
|
+
}
|
|
8762
|
+
const advancing = sharedMessaging.pending.items.length > 0;
|
|
8763
|
+
tui.print(`${c3.yellow}${advancing ? "[stopping; next pending message will run]" : "[stopping at the next safe boundary]"}${c3.reset}`);
|
|
8764
|
+
void applySharedMutation(api.requestSharedStop(threadId, messagingOrigin));
|
|
7711
8765
|
}
|
|
7712
8766
|
};
|
|
7713
8767
|
tui.onBgBadge = () => {
|
|
@@ -7715,11 +8769,13 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
7715
8769
|
});
|
|
7716
8770
|
};
|
|
7717
8771
|
tui.onUpArrow = () => {
|
|
7718
|
-
if (tui.getInput().trim() ||
|
|
7719
|
-
const
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
8772
|
+
if (tui.getInput().trim() || tui.hasExternalAttachments()) return false;
|
|
8773
|
+
const item = sharedMessaging.pending.items.at(-1);
|
|
8774
|
+
if (!item) return false;
|
|
8775
|
+
editingPendingId = item.id;
|
|
8776
|
+
mirroredDraftRefs = item.attachments.filter(isSharedAttachmentRef);
|
|
8777
|
+
tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
|
|
8778
|
+
tui.setInput(item.content, fromSharedAttachments(item.attachments), true, true);
|
|
7723
8779
|
return true;
|
|
7724
8780
|
};
|
|
7725
8781
|
events.connect();
|
|
@@ -7795,16 +8851,26 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
7795
8851
|
};
|
|
7796
8852
|
const poll = async () => {
|
|
7797
8853
|
let msgs;
|
|
8854
|
+
let serverBusy = null;
|
|
8855
|
+
let serverTool = null;
|
|
7798
8856
|
try {
|
|
7799
|
-
|
|
8857
|
+
const snapshot = await api.getSessionState(threadId);
|
|
8858
|
+
msgs = snapshot.messages.slice(-60);
|
|
8859
|
+
serverBusy = snapshot.busy;
|
|
8860
|
+
serverTool = snapshot.current_tool;
|
|
7800
8861
|
} catch {
|
|
7801
|
-
|
|
8862
|
+
try {
|
|
8863
|
+
msgs = await api.getMessages(threadId, 60);
|
|
8864
|
+
} catch {
|
|
8865
|
+
return;
|
|
8866
|
+
}
|
|
7802
8867
|
}
|
|
7803
8868
|
const sorted = [...msgs].sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
|
|
7804
8869
|
for (const m of sorted) {
|
|
7805
|
-
if (shownIds.has(m.id)
|
|
7806
|
-
shownIds.add(m.id);
|
|
8870
|
+
if (shownIds.has(m.id)) continue;
|
|
7807
8871
|
const text = messageText(m.content).trim();
|
|
8872
|
+
if (!transcriptMessageReady(m, text)) continue;
|
|
8873
|
+
shownIds.add(m.id);
|
|
7808
8874
|
const denial = typeof m.error === "string" && m.error || text;
|
|
7809
8875
|
if (denial && isSessionLimitError(denial)) {
|
|
7810
8876
|
void offerUpgrade({ auto: true });
|
|
@@ -7831,19 +8897,20 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
7831
8897
|
}
|
|
7832
8898
|
}
|
|
7833
8899
|
}
|
|
7834
|
-
const polledBusy = threadBusy(msgs);
|
|
7835
|
-
if (
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
} else {
|
|
7839
|
-
|
|
8900
|
+
const polledBusy = (serverBusy ?? false) || threadBusy(msgs);
|
|
8901
|
+
if (serverTool && !activeSteps.has(serverTool.id)) {
|
|
8902
|
+
activeSteps.set(serverTool.id, serverTool.name || "working");
|
|
8903
|
+
refreshStatus();
|
|
8904
|
+
} else if (serverBusy !== null && !serverTool && activeSteps.size) {
|
|
8905
|
+
activeSteps.clear();
|
|
8906
|
+
refreshStatus();
|
|
7840
8907
|
}
|
|
8908
|
+
busy = polledBusy;
|
|
7841
8909
|
tui.setWorking(busy);
|
|
7842
8910
|
if (!busy) {
|
|
7843
8911
|
if (activeSteps.size) activeSteps.clear();
|
|
7844
8912
|
liveOut = 0;
|
|
7845
8913
|
refreshStatus();
|
|
7846
|
-
if (queued.length > 0 && !editingQueued) await flushQueued();
|
|
7847
8914
|
}
|
|
7848
8915
|
refreshBgCount();
|
|
7849
8916
|
void relayApprovals().catch(() => {
|
|
@@ -7879,12 +8946,17 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
7879
8946
|
} catch {
|
|
7880
8947
|
}
|
|
7881
8948
|
};
|
|
7882
|
-
|
|
7883
|
-
})
|
|
8949
|
+
refreshSessionProjection = () => poll().catch(() => {
|
|
8950
|
+
});
|
|
8951
|
+
await refreshSessionProjection();
|
|
8952
|
+
const pollTimer = setInterval(() => {
|
|
8953
|
+
if (busy || approvalPromptOpen) void refreshSessionProjection();
|
|
8954
|
+
}, 1200);
|
|
7884
8955
|
await sessionEnded;
|
|
7885
8956
|
clearInterval(pollTimer);
|
|
7886
8957
|
clearInterval(heartbeatPoll);
|
|
7887
|
-
|
|
8958
|
+
process.off("SIGCONT", onTerminalResume);
|
|
8959
|
+
const stopped = bridge?.isOwner ?? false ? api.requestSharedStop(threadId, messagingOrigin).catch(() => {
|
|
7888
8960
|
}) : Promise.resolve();
|
|
7889
8961
|
const procsStopped = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
|
|
7890
8962
|
bridge?.close();
|
|
@@ -8084,12 +9156,12 @@ async function manageMachineProjects(tui, api, self, machine, dispatch, applyNot
|
|
|
8084
9156
|
);
|
|
8085
9157
|
if (!picked) return;
|
|
8086
9158
|
if (picked === ADD) {
|
|
8087
|
-
const
|
|
9159
|
+
const path14 = await tui.prompt(
|
|
8088
9160
|
`Absolute project path on ${machine.name}`,
|
|
8089
9161
|
machine.id === self.machine_id ? process.cwd() : "/home/you/project"
|
|
8090
9162
|
);
|
|
8091
|
-
if (!
|
|
8092
|
-
const trimmed =
|
|
9163
|
+
if (!path14 || !path14.trim()) return;
|
|
9164
|
+
const trimmed = path14.trim();
|
|
8093
9165
|
if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
|
|
8094
9166
|
tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
|
|
8095
9167
|
return;
|