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