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