@standardagents/code 0.10.1 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1238 -628
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -356,9 +356,13 @@ var ApiClient = class {
|
|
|
356
356
|
).catch(() => [])
|
|
357
357
|
)
|
|
358
358
|
]);
|
|
359
|
-
const arr = pages.flatMap((res) =>
|
|
359
|
+
const arr = pages.flatMap((res, i) => {
|
|
360
|
+
const list = Array.isArray(res) ? res : res.threads || [];
|
|
361
|
+
return list.map((t) => ({ ...t, agent_id: t.agent_id ?? ids[i] }));
|
|
362
|
+
});
|
|
360
363
|
return arr.map((t) => ({
|
|
361
364
|
id: t.id,
|
|
365
|
+
agent_id: t.agent_id,
|
|
362
366
|
tags: Array.isArray(t.tags) ? t.tags : [],
|
|
363
367
|
created_at: t.created_at,
|
|
364
368
|
title: t.title,
|
|
@@ -447,14 +451,6 @@ var ApiClient = class {
|
|
|
447
451
|
})
|
|
448
452
|
);
|
|
449
453
|
}
|
|
450
|
-
async steerInput(threadId, mutation) {
|
|
451
|
-
return parseSharedMessagingSnapshot(
|
|
452
|
-
await this.json(this.messagingPath(threadId, "/steer"), {
|
|
453
|
-
method: "POST",
|
|
454
|
-
body: JSON.stringify(mutation)
|
|
455
|
-
})
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
454
|
async updatePendingInput(threadId, pendingId, mutation) {
|
|
459
455
|
return parseSharedMessagingSnapshot(
|
|
460
456
|
await this.json(this.messagingPath(threadId, `/pending/${encodeURIComponent(pendingId)}`), {
|
|
@@ -495,13 +491,23 @@ var ApiClient = class {
|
|
|
495
491
|
})
|
|
496
492
|
);
|
|
497
493
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
494
|
+
/**
|
|
495
|
+
* Immediately halt a thread: the instance aborts the in-flight LLM
|
|
496
|
+
* request(s), marks dangling tool calls failed, and holds new turns until
|
|
497
|
+
* the next user message. This is Escape's HARD stop — not the old
|
|
498
|
+
* cooperative "stop at the next safe boundary" intent, which deferred the
|
|
499
|
+
* halt until the current step finished.
|
|
500
|
+
*/
|
|
501
|
+
async stopThread(threadId) {
|
|
502
|
+
await this.json(`/api/threads/${threadId}/stop`, { method: "POST" });
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Resume a stopped thread's execution loop. Used after a hard stop when a
|
|
506
|
+
* queued (steered) message should play immediately instead of waiting for
|
|
507
|
+
* the user's next direct send.
|
|
508
|
+
*/
|
|
509
|
+
async continueThread(threadId) {
|
|
510
|
+
await this.json(`/api/threads/${threadId}/continue`, { method: "POST" });
|
|
505
511
|
}
|
|
506
512
|
async getMessages(threadId, limit = 50, order) {
|
|
507
513
|
const orderParam = order ? `&order=${order}` : "";
|
|
@@ -1026,7 +1032,7 @@ function truncateMiddle(text, maxColumns) {
|
|
|
1026
1032
|
const headBudget = Math.ceil(available / 2);
|
|
1027
1033
|
const tailBudget = Math.floor(available / 2);
|
|
1028
1034
|
const head = [];
|
|
1029
|
-
const
|
|
1035
|
+
const tail2 = [];
|
|
1030
1036
|
let headWidth = 0;
|
|
1031
1037
|
let tailWidth = 0;
|
|
1032
1038
|
for (let i = 0; i < chars.length; i++) {
|
|
@@ -1038,12 +1044,12 @@ function truncateMiddle(text, maxColumns) {
|
|
|
1038
1044
|
for (let i = chars.length - 1; i >= head.length; i--) {
|
|
1039
1045
|
const width = terminalGlyphWidth(chars[i]);
|
|
1040
1046
|
if (tailWidth + width > tailBudget) break;
|
|
1041
|
-
|
|
1047
|
+
tail2.unshift(chars[i]);
|
|
1042
1048
|
tailWidth += width;
|
|
1043
1049
|
}
|
|
1044
1050
|
const isSeparator = (ch) => ch === "/" || ch === "\\";
|
|
1045
1051
|
let headEnd = head.length;
|
|
1046
|
-
let tailStart = chars.length -
|
|
1052
|
+
let tailStart = chars.length - tail2.length;
|
|
1047
1053
|
for (let i = headEnd - 1; i >= 0; i--) {
|
|
1048
1054
|
if (isSeparator(chars[i])) {
|
|
1049
1055
|
headEnd = i + 1;
|
|
@@ -1059,7 +1065,7 @@ function truncateMiddle(text, maxColumns) {
|
|
|
1059
1065
|
if (headEnd < tailStart && headEnd > 0 && tailStart < chars.length) {
|
|
1060
1066
|
return `${chars.slice(0, headEnd).join("")}\u2026${chars.slice(tailStart).join("")}`;
|
|
1061
1067
|
}
|
|
1062
|
-
return `${head.join("")}\u2026${
|
|
1068
|
+
return `${head.join("")}\u2026${tail2.join("")}`;
|
|
1063
1069
|
}
|
|
1064
1070
|
function clamp(s, max) {
|
|
1065
1071
|
return s.length > max ? s.slice(0, Math.max(0, max - 1)) + "\u2026" : s;
|
|
@@ -2164,14 +2170,14 @@ ${truncated}`
|
|
|
2164
2170
|
}
|
|
2165
2171
|
if (!pid) return { ok: false, error: "Process failed to start (no pid)." };
|
|
2166
2172
|
if (earlyExit !== void 0 || !isAlive2(pid)) {
|
|
2167
|
-
const
|
|
2173
|
+
const tail2 = await readLogTail(logPath, 15);
|
|
2168
2174
|
const code = earlyExit ?? "unknown";
|
|
2169
2175
|
return {
|
|
2170
2176
|
ok: false,
|
|
2171
|
-
error: `The process exited immediately (exit code ${code}) \u2014 it did not stay running, so nothing was started or tracked.` + (
|
|
2177
|
+
error: `The process exited immediately (exit code ${code}) \u2014 it did not stay running, so nothing was started or tracked.` + (tail2 ? `
|
|
2172
2178
|
|
|
2173
2179
|
Output:
|
|
2174
|
-
${
|
|
2180
|
+
${tail2}` : " No output was captured.")
|
|
2175
2181
|
};
|
|
2176
2182
|
}
|
|
2177
2183
|
child.removeListener("exit", onEarlyExit);
|
|
@@ -2288,8 +2294,8 @@ ${tail}` : " No output was captured.")
|
|
|
2288
2294
|
} catch {
|
|
2289
2295
|
return { ok: true, result: `(no output captured yet for ${id})` };
|
|
2290
2296
|
}
|
|
2291
|
-
const
|
|
2292
|
-
return { ok: true, result:
|
|
2297
|
+
const tail2 = content.split("\n").slice(-maxLines).join("\n");
|
|
2298
|
+
return { ok: true, result: tail2 || `(no output yet for ${id})` };
|
|
2293
2299
|
}
|
|
2294
2300
|
if (action === "stop") {
|
|
2295
2301
|
if (proc.status !== "running") {
|
|
@@ -2673,7 +2679,7 @@ var McpManager = class {
|
|
|
2673
2679
|
}
|
|
2674
2680
|
}
|
|
2675
2681
|
closeAll() {
|
|
2676
|
-
for (const [,
|
|
2682
|
+
for (const [, c5] of this.clients) c5.close();
|
|
2677
2683
|
this.clients.clear();
|
|
2678
2684
|
}
|
|
2679
2685
|
get(name) {
|
|
@@ -2684,13 +2690,13 @@ var McpManager = class {
|
|
|
2684
2690
|
}
|
|
2685
2691
|
toolCount() {
|
|
2686
2692
|
let n = 0;
|
|
2687
|
-
for (const [,
|
|
2693
|
+
for (const [, c5] of this.clients) n += c5.tools.length;
|
|
2688
2694
|
return n;
|
|
2689
2695
|
}
|
|
2690
2696
|
/** A JSON-serializable catalog of every connected server for the KV/context. */
|
|
2691
2697
|
catalog() {
|
|
2692
2698
|
return {
|
|
2693
|
-
servers: Array.from(this.clients.values()).map((
|
|
2699
|
+
servers: Array.from(this.clients.values()).map((c5) => c5.catalogEntry()),
|
|
2694
2700
|
generatedAt: Date.now()
|
|
2695
2701
|
};
|
|
2696
2702
|
}
|
|
@@ -2780,9 +2786,9 @@ function flattenContent(content, structured) {
|
|
|
2780
2786
|
}
|
|
2781
2787
|
function flattenResourceContents(contents) {
|
|
2782
2788
|
const parts = [];
|
|
2783
|
-
for (const
|
|
2784
|
-
if (typeof
|
|
2785
|
-
else if (typeof
|
|
2789
|
+
for (const c5 of contents || []) {
|
|
2790
|
+
if (typeof c5.text === "string") parts.push(c5.text);
|
|
2791
|
+
else if (typeof c5.blob === "string") parts.push(`[binary resource ${String(c5.uri ?? "")} (${c5.blob.length} b64 chars)]`);
|
|
2786
2792
|
}
|
|
2787
2793
|
return parts.join("\n").trim();
|
|
2788
2794
|
}
|
|
@@ -3593,8 +3599,8 @@ function parseStreamingMarkdown(source) {
|
|
|
3593
3599
|
let codeEnd = closing?.start ?? source.length;
|
|
3594
3600
|
if (!closing) {
|
|
3595
3601
|
const tailStart = Math.max(codeStart, source.lastIndexOf("\n") + 1);
|
|
3596
|
-
const
|
|
3597
|
-
const pending = /^ {0,3}(`+|~+)[ \t]*$/.exec(
|
|
3602
|
+
const tail2 = source.slice(tailStart);
|
|
3603
|
+
const pending = /^ {0,3}(`+|~+)[ \t]*$/.exec(tail2);
|
|
3598
3604
|
if (pending && pending[1][0] === opening.marker && pending[1].length < opening.length) codeEnd = tailStart;
|
|
3599
3605
|
}
|
|
3600
3606
|
if (codeEnd > codeStart && source[codeEnd - 1] === "\n") codeEnd--;
|
|
@@ -3813,17 +3819,17 @@ function wrapBlock(out, cols2, leadFirst, leadRest, leadWidth, text) {
|
|
|
3813
3819
|
function renderTable(rows) {
|
|
3814
3820
|
const cols2 = Math.max(...rows.map((r) => r.length));
|
|
3815
3821
|
const widths = [];
|
|
3816
|
-
for (let
|
|
3817
|
-
widths[
|
|
3822
|
+
for (let c5 = 0; c5 < cols2; c5++) {
|
|
3823
|
+
widths[c5] = Math.max(...rows.map((r) => visibleWidth(inline(r[c5] ?? ""))));
|
|
3818
3824
|
}
|
|
3819
3825
|
const sep = `${GRAY} \u2502 ${R}`;
|
|
3820
3826
|
const out = [];
|
|
3821
3827
|
rows.forEach((r, ri) => {
|
|
3822
3828
|
const cells = [];
|
|
3823
|
-
for (let
|
|
3824
|
-
const raw = r[
|
|
3829
|
+
for (let c5 = 0; c5 < cols2; c5++) {
|
|
3830
|
+
const raw = r[c5] ?? "";
|
|
3825
3831
|
const styled = ri === 0 ? `${BOLD}${inline(raw)}${R}` : inline(raw);
|
|
3826
|
-
cells.push(padEndVisible(styled, widths[
|
|
3832
|
+
cells.push(padEndVisible(styled, widths[c5]));
|
|
3827
3833
|
}
|
|
3828
3834
|
out.push((" " + cells.join(sep)).replace(/\s+$/, ""));
|
|
3829
3835
|
if (ri === 0) {
|
|
@@ -4260,7 +4266,11 @@ function buildInputBoxRows(opts) {
|
|
|
4260
4266
|
if (phase == null || !Number.isFinite(phase)) {
|
|
4261
4267
|
const dots2 = borderLevelDots(level);
|
|
4262
4268
|
const dotsStyled2 = borderLevelDotsStyled(level, levelColor);
|
|
4263
|
-
const
|
|
4269
|
+
const agent2 = (opts.agentLabel ?? "").trim();
|
|
4270
|
+
const withAgent2 = agent2.length > 0 && ` ${agent2} ${dots2} `.length <= geo.boxW - 3;
|
|
4271
|
+
const levelText = withAgent2 ? `${agent2} ${dots2}` : dots2;
|
|
4272
|
+
const levelStyled = withAgent2 ? `\x1B[2m${themeGray}${agent2}\x1B[0m ${dotsStyled2}` : dotsStyled2;
|
|
4273
|
+
const top2 = buildBoxTop(geo.boxW, levelText, borderColor, levelColor, "\x1B[0m", levelStyled);
|
|
4264
4274
|
const bottom2 = buildBoxBottom(geo.boxW, borderColor);
|
|
4265
4275
|
return [pad + top2, ...body.map((b) => pad + buildBoxBody(b, geo.contentW, borderColor)), pad + bottom2];
|
|
4266
4276
|
}
|
|
@@ -4271,7 +4281,11 @@ function buildInputBoxRows(opts) {
|
|
|
4271
4281
|
const colorAt = (i) => rotatingBorderCellColor(i, P, phase);
|
|
4272
4282
|
const dots = borderLevelDots(level);
|
|
4273
4283
|
borderLevelDotsStyled(level, levelColor);
|
|
4274
|
-
const
|
|
4284
|
+
const agent = (opts.agentLabel ?? "").trim();
|
|
4285
|
+
const withAgent = agent.length > 0 && ` ${agent} ${dots} `.length <= W - 3;
|
|
4286
|
+
const labelPlain = withAgent ? ` ${agent} ${dots} ` : ` ${dots} `;
|
|
4287
|
+
const agentSpanStart = withAgent ? 1 : -1;
|
|
4288
|
+
const agentSpanEnd = withAgent ? 1 + agent.length : -1;
|
|
4275
4289
|
const rightFillN = 1;
|
|
4276
4290
|
const budget = Math.max(1, W - 2 - rightFillN);
|
|
4277
4291
|
const plain = labelPlain.length > budget ? labelPlain.slice(0, Math.max(1, budget)) : labelPlain;
|
|
@@ -4285,15 +4299,20 @@ function buildInputBoxRows(opts) {
|
|
|
4285
4299
|
{
|
|
4286
4300
|
const plainChars = [...plain];
|
|
4287
4301
|
let di = 0;
|
|
4288
|
-
for (
|
|
4289
|
-
const
|
|
4302
|
+
for (let ci = 0; ci < plainChars.length; ci++) {
|
|
4303
|
+
const ch = plainChars[ci];
|
|
4304
|
+
const c5 = colorAt(perimeterIndex("top", xi++, W, bodyH));
|
|
4305
|
+
if (ci >= agentSpanStart && ci < agentSpanEnd) {
|
|
4306
|
+
top += `\x1B[2m${themeGray}${ch}\x1B[0m`;
|
|
4307
|
+
continue;
|
|
4308
|
+
}
|
|
4290
4309
|
if (ch === "\u25CF" || ch === "\u25CB") {
|
|
4291
4310
|
const levelN = Math.max(1, Math.min(5, level));
|
|
4292
4311
|
const filled = di < levelN;
|
|
4293
4312
|
di++;
|
|
4294
|
-
top += (filled ? levelColor ||
|
|
4313
|
+
top += (filled ? levelColor || c5 : "\x1B[38;5;240m") + (filled ? "\u25CF" : "\u25CB") + reset;
|
|
4295
4314
|
} else {
|
|
4296
|
-
top +=
|
|
4315
|
+
top += c5 + ch + reset;
|
|
4297
4316
|
}
|
|
4298
4317
|
}
|
|
4299
4318
|
}
|
|
@@ -4314,10 +4333,10 @@ function buildInputBoxRows(opts) {
|
|
|
4314
4333
|
let bottom = "";
|
|
4315
4334
|
for (let x = 0; x < W; x++) {
|
|
4316
4335
|
const idx = perimeterIndex("bottom", W - 1 - x, W, bodyH);
|
|
4317
|
-
const
|
|
4318
|
-
if (x === 0) bottom +=
|
|
4319
|
-
else if (x === W - 1) bottom +=
|
|
4320
|
-
else bottom +=
|
|
4336
|
+
const c5 = colorAt(idx);
|
|
4337
|
+
if (x === 0) bottom += c5 + "\u2570" + reset;
|
|
4338
|
+
else if (x === W - 1) bottom += c5 + "\u256F" + reset;
|
|
4339
|
+
else bottom += c5 + "\u2500" + reset;
|
|
4321
4340
|
}
|
|
4322
4341
|
return [pad + top, ...bodyRows, pad + bottom];
|
|
4323
4342
|
}
|
|
@@ -4334,6 +4353,36 @@ var C = {
|
|
|
4334
4353
|
gray: themeGray,
|
|
4335
4354
|
teal: "\x1B[38;5;37m"
|
|
4336
4355
|
};
|
|
4356
|
+
function renderSelectItemRows(it, sel, contentW, busyGlyph) {
|
|
4357
|
+
const hint = it.hint ?? "";
|
|
4358
|
+
const hintW = boxVisibleWidth(hint);
|
|
4359
|
+
const pointerW = 2;
|
|
4360
|
+
const labelMax = Math.max(4, contentW - pointerW - (hintW ? hintW + 2 : 0));
|
|
4361
|
+
let label = it.label.replace(/\s+/g, " ").trim();
|
|
4362
|
+
if (boxVisibleWidth(label) > labelMax) {
|
|
4363
|
+
while (label.length > 0 && boxVisibleWidth(label) > labelMax - 1) label = label.slice(0, -1);
|
|
4364
|
+
label += "\u2026";
|
|
4365
|
+
}
|
|
4366
|
+
const pointer = busyGlyph ? `${busyGlyph} ` : sel ? `${C.magenta}\u276F${C.reset} ` : " ";
|
|
4367
|
+
const styledLabel = it.disabled ? `${C.gray}${label}${C.reset}` : sel ? `${C.bold}${C.cyan}${label}${C.reset}` : `${C.dim}${label}${C.reset}`;
|
|
4368
|
+
let content = `${pointer}${styledLabel}`;
|
|
4369
|
+
if (hintW) {
|
|
4370
|
+
const used = pointerW + boxVisibleWidth(label);
|
|
4371
|
+
const gap = Math.max(1, contentW - used - hintW);
|
|
4372
|
+
content += `${" ".repeat(gap)}${sel ? C.gray : C.dim}${hint}${C.reset}`;
|
|
4373
|
+
}
|
|
4374
|
+
const rows = [content];
|
|
4375
|
+
if (it.detail) {
|
|
4376
|
+
let detail = it.detail.replace(/\s+/g, " ").trim();
|
|
4377
|
+
const detailMax = Math.max(4, contentW - pointerW - 1);
|
|
4378
|
+
if (boxVisibleWidth(detail) > detailMax) {
|
|
4379
|
+
while (detail.length > 0 && boxVisibleWidth(detail) > detailMax - 1) detail = detail.slice(0, -1);
|
|
4380
|
+
detail += "\u2026";
|
|
4381
|
+
}
|
|
4382
|
+
rows.push(`${" ".repeat(pointerW)}${sel ? `${C.dim}${C.cyan}` : `${C.dim}${C.gray}`}${detail}${C.reset}`);
|
|
4383
|
+
}
|
|
4384
|
+
return rows;
|
|
4385
|
+
}
|
|
4337
4386
|
var SYNC_OUTPUT_BEGIN = "\x1B[?2026h";
|
|
4338
4387
|
var SYNC_OUTPUT_END = "\x1B[?2026l";
|
|
4339
4388
|
var CURSOR_HIDE = "\x1B[?25l";
|
|
@@ -4486,7 +4535,7 @@ var Tui = class _Tui {
|
|
|
4486
4535
|
// event hooks (wired by index.ts)
|
|
4487
4536
|
onSubmit = () => {
|
|
4488
4537
|
};
|
|
4489
|
-
/**
|
|
4538
|
+
/** Option/Alt+Return sends a steering input through the portable messaging endpoint. */
|
|
4490
4539
|
onSteer = () => {
|
|
4491
4540
|
};
|
|
4492
4541
|
onInterrupt = () => {
|
|
@@ -4584,7 +4633,7 @@ var Tui = class _Tui {
|
|
|
4584
4633
|
dispatch(str, key) {
|
|
4585
4634
|
const seq = key && key.sequence || str || "";
|
|
4586
4635
|
if (seq === "\n" || seq === "\x1B[13;2u" || seq === "\x1B[27;2;13~") {
|
|
4587
|
-
if (!this.takeoverHandler && !this.pasting && !this.paletteOpen()) this.
|
|
4636
|
+
if (!this.takeoverHandler && !this.pasting && !this.paletteOpen()) this.insertAtCursor("\n");
|
|
4588
4637
|
return;
|
|
4589
4638
|
}
|
|
4590
4639
|
if (key && key.ctrl && key.name === "c") {
|
|
@@ -4717,11 +4766,11 @@ var Tui = class _Tui {
|
|
|
4717
4766
|
}
|
|
4718
4767
|
if (key.name === "return" || key.name === "enter") {
|
|
4719
4768
|
if (key.meta) {
|
|
4720
|
-
this.
|
|
4769
|
+
this.submitInput(true);
|
|
4721
4770
|
return;
|
|
4722
4771
|
}
|
|
4723
4772
|
if (key.shift) {
|
|
4724
|
-
this.
|
|
4773
|
+
this.insertAtCursor("\n");
|
|
4725
4774
|
return;
|
|
4726
4775
|
}
|
|
4727
4776
|
this.submitInput(false);
|
|
@@ -5022,7 +5071,7 @@ var Tui = class _Tui {
|
|
|
5022
5071
|
const q = this.inputBuffer.slice(1).trim().toLowerCase();
|
|
5023
5072
|
if (q === "") return this.commands;
|
|
5024
5073
|
return this.commands.filter(
|
|
5025
|
-
(
|
|
5074
|
+
(c5) => c5.name.startsWith(q) || c5.name.includes(q) || c5.label.toLowerCase().includes(q)
|
|
5026
5075
|
);
|
|
5027
5076
|
}
|
|
5028
5077
|
runCommand(cmd) {
|
|
@@ -5276,7 +5325,8 @@ var Tui = class _Tui {
|
|
|
5276
5325
|
level: this.level,
|
|
5277
5326
|
levelColor: this.levelColor(),
|
|
5278
5327
|
borderColor: inputBoxBorderColor(),
|
|
5279
|
-
borderPhase
|
|
5328
|
+
borderPhase,
|
|
5329
|
+
agentLabel: this.agentLabel
|
|
5280
5330
|
});
|
|
5281
5331
|
const boxTop = boxRows[0];
|
|
5282
5332
|
const boxBottom = boxRows[boxRows.length - 1];
|
|
@@ -5550,6 +5600,14 @@ var Tui = class _Tui {
|
|
|
5550
5600
|
});
|
|
5551
5601
|
this.print("");
|
|
5552
5602
|
}
|
|
5603
|
+
// ─── agent label (under the input box) ─────────────────────────────────────
|
|
5604
|
+
/** The loaded agent's display title, shown low-contrast under the input. */
|
|
5605
|
+
agentLabel = "";
|
|
5606
|
+
setAgentLabel(label) {
|
|
5607
|
+
if (label === this.agentLabel) return;
|
|
5608
|
+
this.agentLabel = label;
|
|
5609
|
+
this.renderBottom();
|
|
5610
|
+
}
|
|
5553
5611
|
// ─── working indicator (turn state) ───────────────────────────────────────
|
|
5554
5612
|
setWorking(on) {
|
|
5555
5613
|
if (on === this.working) return;
|
|
@@ -5930,8 +5988,13 @@ var Tui = class _Tui {
|
|
|
5930
5988
|
* Arrow-key selection menu (slash menu, process menu, resume). Pauses input.
|
|
5931
5989
|
* Boxed chrome matches the bottom input HUD: side margin + grey rounded
|
|
5932
5990
|
* border, selected row with a brand-tinted pointer.
|
|
5991
|
+
*
|
|
5992
|
+
* An item may carry a `detail` — a dim second physical line under its label
|
|
5993
|
+
* for context that doesn't fit the one-row label+hint layout (e.g. a
|
|
5994
|
+
* machine's hostname · daemon status). `opts.spaced` inserts a blank row
|
|
5995
|
+
* between items for readability in short, dense menus.
|
|
5933
5996
|
*/
|
|
5934
|
-
select(title, items) {
|
|
5997
|
+
select(title, items, opts) {
|
|
5935
5998
|
return new Promise((resolve) => {
|
|
5936
5999
|
let idx = Math.max(0, items.findIndex((it) => !it.disabled));
|
|
5937
6000
|
this.beginTakeover();
|
|
@@ -5944,41 +6007,28 @@ var Tui = class _Tui {
|
|
|
5944
6007
|
${pad}${title}
|
|
5945
6008
|
`);
|
|
5946
6009
|
else process.stdout.write("\n");
|
|
5947
|
-
const
|
|
5948
|
-
const it = items[i];
|
|
5949
|
-
const sel = i === idx;
|
|
5950
|
-
const hint = it.hint ?? "";
|
|
5951
|
-
const hintW = hint.length;
|
|
5952
|
-
const pointerW = 2;
|
|
5953
|
-
const labelMax = Math.max(4, geo.contentW - pointerW - (hintW ? hintW + 2 : 0));
|
|
5954
|
-
let label = it.label.replace(/\s+/g, " ").trim();
|
|
5955
|
-
if (label.length > labelMax) label = label.slice(0, Math.max(0, labelMax - 1)) + "\u2026";
|
|
5956
|
-
const pointer = sel ? `${C.magenta}\u276F${C.reset} ` : " ";
|
|
5957
|
-
const styledLabel = it.disabled ? `${C.gray}${label}${C.reset}` : sel ? `${C.bold}${C.cyan}${label}${C.reset}` : `${C.dim}${label}${C.reset}`;
|
|
5958
|
-
let content = `${pointer}${styledLabel}`;
|
|
5959
|
-
if (hintW) {
|
|
5960
|
-
const used = pointerW + label.length;
|
|
5961
|
-
const gap = Math.max(1, geo.contentW - used - hintW);
|
|
5962
|
-
content += `${" ".repeat(gap)}${sel ? C.gray : C.dim}${hint}${C.reset}`;
|
|
5963
|
-
}
|
|
5964
|
-
return content;
|
|
5965
|
-
};
|
|
6010
|
+
const renderItemRows = (i) => renderSelectItemRows(items[i], i === idx, geo.contentW);
|
|
5966
6011
|
const writeRow = (line) => {
|
|
5967
6012
|
const row = this.clampVisible(sanitizeHudRow(line), rowCap);
|
|
5968
6013
|
process.stdout.write(`\r\x1B[K${row}
|
|
5969
6014
|
`);
|
|
5970
6015
|
};
|
|
6016
|
+
const spaced = opts?.spaced ?? false;
|
|
6017
|
+
const contentRows = items.reduce((n, it) => n + 1 + (it.detail ? 1 : 0), 0) + (spaced ? Math.max(0, items.length - 1) : 0);
|
|
5971
6018
|
const draw = (moveUp) => {
|
|
5972
|
-
if (moveUp) process.stdout.write(`\x1B[${
|
|
6019
|
+
if (moveUp) process.stdout.write(`\x1B[${contentRows + 1}A`);
|
|
5973
6020
|
else writeRow(pad + buildBoxTopPlain(geo.boxW, border));
|
|
5974
6021
|
for (let i = 0; i < items.length; i++) {
|
|
5975
|
-
writeRow(pad + buildBoxBody(
|
|
6022
|
+
if (spaced && i > 0) writeRow(pad + buildBoxBody("", geo.contentW, border));
|
|
6023
|
+
for (const row of renderItemRows(i)) {
|
|
6024
|
+
writeRow(pad + buildBoxBody(row, geo.contentW, border));
|
|
6025
|
+
}
|
|
5976
6026
|
}
|
|
5977
6027
|
writeRow(pad + buildBoxBottom(geo.boxW, border));
|
|
5978
6028
|
};
|
|
5979
6029
|
draw(false);
|
|
5980
6030
|
const titleRows = title ? 2 : 1;
|
|
5981
|
-
const boxRows = 1 +
|
|
6031
|
+
const boxRows = 1 + contentRows + 1;
|
|
5982
6032
|
const erase = () => {
|
|
5983
6033
|
process.stdout.write("\r");
|
|
5984
6034
|
const up = titleRows + boxRows;
|
|
@@ -6006,6 +6056,182 @@ ${pad}${title}
|
|
|
6006
6056
|
};
|
|
6007
6057
|
});
|
|
6008
6058
|
}
|
|
6059
|
+
/**
|
|
6060
|
+
* A select menu that stays OPEN while its contents change — the primitive
|
|
6061
|
+
* behind in-place navigation UIs like the directory browser. Instead of
|
|
6062
|
+
* resolving once like `select`, it returns a handle:
|
|
6063
|
+
*
|
|
6064
|
+
* - `next()` awaits the next interaction: a pick, a registered extra key
|
|
6065
|
+
* (e.g. "." to toggle hidden files), or Escape (cancel).
|
|
6066
|
+
* - `update()` swaps title/items/footer and redraws the SAME screen region
|
|
6067
|
+
* (erase + repaint in one write), so navigating levels never feels like
|
|
6068
|
+
* leaving the menu.
|
|
6069
|
+
* - `setBusy(i)` animates a spinner in row i's pointer column while the
|
|
6070
|
+
* caller loads what's behind it; input (except Escape) is swallowed until
|
|
6071
|
+
* the next update()/setBusy(null).
|
|
6072
|
+
* - `close()` erases the region and releases the takeover.
|
|
6073
|
+
*
|
|
6074
|
+
* The optional `footer` is a dim line under the box — the home for key
|
|
6075
|
+
* hints and transient error text.
|
|
6076
|
+
*/
|
|
6077
|
+
openLiveSelect(opts) {
|
|
6078
|
+
let { title, items } = opts;
|
|
6079
|
+
let footer = opts.footer ?? "";
|
|
6080
|
+
const spaced = opts.spaced ?? false;
|
|
6081
|
+
const extraKeys = opts.keys ?? [];
|
|
6082
|
+
let idx = Math.max(0, items.findIndex((it) => !it.disabled));
|
|
6083
|
+
let busyIdx = null;
|
|
6084
|
+
let busyTimer = null;
|
|
6085
|
+
let closed = false;
|
|
6086
|
+
let edit = null;
|
|
6087
|
+
this.beginTakeover();
|
|
6088
|
+
const cols2 = process.stdout.columns || 80;
|
|
6089
|
+
const geo = inputBoxGeometry(cols2);
|
|
6090
|
+
const border = inputBoxBorderColor();
|
|
6091
|
+
const pad = " ".repeat(geo.margin);
|
|
6092
|
+
const rowCap = Math.max(1, cols2 - 1);
|
|
6093
|
+
let regionRows = 0;
|
|
6094
|
+
let lifted = 0;
|
|
6095
|
+
const draw = () => {
|
|
6096
|
+
if (closed) return;
|
|
6097
|
+
const rows = [""];
|
|
6098
|
+
let editCursor = null;
|
|
6099
|
+
if (title) rows.push(pad + title);
|
|
6100
|
+
rows.push(pad + buildBoxTopPlain(geo.boxW, border));
|
|
6101
|
+
for (let i = 0; i < items.length; i++) {
|
|
6102
|
+
const it = items[i];
|
|
6103
|
+
if (spaced && i > 0) rows.push(pad + buildBoxBody("", geo.contentW, border));
|
|
6104
|
+
const glyph = busyIdx === i ? this.spinnerFrame() : void 0;
|
|
6105
|
+
if (edit && edit.index === i) {
|
|
6106
|
+
const pointer = `${glyph ?? `${C.magenta}\u276F${C.reset}`} `;
|
|
6107
|
+
const max = Math.max(4, geo.contentW - 2 - boxVisibleWidth(edit.prefix) - 1);
|
|
6108
|
+
let buf = edit.buf;
|
|
6109
|
+
while (buf.length > 0 && boxVisibleWidth(buf) > max) buf = buf.slice(1);
|
|
6110
|
+
const body = buf ? `${C.bold}${C.cyan}${buf}${C.reset}` : `${C.dim}${edit.placeholder}${C.reset}`;
|
|
6111
|
+
rows.push(pad + buildBoxBody(`${pointer}${C.dim}${edit.prefix}${C.reset}${body}`, geo.contentW, border));
|
|
6112
|
+
if (!glyph) {
|
|
6113
|
+
editCursor = {
|
|
6114
|
+
rowIndex: rows.length - 1,
|
|
6115
|
+
// 1-based ANSI column: margin + "│ " + pointer(2) + prefix + typed text.
|
|
6116
|
+
col: geo.margin + 2 + 2 + boxVisibleWidth(edit.prefix) + boxVisibleWidth(buf) + 1
|
|
6117
|
+
};
|
|
6118
|
+
}
|
|
6119
|
+
continue;
|
|
6120
|
+
}
|
|
6121
|
+
for (const r of renderSelectItemRows(it, i === idx, geo.contentW, glyph)) {
|
|
6122
|
+
rows.push(pad + buildBoxBody(r, geo.contentW, border));
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
rows.push(pad + buildBoxBottom(geo.boxW, border));
|
|
6126
|
+
if (footer) rows.push(pad + footer);
|
|
6127
|
+
let out = lifted > 0 ? `\x1B[${lifted}B` : "";
|
|
6128
|
+
lifted = 0;
|
|
6129
|
+
out += regionRows > 0 ? `\r\x1B[${regionRows}A\x1B[J` : "";
|
|
6130
|
+
for (const line of rows) out += `\r\x1B[K${this.clampVisible(sanitizeHudRow(line), rowCap)}
|
|
6131
|
+
`;
|
|
6132
|
+
if (editCursor) {
|
|
6133
|
+
lifted = rows.length - editCursor.rowIndex;
|
|
6134
|
+
out += `\x1B[${lifted}A\x1B[${editCursor.col}G\x1B[?25h`;
|
|
6135
|
+
} else {
|
|
6136
|
+
out += "\x1B[?25l";
|
|
6137
|
+
}
|
|
6138
|
+
process.stdout.write(out);
|
|
6139
|
+
regionRows = rows.length;
|
|
6140
|
+
};
|
|
6141
|
+
draw();
|
|
6142
|
+
const queue = [];
|
|
6143
|
+
let waiter = null;
|
|
6144
|
+
const emit = (e) => {
|
|
6145
|
+
if (waiter) {
|
|
6146
|
+
const w = waiter;
|
|
6147
|
+
waiter = null;
|
|
6148
|
+
w(e);
|
|
6149
|
+
} else queue.push(e);
|
|
6150
|
+
};
|
|
6151
|
+
const stopSpinner = () => {
|
|
6152
|
+
if (busyTimer) clearInterval(busyTimer);
|
|
6153
|
+
busyTimer = null;
|
|
6154
|
+
busyIdx = null;
|
|
6155
|
+
};
|
|
6156
|
+
this.takeoverHandler = (str, key) => {
|
|
6157
|
+
if (edit && busyIdx === null) {
|
|
6158
|
+
if (key?.name === "escape") {
|
|
6159
|
+
edit = null;
|
|
6160
|
+
draw();
|
|
6161
|
+
} else if (key?.name === "return" || key?.name === "enter") {
|
|
6162
|
+
const text = edit.buf.trim();
|
|
6163
|
+
if (!text) {
|
|
6164
|
+
edit = null;
|
|
6165
|
+
draw();
|
|
6166
|
+
} else {
|
|
6167
|
+
emit({ kind: "edit", text, index: edit.index });
|
|
6168
|
+
}
|
|
6169
|
+
} else if (key?.name === "backspace") {
|
|
6170
|
+
edit.buf = edit.buf.slice(0, -1);
|
|
6171
|
+
draw();
|
|
6172
|
+
} else if (str && !key?.ctrl && !key?.meta) {
|
|
6173
|
+
const clean2 = str.replace(/[\x00-\x1f\x7f]/g, "");
|
|
6174
|
+
if (clean2) {
|
|
6175
|
+
edit.buf += clean2;
|
|
6176
|
+
draw();
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
return;
|
|
6180
|
+
}
|
|
6181
|
+
if (key?.name === "escape") return emit({ kind: "cancel" });
|
|
6182
|
+
if (busyIdx !== null) return;
|
|
6183
|
+
if (key?.name === "up" || str === "k") {
|
|
6184
|
+
idx = (idx - 1 + items.length) % items.length;
|
|
6185
|
+
draw();
|
|
6186
|
+
} else if (key?.name === "down" || str === "j") {
|
|
6187
|
+
idx = (idx + 1) % items.length;
|
|
6188
|
+
draw();
|
|
6189
|
+
} else if (key?.name === "return" || key?.name === "enter") {
|
|
6190
|
+
if (!items[idx]?.disabled) emit({ kind: "pick", value: items[idx].value, index: idx });
|
|
6191
|
+
} else if (str && extraKeys.includes(str)) {
|
|
6192
|
+
emit({ kind: "key", name: str, index: idx });
|
|
6193
|
+
}
|
|
6194
|
+
};
|
|
6195
|
+
return {
|
|
6196
|
+
next: () => queue.length > 0 ? Promise.resolve(queue.shift()) : new Promise((r) => waiter = r),
|
|
6197
|
+
update: (state) => {
|
|
6198
|
+
stopSpinner();
|
|
6199
|
+
edit = null;
|
|
6200
|
+
if (state.title !== void 0) title = state.title;
|
|
6201
|
+
if (state.items) {
|
|
6202
|
+
items = state.items;
|
|
6203
|
+
if (idx >= items.length || items[idx]?.disabled) {
|
|
6204
|
+
idx = Math.max(0, items.findIndex((it) => !it.disabled));
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
if (state.footer !== void 0) footer = state.footer;
|
|
6208
|
+
draw();
|
|
6209
|
+
},
|
|
6210
|
+
setBusy: (index) => {
|
|
6211
|
+
stopSpinner();
|
|
6212
|
+
if (index !== null) {
|
|
6213
|
+
busyIdx = index;
|
|
6214
|
+
busyTimer = setInterval(draw, SPINNER_FRAME_MS);
|
|
6215
|
+
}
|
|
6216
|
+
draw();
|
|
6217
|
+
},
|
|
6218
|
+
beginEdit: (index, editOpts) => {
|
|
6219
|
+
stopSpinner();
|
|
6220
|
+
edit = { index, buf: "", placeholder: editOpts?.placeholder ?? "", prefix: editOpts?.prefix ?? "" };
|
|
6221
|
+
idx = index;
|
|
6222
|
+
draw();
|
|
6223
|
+
},
|
|
6224
|
+
close: () => {
|
|
6225
|
+
if (closed) return;
|
|
6226
|
+
closed = true;
|
|
6227
|
+
stopSpinner();
|
|
6228
|
+
if (lifted > 0) process.stdout.write(`\x1B[${lifted}B`);
|
|
6229
|
+
lifted = 0;
|
|
6230
|
+
if (regionRows > 0) process.stdout.write(`\r\x1B[${regionRows}A\x1B[J`);
|
|
6231
|
+
this.endTakeover();
|
|
6232
|
+
}
|
|
6233
|
+
};
|
|
6234
|
+
}
|
|
6009
6235
|
/**
|
|
6010
6236
|
* Free-text prompt (single line). Pauses the main input and reads a line —
|
|
6011
6237
|
* used where a menu can't, e.g. entering an MCP server command. Enter submits,
|
|
@@ -6257,6 +6483,7 @@ var NAME_SUFFIX = ".name";
|
|
|
6257
6483
|
var ICON_SUFFIX = ".icon";
|
|
6258
6484
|
var FSREQ_SUFFIX = ".fsreq";
|
|
6259
6485
|
var FSRES_SUFFIX = ".fsres";
|
|
6486
|
+
var PROJNAMES_SUFFIX = ".projnames";
|
|
6260
6487
|
var DAEMON_ONLINE_WINDOW_MS = 90 * 1e3;
|
|
6261
6488
|
function machineKey(machineId) {
|
|
6262
6489
|
return `${KEY_PREFIX}${machineId}`;
|
|
@@ -6270,6 +6497,9 @@ function nameKey(machineId) {
|
|
|
6270
6497
|
function iconKey(machineId) {
|
|
6271
6498
|
return `${KEY_PREFIX}${machineId}${ICON_SUFFIX}`;
|
|
6272
6499
|
}
|
|
6500
|
+
function projectNamesKey(machineId) {
|
|
6501
|
+
return `${KEY_PREFIX}${machineId}${PROJNAMES_SUFFIX}`;
|
|
6502
|
+
}
|
|
6273
6503
|
function fsRequestKey(machineId) {
|
|
6274
6504
|
return `${KEY_PREFIX}${machineId}${FSREQ_SUFFIX}`;
|
|
6275
6505
|
}
|
|
@@ -6312,25 +6542,6 @@ async function readFsRequest(api, machineId) {
|
|
|
6312
6542
|
async function writeFsResponse(api, machineId, res) {
|
|
6313
6543
|
await api.userKvSet(fsResponseKey(machineId), { ...res, responded_at: Date.now() });
|
|
6314
6544
|
}
|
|
6315
|
-
async function resolveRemoteProjectPath(api, machineId, typedPath, timeoutMs = 12e3) {
|
|
6316
|
-
const trimmed = typedPath.trim();
|
|
6317
|
-
if (!trimmed.startsWith("~")) return trimmed;
|
|
6318
|
-
const nonce = crypto.randomBytes(8).toString("hex");
|
|
6319
|
-
try {
|
|
6320
|
-
await writeFsRequest(api, machineId, { nonce, op: "list", path: trimmed });
|
|
6321
|
-
const deadline = Date.now() + timeoutMs;
|
|
6322
|
-
while (Date.now() < deadline) {
|
|
6323
|
-
await new Promise((r) => setTimeout(r, 700));
|
|
6324
|
-
const res = await readFsResponse(api, machineId).catch(() => null);
|
|
6325
|
-
if (res && res.nonce === nonce) {
|
|
6326
|
-
const resolved = res.result && typeof res.result.path === "string" ? res.result.path : "";
|
|
6327
|
-
return resolved || trimmed;
|
|
6328
|
-
}
|
|
6329
|
-
}
|
|
6330
|
-
} catch {
|
|
6331
|
-
}
|
|
6332
|
-
return trimmed;
|
|
6333
|
-
}
|
|
6334
6545
|
function machineIcon(record2) {
|
|
6335
6546
|
if (record2.icon && record2.icon.trim()) return record2.icon.trim();
|
|
6336
6547
|
if (record2.platform === "darwin") return "\u{1F4BB}";
|
|
@@ -6360,7 +6571,10 @@ function parseMachineRecord(value) {
|
|
|
6360
6571
|
arch: typeof r.arch === "string" ? r.arch : "",
|
|
6361
6572
|
version: typeof r.version === "string" ? r.version : void 0,
|
|
6362
6573
|
daemon: r.daemon && typeof r.daemon === "object" && !Array.isArray(r.daemon) ? r.daemon : null,
|
|
6363
|
-
|
|
6574
|
+
// Clone rather than alias the caller's object: read-side overlays (e.g.
|
|
6575
|
+
// user-assigned project names) mutate the parsed record's entries and
|
|
6576
|
+
// must never write through into the source value.
|
|
6577
|
+
projects: r.projects && typeof r.projects === "object" && !Array.isArray(r.projects) ? structuredClone(r.projects) : {},
|
|
6364
6578
|
created_at: typeof r.created_at === "number" ? r.created_at : 0,
|
|
6365
6579
|
updated_at: typeof r.updated_at === "number" ? r.updated_at : 0
|
|
6366
6580
|
};
|
|
@@ -6373,6 +6587,7 @@ async function loadMachines(api) {
|
|
|
6373
6587
|
const records = [];
|
|
6374
6588
|
const names = /* @__PURE__ */ new Map();
|
|
6375
6589
|
const icons = /* @__PURE__ */ new Map();
|
|
6590
|
+
const projNames = /* @__PURE__ */ new Map();
|
|
6376
6591
|
for (const e of entries) {
|
|
6377
6592
|
const rest = e.key.slice(KEY_PREFIX.length);
|
|
6378
6593
|
if (rest.endsWith(CMD_SUFFIX) || rest.endsWith(FSREQ_SUFFIX) || rest.endsWith(FSRES_SUFFIX)) continue;
|
|
@@ -6386,6 +6601,12 @@ async function loadMachines(api) {
|
|
|
6386
6601
|
if (typeof e.value === "string" && e.value.trim()) icons.set(id, e.value.trim());
|
|
6387
6602
|
continue;
|
|
6388
6603
|
}
|
|
6604
|
+
if (rest.endsWith(PROJNAMES_SUFFIX)) {
|
|
6605
|
+
const id = rest.slice(0, -PROJNAMES_SUFFIX.length);
|
|
6606
|
+
const map = parseProjectNames(e.value);
|
|
6607
|
+
if (map) projNames.set(id, map);
|
|
6608
|
+
continue;
|
|
6609
|
+
}
|
|
6389
6610
|
const rec = parseMachineRecord(e.value);
|
|
6390
6611
|
if (rec) records.push(rec);
|
|
6391
6612
|
}
|
|
@@ -6394,9 +6615,58 @@ async function loadMachines(api) {
|
|
|
6394
6615
|
if (override) rec.name = override;
|
|
6395
6616
|
const icon = icons.get(rec.id);
|
|
6396
6617
|
if (icon) rec.icon = icon;
|
|
6618
|
+
overlayProjectNames(rec, projNames.get(rec.id));
|
|
6397
6619
|
}
|
|
6398
6620
|
return records;
|
|
6399
6621
|
}
|
|
6622
|
+
function parseProjectNames(value) {
|
|
6623
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
6624
|
+
const out = {};
|
|
6625
|
+
for (const [dir2, name] of Object.entries(value)) {
|
|
6626
|
+
if (typeof name === "string" && name.trim()) out[dir2] = name.trim();
|
|
6627
|
+
}
|
|
6628
|
+
return out;
|
|
6629
|
+
}
|
|
6630
|
+
function overlayProjectNames(record2, names) {
|
|
6631
|
+
if (!names) return;
|
|
6632
|
+
for (const [dir2, name] of Object.entries(names)) {
|
|
6633
|
+
const project = record2.projects[dir2];
|
|
6634
|
+
if (project) project.name = name;
|
|
6635
|
+
}
|
|
6636
|
+
}
|
|
6637
|
+
function projectDisplayName(dir2, project) {
|
|
6638
|
+
return project?.name?.trim() || path4.basename(dir2) || dir2;
|
|
6639
|
+
}
|
|
6640
|
+
function projectDisplayLabels(projects) {
|
|
6641
|
+
const names = /* @__PURE__ */ new Map();
|
|
6642
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6643
|
+
for (const [dir2, p] of Object.entries(projects)) {
|
|
6644
|
+
const n = projectDisplayName(dir2, p);
|
|
6645
|
+
names.set(dir2, n);
|
|
6646
|
+
counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
6647
|
+
}
|
|
6648
|
+
const out = /* @__PURE__ */ new Map();
|
|
6649
|
+
for (const [dir2, n] of names) {
|
|
6650
|
+
if ((counts.get(n) ?? 0) > 1) {
|
|
6651
|
+
const parts = dir2.split(/[\\/]/).filter(Boolean);
|
|
6652
|
+
const parent = parts.length > 1 ? parts[parts.length - 2] : "";
|
|
6653
|
+
out.set(dir2, parent ? `${parent}/${n}` : n);
|
|
6654
|
+
} else {
|
|
6655
|
+
out.set(dir2, n);
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
return out;
|
|
6659
|
+
}
|
|
6660
|
+
async function getProjectNames(api, machineId) {
|
|
6661
|
+
return parseProjectNames(await api.userKvGet(projectNamesKey(machineId))) ?? {};
|
|
6662
|
+
}
|
|
6663
|
+
async function setProjectName(api, machineId, dir2, name) {
|
|
6664
|
+
const map = await getProjectNames(api, machineId);
|
|
6665
|
+
const trimmed = name.trim();
|
|
6666
|
+
if (trimmed) map[dir2] = trimmed;
|
|
6667
|
+
else delete map[dir2];
|
|
6668
|
+
await api.userKvSet(projectNamesKey(machineId), Object.keys(map).length ? map : null);
|
|
6669
|
+
}
|
|
6400
6670
|
async function getMachineIcon(api, machineId) {
|
|
6401
6671
|
const v = await api.userKvGet(iconKey(machineId));
|
|
6402
6672
|
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
@@ -6408,9 +6678,14 @@ async function setMachineIcon(api, machineId, icon) {
|
|
|
6408
6678
|
async function loadMachine(api, machineId) {
|
|
6409
6679
|
const rec = await loadRawMachine(api, machineId);
|
|
6410
6680
|
if (!rec) return null;
|
|
6411
|
-
const [override, icon] = await Promise.all([
|
|
6681
|
+
const [override, icon, projNames] = await Promise.all([
|
|
6682
|
+
getMachineName(api, machineId),
|
|
6683
|
+
getMachineIcon(api, machineId),
|
|
6684
|
+
getProjectNames(api, machineId)
|
|
6685
|
+
]);
|
|
6412
6686
|
if (override) rec.name = override;
|
|
6413
6687
|
if (icon) rec.icon = icon;
|
|
6688
|
+
overlayProjectNames(rec, projNames);
|
|
6414
6689
|
return rec;
|
|
6415
6690
|
}
|
|
6416
6691
|
function daemonOnline(record2, now = Date.now()) {
|
|
@@ -6508,7 +6783,7 @@ async function clearDaemon(api, identity) {
|
|
|
6508
6783
|
function parseCommands(value) {
|
|
6509
6784
|
if (!Array.isArray(value)) return [];
|
|
6510
6785
|
return value.filter(
|
|
6511
|
-
(
|
|
6786
|
+
(c5) => !!c5 && typeof c5 === "object" && typeof c5.id === "string" && typeof c5.kind === "string"
|
|
6512
6787
|
);
|
|
6513
6788
|
}
|
|
6514
6789
|
async function enqueueMachineCommand(api, machineId, kind, args) {
|
|
@@ -6531,7 +6806,7 @@ async function clearMachineCommands(api, machineId, appliedIds) {
|
|
|
6531
6806
|
if (appliedIds.length === 0) return;
|
|
6532
6807
|
const key = commandKey(machineId);
|
|
6533
6808
|
const remaining = parseCommands(await api.userKvGet(key)).filter(
|
|
6534
|
-
(
|
|
6809
|
+
(c5) => !appliedIds.includes(c5.id)
|
|
6535
6810
|
);
|
|
6536
6811
|
await api.userKvSet(key, remaining.length ? remaining : null);
|
|
6537
6812
|
}
|
|
@@ -6555,74 +6830,358 @@ async function applyMachineCommand(api, identity, cmd) {
|
|
|
6555
6830
|
return `unknown command ${cmd.kind}`;
|
|
6556
6831
|
}
|
|
6557
6832
|
}
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
if (
|
|
6578
|
-
|
|
6579
|
-
if (typeof r.tool_call_id !== "string" || typeof r.choice !== "string") return null;
|
|
6580
|
-
if (!["allow", "deny", "always", "always_risk"].includes(r.choice)) return null;
|
|
6581
|
-
return {
|
|
6582
|
-
tool_call_id: r.tool_call_id,
|
|
6583
|
-
choice: r.choice,
|
|
6584
|
-
reason: typeof r.reason === "string" ? r.reason : void 0,
|
|
6585
|
-
decided_at: typeof r.decided_at === "number" ? r.decided_at : 0
|
|
6586
|
-
};
|
|
6833
|
+
var PROJECT_MARKERS = [
|
|
6834
|
+
".git",
|
|
6835
|
+
"package.json",
|
|
6836
|
+
"pyproject.toml",
|
|
6837
|
+
"Cargo.toml",
|
|
6838
|
+
"go.mod",
|
|
6839
|
+
"pom.xml",
|
|
6840
|
+
"build.gradle",
|
|
6841
|
+
"Gemfile",
|
|
6842
|
+
"composer.json",
|
|
6843
|
+
"requirements.txt",
|
|
6844
|
+
".standardagents"
|
|
6845
|
+
];
|
|
6846
|
+
var MAX_ENTRIES3 = 500;
|
|
6847
|
+
function resolveBrowsePath(input3) {
|
|
6848
|
+
const home = os8.homedir();
|
|
6849
|
+
let p = (input3 ?? "").trim();
|
|
6850
|
+
if (!p) return home;
|
|
6851
|
+
if (p === "~") return home;
|
|
6852
|
+
if (p.startsWith("~/")) p = path4.join(home, p.slice(2));
|
|
6853
|
+
return path4.resolve(p);
|
|
6587
6854
|
}
|
|
6588
|
-
|
|
6589
|
-
|
|
6855
|
+
function markers(dirPath) {
|
|
6856
|
+
let repo = false;
|
|
6857
|
+
let project = false;
|
|
6858
|
+
for (const marker of PROJECT_MARKERS) {
|
|
6859
|
+
let hit = false;
|
|
6860
|
+
try {
|
|
6861
|
+
hit = fs5.existsSync(path4.join(dirPath, marker));
|
|
6862
|
+
} catch {
|
|
6863
|
+
hit = false;
|
|
6864
|
+
}
|
|
6865
|
+
if (!hit) continue;
|
|
6866
|
+
project = true;
|
|
6867
|
+
if (marker === ".git") repo = true;
|
|
6868
|
+
if (repo) break;
|
|
6869
|
+
}
|
|
6870
|
+
return { project, repo };
|
|
6590
6871
|
}
|
|
6591
|
-
|
|
6592
|
-
const
|
|
6593
|
-
const
|
|
6594
|
-
|
|
6595
|
-
const
|
|
6872
|
+
function browseDirectory(input3, opts = {}) {
|
|
6873
|
+
const home = os8.homedir();
|
|
6874
|
+
const abs = resolveBrowsePath(input3);
|
|
6875
|
+
const parent = path4.dirname(abs);
|
|
6876
|
+
const base = {
|
|
6877
|
+
path: abs,
|
|
6878
|
+
parent: parent === abs ? null : parent,
|
|
6879
|
+
home
|
|
6880
|
+
};
|
|
6881
|
+
let dirents;
|
|
6596
6882
|
try {
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6883
|
+
const stat = fs5.statSync(abs);
|
|
6884
|
+
if (!stat.isDirectory()) {
|
|
6885
|
+
return { ...base, entries: [], truncated: false, error: "Not a directory" };
|
|
6886
|
+
}
|
|
6887
|
+
dirents = fs5.readdirSync(abs, { withFileTypes: true });
|
|
6888
|
+
} catch (err) {
|
|
6889
|
+
const code = err?.code;
|
|
6890
|
+
const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : code === "ENOENT" ? "Folder not found" : "Could not read this folder";
|
|
6891
|
+
return { ...base, entries: [], truncated: false, error: message };
|
|
6892
|
+
}
|
|
6893
|
+
const showHidden = !!opts.showHidden;
|
|
6894
|
+
const rawDirs = [];
|
|
6895
|
+
const rawFiles = [];
|
|
6896
|
+
for (const d of dirents) {
|
|
6897
|
+
const name = d.name;
|
|
6898
|
+
if (!showHidden && name.startsWith(".")) continue;
|
|
6899
|
+
let isDir = d.isDirectory();
|
|
6900
|
+
if (d.isSymbolicLink()) {
|
|
6901
|
+
try {
|
|
6902
|
+
isDir = fs5.statSync(path4.join(abs, name)).isDirectory();
|
|
6903
|
+
} catch {
|
|
6904
|
+
isDir = false;
|
|
6602
6905
|
}
|
|
6603
6906
|
}
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
}
|
|
6608
|
-
|
|
6609
|
-
}
|
|
6907
|
+
if (isDir) {
|
|
6908
|
+
const { project, repo } = markers(path4.join(abs, name));
|
|
6909
|
+
rawDirs.push({ name, dir: true, project: project || void 0, repo: repo || void 0 });
|
|
6910
|
+
} else {
|
|
6911
|
+
rawFiles.push({ name, dir: false });
|
|
6912
|
+
}
|
|
6610
6913
|
}
|
|
6914
|
+
const cmp = (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
6915
|
+
rawDirs.sort(cmp);
|
|
6916
|
+
rawFiles.sort(cmp);
|
|
6917
|
+
const all = [...rawDirs, ...rawFiles];
|
|
6918
|
+
const truncated = all.length > MAX_ENTRIES3;
|
|
6919
|
+
return { ...base, entries: truncated ? all.slice(0, MAX_ENTRIES3) : all, truncated };
|
|
6611
6920
|
}
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
this.hooks = hooks;
|
|
6619
|
-
this.clientName = clientName;
|
|
6921
|
+
function mkdirDirectory(parentInput, name, opts = {}) {
|
|
6922
|
+
const parent = resolveBrowsePath(parentInput);
|
|
6923
|
+
const clean2 = (name ?? "").trim();
|
|
6924
|
+
const listParent = () => browseDirectory(parent, opts);
|
|
6925
|
+
if (!clean2 || clean2 === "." || clean2 === ".." || clean2.includes("/") || clean2.includes("\\") || clean2.includes("\0")) {
|
|
6926
|
+
return { ...listParent(), error: "Invalid folder name." };
|
|
6620
6927
|
}
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6928
|
+
const target = path4.join(parent, clean2);
|
|
6929
|
+
if (path4.dirname(target) !== parent) {
|
|
6930
|
+
return { ...listParent(), error: "Invalid folder name." };
|
|
6931
|
+
}
|
|
6932
|
+
try {
|
|
6933
|
+
fs5.mkdirSync(target, { recursive: false });
|
|
6934
|
+
} catch (err) {
|
|
6935
|
+
const code = err?.code;
|
|
6936
|
+
const message = code === "EEXIST" ? "A folder with that name already exists." : code === "EACCES" || code === "EPERM" ? "Permission denied." : code === "ENOENT" ? "The parent folder no longer exists." : "Could not create the folder.";
|
|
6937
|
+
return { ...listParent(), error: message };
|
|
6938
|
+
}
|
|
6939
|
+
return listParent();
|
|
6940
|
+
}
|
|
6941
|
+
|
|
6942
|
+
// src/dir-picker.ts
|
|
6943
|
+
var c2 = {
|
|
6944
|
+
reset: "\x1B[0m",
|
|
6945
|
+
dim: "\x1B[2m",
|
|
6946
|
+
bold: "\x1B[1m",
|
|
6947
|
+
gray: themeGray,
|
|
6948
|
+
yellow: "\x1B[33m",
|
|
6949
|
+
red: "\x1B[31m",
|
|
6950
|
+
teal: "\x1B[38;5;37m"
|
|
6951
|
+
};
|
|
6952
|
+
function localBrowseBackend(label = "this machine") {
|
|
6953
|
+
return {
|
|
6954
|
+
label,
|
|
6955
|
+
list: async (p, showHidden) => browseDirectory(p, { showHidden }),
|
|
6956
|
+
mkdir: async (parent, name, showHidden) => mkdirDirectory(parent, name, { showHidden })
|
|
6957
|
+
};
|
|
6958
|
+
}
|
|
6959
|
+
function parseBrowseResult(value) {
|
|
6960
|
+
if (!value || typeof value !== "object") return null;
|
|
6961
|
+
const r = value;
|
|
6962
|
+
if (typeof r.path !== "string" || !Array.isArray(r.entries)) return null;
|
|
6963
|
+
return {
|
|
6964
|
+
path: r.path,
|
|
6965
|
+
parent: typeof r.parent === "string" ? r.parent : null,
|
|
6966
|
+
home: typeof r.home === "string" ? r.home : "",
|
|
6967
|
+
entries: r.entries.filter(
|
|
6968
|
+
(e) => !!e && typeof e.name === "string"
|
|
6969
|
+
),
|
|
6970
|
+
truncated: r.truncated === true,
|
|
6971
|
+
error: typeof r.error === "string" ? r.error : void 0
|
|
6972
|
+
};
|
|
6973
|
+
}
|
|
6974
|
+
function remoteBrowseBackend(api, machineId, label) {
|
|
6975
|
+
const rpc = async (req) => {
|
|
6976
|
+
const nonce = crypto.randomBytes(8).toString("hex");
|
|
6977
|
+
await writeFsRequest(api, machineId, { nonce, ...req });
|
|
6978
|
+
const deadline = Date.now() + 15e3;
|
|
6979
|
+
while (Date.now() < deadline) {
|
|
6980
|
+
await new Promise((r) => setTimeout(r, 700));
|
|
6981
|
+
const res = await readFsResponse(api, machineId).catch(() => null);
|
|
6982
|
+
if (res && res.nonce === nonce) {
|
|
6983
|
+
if (!res.ok) throw new Error(res.error || "Browse failed on the remote machine.");
|
|
6984
|
+
const parsed = parseBrowseResult(res.result);
|
|
6985
|
+
if (!parsed) throw new Error("The remote machine sent an unreadable listing.");
|
|
6986
|
+
return parsed;
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
throw new Error(`${label} didn't answer \u2014 is its daemon online?`);
|
|
6990
|
+
};
|
|
6991
|
+
return {
|
|
6992
|
+
label,
|
|
6993
|
+
list: (p, showHidden) => rpc({ op: "list", path: p ?? "~", show_hidden: showHidden }),
|
|
6994
|
+
mkdir: (parent, name, showHidden) => rpc({ op: "mkdir", path: parent, name, show_hidden: showHidden })
|
|
6995
|
+
};
|
|
6996
|
+
}
|
|
6997
|
+
function joinBrowsed(parent, name) {
|
|
6998
|
+
const sep = parent.includes("\\") && !parent.includes("/") ? "\\" : "/";
|
|
6999
|
+
return parent.endsWith(sep) ? parent + name : parent + sep + name;
|
|
7000
|
+
}
|
|
7001
|
+
function tail(p, max = 44) {
|
|
7002
|
+
return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
|
|
7003
|
+
}
|
|
7004
|
+
var KEY_HINTS = `enter open \xB7 s use this directory \xB7 . hidden files \xB7 esc back`;
|
|
7005
|
+
async function pickDirectory(tui, backend, opts = {}) {
|
|
7006
|
+
let showHidden = false;
|
|
7007
|
+
let page = 0;
|
|
7008
|
+
const spin = opts.loader?.(`Reading ${backend.label}`);
|
|
7009
|
+
let listing;
|
|
7010
|
+
try {
|
|
7011
|
+
listing = await backend.list(opts.startPath ?? null, showHidden);
|
|
7012
|
+
} catch (e) {
|
|
7013
|
+
spin?.stop();
|
|
7014
|
+
tui.print(`${c2.red}\u2717${c2.reset} ${c2.gray}${e instanceof Error ? e.message : String(e)}${c2.reset}`);
|
|
7015
|
+
return null;
|
|
7016
|
+
}
|
|
7017
|
+
spin?.stop();
|
|
7018
|
+
const title = () => `${c2.bold}${gradientText(`Browse ${backend.label}`)}${c2.reset} ${c2.teal}${tail(listing.path, 48)}${c2.reset}` + (showHidden ? ` ${c2.dim}\xB7 hidden shown${c2.reset}` : "");
|
|
7019
|
+
const footer = (error) => error ? `${c2.yellow}${error}${c2.reset} ${c2.dim}${KEY_HINTS}${c2.reset}` : `${c2.dim}${KEY_HINTS}${c2.reset}`;
|
|
7020
|
+
const buildItems = () => {
|
|
7021
|
+
const pageSize = Math.max(5, Math.min(14, (process.stdout.rows || 24) - 9));
|
|
7022
|
+
const pages = Math.max(1, Math.ceil(listing.entries.length / pageSize));
|
|
7023
|
+
if (page >= pages) page = 0;
|
|
7024
|
+
const slice = listing.entries.slice(page * pageSize, (page + 1) * pageSize);
|
|
7025
|
+
const items = [];
|
|
7026
|
+
if (listing.parent) items.push({ label: "..", hint: "up a level", value: { kind: "up", to: listing.parent } });
|
|
7027
|
+
for (const entry of slice) {
|
|
7028
|
+
if (entry.dir) {
|
|
7029
|
+
items.push({
|
|
7030
|
+
label: `${entry.name}/`,
|
|
7031
|
+
hint: entry.repo ? "git repo" : entry.project ? "project" : "",
|
|
7032
|
+
value: { kind: "open", to: joinBrowsed(listing.path, entry.name) }
|
|
7033
|
+
});
|
|
7034
|
+
} else {
|
|
7035
|
+
items.push({ label: entry.name, value: { kind: "noop" }, disabled: true });
|
|
7036
|
+
}
|
|
7037
|
+
}
|
|
7038
|
+
if (listing.entries.length === 0) {
|
|
7039
|
+
items.push({ label: "(empty directory)", value: { kind: "noop" }, disabled: true });
|
|
7040
|
+
}
|
|
7041
|
+
if (pages > 1) {
|
|
7042
|
+
const remaining = listing.entries.length - slice.length;
|
|
7043
|
+
items.push({
|
|
7044
|
+
label: `\u2192 more (page ${page + 1}/${pages}, ${remaining} more entr${remaining === 1 ? "y" : "ies"})`,
|
|
7045
|
+
value: { kind: "page" }
|
|
7046
|
+
});
|
|
7047
|
+
}
|
|
7048
|
+
if (listing.truncated) {
|
|
7049
|
+
items.push({ label: "\u2026listing capped \u2014 very large directory", value: { kind: "noop" }, disabled: true });
|
|
7050
|
+
}
|
|
7051
|
+
items.push({ label: "\uFF0B New folder here\u2026", value: { kind: "mkdir" } });
|
|
7052
|
+
return items;
|
|
7053
|
+
};
|
|
7054
|
+
const menu = tui.openLiveSelect({
|
|
7055
|
+
title: title(),
|
|
7056
|
+
items: buildItems(),
|
|
7057
|
+
footer: footer(listing.error),
|
|
7058
|
+
keys: ["s", "."]
|
|
7059
|
+
});
|
|
7060
|
+
const refresh = () => menu.update({ title: title(), items: buildItems(), footer: footer(listing.error) });
|
|
7061
|
+
const navigate = async (to, index) => {
|
|
7062
|
+
menu.setBusy(index);
|
|
7063
|
+
try {
|
|
7064
|
+
listing = await backend.list(to, showHidden);
|
|
7065
|
+
page = 0;
|
|
7066
|
+
refresh();
|
|
7067
|
+
} catch (e) {
|
|
7068
|
+
menu.update({ footer: footer(e instanceof Error ? e.message : String(e)) });
|
|
7069
|
+
}
|
|
7070
|
+
};
|
|
7071
|
+
for (; ; ) {
|
|
7072
|
+
const ev = await menu.next();
|
|
7073
|
+
if (ev.kind === "cancel") {
|
|
7074
|
+
menu.close();
|
|
7075
|
+
return null;
|
|
7076
|
+
}
|
|
7077
|
+
if (ev.kind === "key") {
|
|
7078
|
+
if (ev.name === "s") {
|
|
7079
|
+
menu.close();
|
|
7080
|
+
return listing.path;
|
|
7081
|
+
}
|
|
7082
|
+
if (ev.name === ".") {
|
|
7083
|
+
showHidden = !showHidden;
|
|
7084
|
+
await navigate(listing.path, ev.index);
|
|
7085
|
+
}
|
|
7086
|
+
continue;
|
|
7087
|
+
}
|
|
7088
|
+
if (ev.kind === "edit") {
|
|
7089
|
+
menu.setBusy(ev.index);
|
|
7090
|
+
try {
|
|
7091
|
+
const after = await backend.mkdir(listing.path, ev.text, showHidden);
|
|
7092
|
+
if (after.error) {
|
|
7093
|
+
listing = after;
|
|
7094
|
+
page = 0;
|
|
7095
|
+
menu.update({ title: title(), items: buildItems(), footer: footer(after.error) });
|
|
7096
|
+
} else {
|
|
7097
|
+
listing = await backend.list(joinBrowsed(listing.path, ev.text), showHidden);
|
|
7098
|
+
page = 0;
|
|
7099
|
+
refresh();
|
|
7100
|
+
}
|
|
7101
|
+
} catch (e) {
|
|
7102
|
+
menu.update({ footer: footer(e instanceof Error ? e.message : String(e)) });
|
|
7103
|
+
}
|
|
7104
|
+
continue;
|
|
7105
|
+
}
|
|
7106
|
+
const move = ev.value;
|
|
7107
|
+
if (move.kind === "up" || move.kind === "open") {
|
|
7108
|
+
await navigate(move.to, ev.index);
|
|
7109
|
+
} else if (move.kind === "page") {
|
|
7110
|
+
page += 1;
|
|
7111
|
+
refresh();
|
|
7112
|
+
} else if (move.kind === "mkdir") {
|
|
7113
|
+
menu.beginEdit(ev.index, { placeholder: "folder name\u2026", prefix: "\uFF0B " });
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
|
|
7118
|
+
// src/relay.ts
|
|
7119
|
+
var APPROVAL_REQUEST_KEY = "approval_request";
|
|
7120
|
+
var APPROVAL_RESPONSE_KEY = "approval_response";
|
|
7121
|
+
function parseApprovalRequest(value) {
|
|
7122
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
7123
|
+
const r = value;
|
|
7124
|
+
if (typeof r.tool_call_id !== "string" || typeof r.tool !== "string") return null;
|
|
7125
|
+
return {
|
|
7126
|
+
tool_call_id: r.tool_call_id,
|
|
7127
|
+
tool: r.tool,
|
|
7128
|
+
summary: typeof r.summary === "string" ? r.summary : r.tool,
|
|
7129
|
+
permission: typeof r.permission === "string" ? r.permission : null,
|
|
7130
|
+
risk: typeof r.risk === "number" ? r.risk : 3,
|
|
7131
|
+
machine: typeof r.machine === "string" ? r.machine : "",
|
|
7132
|
+
requested_at: typeof r.requested_at === "number" ? r.requested_at : 0
|
|
7133
|
+
};
|
|
7134
|
+
}
|
|
7135
|
+
function parseApprovalResponse(value) {
|
|
7136
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
7137
|
+
const r = value;
|
|
7138
|
+
if (typeof r.tool_call_id !== "string" || typeof r.choice !== "string") return null;
|
|
7139
|
+
if (!["allow", "deny", "always", "always_risk"].includes(r.choice)) return null;
|
|
7140
|
+
return {
|
|
7141
|
+
tool_call_id: r.tool_call_id,
|
|
7142
|
+
choice: r.choice,
|
|
7143
|
+
reason: typeof r.reason === "string" ? r.reason : void 0,
|
|
7144
|
+
decided_at: typeof r.decided_at === "number" ? r.decided_at : 0
|
|
7145
|
+
};
|
|
7146
|
+
}
|
|
7147
|
+
async function writeApprovalResponse(api, threadId, response) {
|
|
7148
|
+
await api.kvSet(threadId, APPROVAL_RESPONSE_KEY, response);
|
|
7149
|
+
}
|
|
7150
|
+
async function awaitApprovalViaRelay(api, threadId, request, options = {}) {
|
|
7151
|
+
const pollMs = options.pollMs ?? 2e3;
|
|
7152
|
+
const timeoutMs = options.timeoutMs ?? 24 * 60 * 60 * 1e3;
|
|
7153
|
+
await api.kvSet(threadId, APPROVAL_REQUEST_KEY, request);
|
|
7154
|
+
const deadline = Date.now() + timeoutMs;
|
|
7155
|
+
try {
|
|
7156
|
+
while (Date.now() < deadline) {
|
|
7157
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
7158
|
+
const response = parseApprovalResponse(await api.kvGet(threadId, APPROVAL_RESPONSE_KEY));
|
|
7159
|
+
if (response && response.tool_call_id === request.tool_call_id) {
|
|
7160
|
+
return response;
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7163
|
+
return null;
|
|
7164
|
+
} finally {
|
|
7165
|
+
await api.kvSet(threadId, APPROVAL_REQUEST_KEY, null).catch(() => {
|
|
7166
|
+
});
|
|
7167
|
+
await api.kvSet(threadId, APPROVAL_RESPONSE_KEY, null).catch(() => {
|
|
7168
|
+
});
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
|
|
7172
|
+
// src/hub.ts
|
|
7173
|
+
var HubSocket = class {
|
|
7174
|
+
constructor(api, clientId, hooks, clientName) {
|
|
7175
|
+
this.api = api;
|
|
7176
|
+
this.clientId = clientId;
|
|
7177
|
+
this.hooks = hooks;
|
|
7178
|
+
this.clientName = clientName;
|
|
7179
|
+
}
|
|
7180
|
+
api;
|
|
7181
|
+
clientId;
|
|
7182
|
+
hooks;
|
|
7183
|
+
clientName;
|
|
7184
|
+
ws = null;
|
|
6626
7185
|
closed = false;
|
|
6627
7186
|
heartbeat = null;
|
|
6628
7187
|
reconnectAttempt = 0;
|
|
@@ -6704,114 +7263,6 @@ var HubSocket = class {
|
|
|
6704
7263
|
this.ws?.close();
|
|
6705
7264
|
}
|
|
6706
7265
|
};
|
|
6707
|
-
var PROJECT_MARKERS = [
|
|
6708
|
-
".git",
|
|
6709
|
-
"package.json",
|
|
6710
|
-
"pyproject.toml",
|
|
6711
|
-
"Cargo.toml",
|
|
6712
|
-
"go.mod",
|
|
6713
|
-
"pom.xml",
|
|
6714
|
-
"build.gradle",
|
|
6715
|
-
"Gemfile",
|
|
6716
|
-
"composer.json",
|
|
6717
|
-
"requirements.txt",
|
|
6718
|
-
".standardagents"
|
|
6719
|
-
];
|
|
6720
|
-
var MAX_ENTRIES3 = 500;
|
|
6721
|
-
function resolveBrowsePath(input3) {
|
|
6722
|
-
const home = os8.homedir();
|
|
6723
|
-
let p = (input3 ?? "").trim();
|
|
6724
|
-
if (!p) return home;
|
|
6725
|
-
if (p === "~") return home;
|
|
6726
|
-
if (p.startsWith("~/")) p = path4.join(home, p.slice(2));
|
|
6727
|
-
return path4.resolve(p);
|
|
6728
|
-
}
|
|
6729
|
-
function markers(dirPath) {
|
|
6730
|
-
let repo = false;
|
|
6731
|
-
let project = false;
|
|
6732
|
-
for (const marker of PROJECT_MARKERS) {
|
|
6733
|
-
let hit = false;
|
|
6734
|
-
try {
|
|
6735
|
-
hit = fs5.existsSync(path4.join(dirPath, marker));
|
|
6736
|
-
} catch {
|
|
6737
|
-
hit = false;
|
|
6738
|
-
}
|
|
6739
|
-
if (!hit) continue;
|
|
6740
|
-
project = true;
|
|
6741
|
-
if (marker === ".git") repo = true;
|
|
6742
|
-
if (repo) break;
|
|
6743
|
-
}
|
|
6744
|
-
return { project, repo };
|
|
6745
|
-
}
|
|
6746
|
-
function browseDirectory(input3, opts = {}) {
|
|
6747
|
-
const home = os8.homedir();
|
|
6748
|
-
const abs = resolveBrowsePath(input3);
|
|
6749
|
-
const parent = path4.dirname(abs);
|
|
6750
|
-
const base = {
|
|
6751
|
-
path: abs,
|
|
6752
|
-
parent: parent === abs ? null : parent,
|
|
6753
|
-
home
|
|
6754
|
-
};
|
|
6755
|
-
let dirents;
|
|
6756
|
-
try {
|
|
6757
|
-
const stat = fs5.statSync(abs);
|
|
6758
|
-
if (!stat.isDirectory()) {
|
|
6759
|
-
return { ...base, entries: [], truncated: false, error: "Not a directory" };
|
|
6760
|
-
}
|
|
6761
|
-
dirents = fs5.readdirSync(abs, { withFileTypes: true });
|
|
6762
|
-
} catch (err) {
|
|
6763
|
-
const code = err?.code;
|
|
6764
|
-
const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : code === "ENOENT" ? "Folder not found" : "Could not read this folder";
|
|
6765
|
-
return { ...base, entries: [], truncated: false, error: message };
|
|
6766
|
-
}
|
|
6767
|
-
const showHidden = !!opts.showHidden;
|
|
6768
|
-
const rawDirs = [];
|
|
6769
|
-
const rawFiles = [];
|
|
6770
|
-
for (const d of dirents) {
|
|
6771
|
-
const name = d.name;
|
|
6772
|
-
if (!showHidden && name.startsWith(".")) continue;
|
|
6773
|
-
let isDir = d.isDirectory();
|
|
6774
|
-
if (d.isSymbolicLink()) {
|
|
6775
|
-
try {
|
|
6776
|
-
isDir = fs5.statSync(path4.join(abs, name)).isDirectory();
|
|
6777
|
-
} catch {
|
|
6778
|
-
isDir = false;
|
|
6779
|
-
}
|
|
6780
|
-
}
|
|
6781
|
-
if (isDir) {
|
|
6782
|
-
const { project, repo } = markers(path4.join(abs, name));
|
|
6783
|
-
rawDirs.push({ name, dir: true, project: project || void 0, repo: repo || void 0 });
|
|
6784
|
-
} else {
|
|
6785
|
-
rawFiles.push({ name, dir: false });
|
|
6786
|
-
}
|
|
6787
|
-
}
|
|
6788
|
-
const cmp = (a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
6789
|
-
rawDirs.sort(cmp);
|
|
6790
|
-
rawFiles.sort(cmp);
|
|
6791
|
-
const all = [...rawDirs, ...rawFiles];
|
|
6792
|
-
const truncated = all.length > MAX_ENTRIES3;
|
|
6793
|
-
return { ...base, entries: truncated ? all.slice(0, MAX_ENTRIES3) : all, truncated };
|
|
6794
|
-
}
|
|
6795
|
-
function mkdirDirectory(parentInput, name, opts = {}) {
|
|
6796
|
-
const parent = resolveBrowsePath(parentInput);
|
|
6797
|
-
const clean2 = (name ?? "").trim();
|
|
6798
|
-
const listParent = () => browseDirectory(parent, opts);
|
|
6799
|
-
if (!clean2 || clean2 === "." || clean2 === ".." || clean2.includes("/") || clean2.includes("\\") || clean2.includes("\0")) {
|
|
6800
|
-
return { ...listParent(), error: "Invalid folder name." };
|
|
6801
|
-
}
|
|
6802
|
-
const target = path4.join(parent, clean2);
|
|
6803
|
-
if (path4.dirname(target) !== parent) {
|
|
6804
|
-
return { ...listParent(), error: "Invalid folder name." };
|
|
6805
|
-
}
|
|
6806
|
-
try {
|
|
6807
|
-
fs5.mkdirSync(target, { recursive: false });
|
|
6808
|
-
} catch (err) {
|
|
6809
|
-
const code = err?.code;
|
|
6810
|
-
const message = code === "EEXIST" ? "A folder with that name already exists." : code === "EACCES" || code === "EPERM" ? "Permission denied." : code === "ENOENT" ? "The parent folder no longer exists." : "Could not create the folder.";
|
|
6811
|
-
return { ...listParent(), error: message };
|
|
6812
|
-
}
|
|
6813
|
-
return listParent();
|
|
6814
|
-
}
|
|
6815
7266
|
var PKG_NAME = "@standardagents/code";
|
|
6816
7267
|
var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}`;
|
|
6817
7268
|
var CACHE_REL_DIR = ".config/standardagents-cli";
|
|
@@ -7576,7 +8027,7 @@ function serviceStatus() {
|
|
|
7576
8027
|
}
|
|
7577
8028
|
|
|
7578
8029
|
// src/daemon-cli.ts
|
|
7579
|
-
var
|
|
8030
|
+
var c3 = {
|
|
7580
8031
|
reset: "\x1B[0m",
|
|
7581
8032
|
dim: "\x1B[2m",
|
|
7582
8033
|
bold: "\x1B[1m",
|
|
@@ -7589,9 +8040,9 @@ function usage() {
|
|
|
7589
8040
|
stdout.write(
|
|
7590
8041
|
[
|
|
7591
8042
|
"",
|
|
7592
|
-
`${
|
|
8043
|
+
`${c3.bold}standardcode daemon${c3.reset} \u2014 headless execution client for this machine`,
|
|
7593
8044
|
"",
|
|
7594
|
-
`${
|
|
8045
|
+
`${c3.bold}Commands${c3.reset}`,
|
|
7595
8046
|
" install [--endpoint url] Sign in (if needed), name this machine, and install",
|
|
7596
8047
|
" the always-on service (launchd / systemd).",
|
|
7597
8048
|
" uninstall Stop and remove the service.",
|
|
@@ -7631,11 +8082,11 @@ async function ensureSignedIn(endpoint) {
|
|
|
7631
8082
|
const api2 = new ApiClient(endpoint, stored.access_token);
|
|
7632
8083
|
const check2 = await api2.verifyDetailed();
|
|
7633
8084
|
if (check2.ok) return api2;
|
|
7634
|
-
stdout.write(`${
|
|
8085
|
+
stdout.write(`${c3.yellow}Saved sign-in for ${host} failed:${c3.reset} ${check2.reason}
|
|
7635
8086
|
|
|
7636
8087
|
`);
|
|
7637
8088
|
}
|
|
7638
|
-
stdout.write(`${
|
|
8089
|
+
stdout.write(`${c3.bold}Sign in to Standard Code${c3.reset} ${c3.dim}(${host})${c3.reset}
|
|
7639
8090
|
|
|
7640
8091
|
`);
|
|
7641
8092
|
const token = await deviceLogin(endpoint);
|
|
@@ -7646,7 +8097,7 @@ async function ensureSignedIn(endpoint) {
|
|
|
7646
8097
|
{ endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
|
|
7647
8098
|
{ updateDefault: false }
|
|
7648
8099
|
);
|
|
7649
|
-
stdout.write(`${
|
|
8100
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Signed in to ${c3.teal}${host}${c3.reset}
|
|
7650
8101
|
|
|
7651
8102
|
`);
|
|
7652
8103
|
return api;
|
|
@@ -7659,26 +8110,26 @@ async function installCommand(endpointFlag) {
|
|
|
7659
8110
|
const suggested = machineDisplayName(existing ?? { hostname: os8.hostname(), id: identity.machine_id });
|
|
7660
8111
|
const rl = readline2.createInterface({ input: stdin, output: stdout });
|
|
7661
8112
|
const answer = (await rl.question(
|
|
7662
|
-
`${
|
|
8113
|
+
`${c3.bold}Machine name${c3.reset} ${c3.dim}(shown in the session picker)${c3.reset} [${suggested}]: `
|
|
7663
8114
|
)).trim();
|
|
7664
8115
|
rl.close();
|
|
7665
8116
|
if (answer) await setMachineName(api, identity.machine_id, answer);
|
|
7666
8117
|
await updateOwnMachineRecord(api, identity);
|
|
7667
8118
|
const displayName = answer || suggested;
|
|
7668
|
-
stdout.write(`${
|
|
8119
|
+
stdout.write(`${c3.dim}Installing the always-on service\u2026${c3.reset}
|
|
7669
8120
|
`);
|
|
7670
8121
|
const extra = endpointFlag ? ["--endpoint", endpoint] : [];
|
|
7671
8122
|
const result = installService(resolveDaemonCommand(extra), endpointFlag ? endpoint : void 0);
|
|
7672
8123
|
if (!result.ok) {
|
|
7673
|
-
stdout.write(`${
|
|
8124
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${result.detail}
|
|
7674
8125
|
`);
|
|
7675
|
-
if (result.manualHint) stdout.write(`${
|
|
8126
|
+
if (result.manualHint) stdout.write(`${c3.dim}${result.manualHint}${c3.reset}
|
|
7676
8127
|
`);
|
|
7677
8128
|
process.exit(1);
|
|
7678
8129
|
}
|
|
7679
|
-
stdout.write(`${
|
|
8130
|
+
stdout.write(`${c3.green}\u2713${c3.reset} ${result.detail}
|
|
7680
8131
|
`);
|
|
7681
|
-
stdout.write(`${
|
|
8132
|
+
stdout.write(`${c3.dim}Waiting for the daemon's first heartbeat\u2026${c3.reset}
|
|
7682
8133
|
`);
|
|
7683
8134
|
const deadline = Date.now() + 6e4;
|
|
7684
8135
|
let alive = false;
|
|
@@ -7692,17 +8143,17 @@ async function installCommand(endpointFlag) {
|
|
|
7692
8143
|
}
|
|
7693
8144
|
if (alive) {
|
|
7694
8145
|
stdout.write(
|
|
7695
|
-
`${
|
|
8146
|
+
`${c3.green}\u2713${c3.reset} ${c3.bold}${displayName}${c3.reset} is online.
|
|
7696
8147
|
|
|
7697
8148
|
Sessions started elsewhere can now run on this machine.
|
|
7698
|
-
${
|
|
7699
|
-
or add one now: standardcode daemon add-project <path>${
|
|
8149
|
+
${c3.dim}Projects register automatically when you run standardcode in a directory here,
|
|
8150
|
+
or add one now: standardcode daemon add-project <path>${c3.reset}
|
|
7700
8151
|
`
|
|
7701
8152
|
);
|
|
7702
8153
|
} else {
|
|
7703
8154
|
stdout.write(
|
|
7704
|
-
`${
|
|
7705
|
-
${
|
|
8155
|
+
`${c3.yellow}\u26A0${c3.reset} The service installed but no heartbeat arrived yet.
|
|
8156
|
+
${c3.dim}Check ~/.standardagents/daemon.log and \`standardcode daemon status\`.${c3.reset}
|
|
7706
8157
|
`
|
|
7707
8158
|
);
|
|
7708
8159
|
}
|
|
@@ -7710,16 +8161,16 @@ ${c2.dim}Check ~/.standardagents/daemon.log and \`standardcode daemon status\`.$
|
|
|
7710
8161
|
async function statusCommand() {
|
|
7711
8162
|
const status = serviceStatus();
|
|
7712
8163
|
const identity = loadMachineIdentity();
|
|
7713
|
-
stdout.write(`${
|
|
8164
|
+
stdout.write(`${c3.bold}Service:${c3.reset} ${status.detail}
|
|
7714
8165
|
`);
|
|
7715
8166
|
const endpoint = resolveEndpoint();
|
|
7716
8167
|
const cred = getCredential(endpoint);
|
|
7717
8168
|
if (!cred) {
|
|
7718
8169
|
stdout.write(
|
|
7719
|
-
`${
|
|
8170
|
+
`${c3.bold}Machine:${c3.reset} ${os8.hostname()} ${c3.dim}(${identity.machine_id})${c3.reset}
|
|
7720
8171
|
`
|
|
7721
8172
|
);
|
|
7722
|
-
stdout.write(`${
|
|
8173
|
+
stdout.write(`${c3.bold}Account:${c3.reset} ${c3.yellow}not signed in to ${endpoint}${c3.reset}
|
|
7723
8174
|
`);
|
|
7724
8175
|
return;
|
|
7725
8176
|
}
|
|
@@ -7727,33 +8178,33 @@ async function statusCommand() {
|
|
|
7727
8178
|
const api = new ApiClient(endpoint, cred.access_token);
|
|
7728
8179
|
const record2 = await loadMachine(api, identity.machine_id).catch(() => null);
|
|
7729
8180
|
stdout.write(
|
|
7730
|
-
`${
|
|
8181
|
+
`${c3.bold}Machine:${c3.reset} ${machineDisplayName(record2 ?? { hostname: os8.hostname(), id: identity.machine_id })} ${c3.dim}(${identity.machine_id})${c3.reset}
|
|
7731
8182
|
`
|
|
7732
8183
|
);
|
|
7733
8184
|
if (!record2) {
|
|
7734
|
-
stdout.write(`${
|
|
8185
|
+
stdout.write(`${c3.bold}Registry:${c3.reset} not registered yet
|
|
7735
8186
|
`);
|
|
7736
8187
|
return;
|
|
7737
8188
|
}
|
|
7738
8189
|
const online = daemonOnline(record2);
|
|
7739
8190
|
const seen = record2.daemon ? `${Math.round((Date.now() - record2.daemon.last_seen_at) / 1e3)}s ago (v${record2.daemon.version})` : "never";
|
|
7740
|
-
stdout.write(`${
|
|
8191
|
+
stdout.write(`${c3.bold}Registry:${c3.reset} ${online ? `${c3.green}online${c3.reset}` : `${c3.yellow}offline${c3.reset}`} \xB7 last heartbeat ${seen}
|
|
7741
8192
|
`);
|
|
7742
8193
|
const projects = Object.keys(record2.projects);
|
|
7743
|
-
stdout.write(`${
|
|
8194
|
+
stdout.write(`${c3.bold}Projects:${c3.reset} ${projects.length ? "" : c3.dim + "none registered" + c3.reset}
|
|
7744
8195
|
`);
|
|
7745
|
-
for (const p of projects.sort()) stdout.write(` ${
|
|
8196
|
+
for (const p of projects.sort()) stdout.write(` ${c3.dim}${p}${c3.reset}
|
|
7746
8197
|
`);
|
|
7747
8198
|
}
|
|
7748
8199
|
async function projectCommand(action, target) {
|
|
7749
8200
|
if (!target) {
|
|
7750
|
-
stdout.write(`${
|
|
8201
|
+
stdout.write(`${c3.red}\u2717${c3.reset} Expected a project path.
|
|
7751
8202
|
`);
|
|
7752
8203
|
process.exit(1);
|
|
7753
8204
|
}
|
|
7754
8205
|
const dir2 = path4.resolve(target);
|
|
7755
8206
|
if (action === "add" && !fs5.existsSync(dir2)) {
|
|
7756
|
-
stdout.write(`${
|
|
8207
|
+
stdout.write(`${c3.red}\u2717${c3.reset} ${dir2} does not exist on this machine.
|
|
7757
8208
|
`);
|
|
7758
8209
|
process.exit(1);
|
|
7759
8210
|
}
|
|
@@ -7764,11 +8215,11 @@ async function projectCommand(action, target) {
|
|
|
7764
8215
|
const displayName = machineDisplayName(record2 ?? { hostname: os8.hostname(), id: identity.machine_id });
|
|
7765
8216
|
if (action === "add") {
|
|
7766
8217
|
await registerProject(api, identity, dir2);
|
|
7767
|
-
stdout.write(`${
|
|
8218
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Registered ${dir2} for remote sessions on ${displayName}.
|
|
7768
8219
|
`);
|
|
7769
8220
|
} else {
|
|
7770
8221
|
await unregisterProject(api, identity, dir2);
|
|
7771
|
-
stdout.write(`${
|
|
8222
|
+
stdout.write(`${c3.green}\u2713${c3.reset} Removed ${dir2} from this machine's projects.
|
|
7772
8223
|
`);
|
|
7773
8224
|
}
|
|
7774
8225
|
}
|
|
@@ -7784,7 +8235,7 @@ async function runDaemonCommand(argv) {
|
|
|
7784
8235
|
return;
|
|
7785
8236
|
case "uninstall": {
|
|
7786
8237
|
const result = uninstallService();
|
|
7787
|
-
stdout.write(`${result.ok ?
|
|
8238
|
+
stdout.write(`${result.ok ? c3.green + "\u2713" : c3.red + "\u2717"}${c3.reset} ${result.detail}
|
|
7788
8239
|
`);
|
|
7789
8240
|
const ep = resolveEndpoint(endpoint);
|
|
7790
8241
|
const cred = getCredential(ep);
|
|
@@ -7817,7 +8268,7 @@ async function runDaemonCommand(argv) {
|
|
|
7817
8268
|
}
|
|
7818
8269
|
|
|
7819
8270
|
// src/index.ts
|
|
7820
|
-
var
|
|
8271
|
+
var c4 = {
|
|
7821
8272
|
reset: "\x1B[0m",
|
|
7822
8273
|
dim: "\x1B[2m",
|
|
7823
8274
|
bold: "\x1B[1m",
|
|
@@ -7846,10 +8297,10 @@ function printUsage() {
|
|
|
7846
8297
|
stdout.write(
|
|
7847
8298
|
[
|
|
7848
8299
|
"",
|
|
7849
|
-
`${
|
|
8300
|
+
`${c4.bold}Usage${c4.reset}`,
|
|
7850
8301
|
" standardcode [options] [dir]",
|
|
7851
8302
|
"",
|
|
7852
|
-
`${
|
|
8303
|
+
`${c4.bold}Options${c4.reset}`,
|
|
7853
8304
|
" -e, --endpoint [url] Use a different Standard Agents instance for this run",
|
|
7854
8305
|
" (default: https://api.standardcode.ai).",
|
|
7855
8306
|
" If url is omitted, prompt for it.",
|
|
@@ -7917,15 +8368,15 @@ function parseArgs2(args) {
|
|
|
7917
8368
|
}
|
|
7918
8369
|
function printCommandBlock(tui, command, output4, ok, where) {
|
|
7919
8370
|
tui.print("");
|
|
7920
|
-
const note = where ? ` ${
|
|
7921
|
-
tui.print(`${
|
|
8371
|
+
const note = where ? ` ${c4.dim}(ran on ${where})${c4.reset}` : "";
|
|
8372
|
+
tui.print(`${c4.magenta}!${c4.reset} ${c4.bold}${command}${c4.reset}${note}`);
|
|
7922
8373
|
const body = (output4 ?? "").replace(/\s+$/, "");
|
|
7923
8374
|
if (body) {
|
|
7924
8375
|
for (const line of body.split("\n")) {
|
|
7925
|
-
tui.print(` ${ok ?
|
|
8376
|
+
tui.print(` ${ok ? c4.dim : c4.red}${line}${c4.reset}`);
|
|
7926
8377
|
}
|
|
7927
8378
|
} else {
|
|
7928
|
-
tui.print(` ${
|
|
8379
|
+
tui.print(` ${c4.dim}(no output)${c4.reset}`);
|
|
7929
8380
|
}
|
|
7930
8381
|
tui.print("");
|
|
7931
8382
|
}
|
|
@@ -7936,7 +8387,7 @@ function printAssistant(tui, text) {
|
|
|
7936
8387
|
let dotted = false;
|
|
7937
8388
|
for (const line of renderStreamingMarkdown(text, cols2)) {
|
|
7938
8389
|
if (!dotted && line.trim()) {
|
|
7939
|
-
tui.print(`${
|
|
8390
|
+
tui.print(`${c4.gray}\u2022${c4.reset} ${line}`);
|
|
7940
8391
|
dotted = true;
|
|
7941
8392
|
} else {
|
|
7942
8393
|
tui.print(` ${line}`);
|
|
@@ -7951,7 +8402,7 @@ function startLoader(label) {
|
|
|
7951
8402
|
const draw = () => {
|
|
7952
8403
|
const now = Date.now();
|
|
7953
8404
|
const f = frames[Math.floor(now / 70) % frames.length];
|
|
7954
|
-
stdout.write(`\r\x1B[K${pad}${brandCycleColor(now)}${f}${
|
|
8405
|
+
stdout.write(`\r\x1B[K${pad}${brandCycleColor(now)}${f}${c4.reset} ${c4.dim}${label}\u2026${c4.reset}`);
|
|
7955
8406
|
};
|
|
7956
8407
|
draw();
|
|
7957
8408
|
const timer = setInterval(draw, 70);
|
|
@@ -7967,12 +8418,12 @@ function farewell(stoppedProcs = 0) {
|
|
|
7967
8418
|
if (stoppedProcs > 0) {
|
|
7968
8419
|
stdout.write(
|
|
7969
8420
|
`
|
|
7970
|
-
${
|
|
8421
|
+
${c4.cyan}\u2699${c4.reset} Stopped ${stoppedProcs} background process${stoppedProcs === 1 ? "" : "es"}.
|
|
7971
8422
|
`
|
|
7972
8423
|
);
|
|
7973
8424
|
}
|
|
7974
8425
|
stdout.write(`
|
|
7975
|
-
${
|
|
8426
|
+
${c4.teal}\u25C7${c4.reset} ${c4.dim}Standard Code \u2014 see you soon.${c4.reset}
|
|
7976
8427
|
`);
|
|
7977
8428
|
}
|
|
7978
8429
|
function printWelcome(endpoint, projectDir) {
|
|
@@ -7986,10 +8437,10 @@ function printWelcome(endpoint, projectDir) {
|
|
|
7986
8437
|
const metaWidth = Math.max(1, terminalColumns - pad.length - markWidth - 3 - 1);
|
|
7987
8438
|
const displayDir = truncateMiddle(dir2, metaWidth);
|
|
7988
8439
|
const meta = [
|
|
7989
|
-
`${
|
|
7990
|
-
`${
|
|
7991
|
-
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${
|
|
7992
|
-
`${
|
|
8440
|
+
`${c4.bold}${gradientText("Standard Code")}${c4.reset}${version ? ` ${c4.dim}v${version}${c4.reset}` : ""}`,
|
|
8441
|
+
`${c4.dim}terminal coding agent${c4.reset}`,
|
|
8442
|
+
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c4.teal}${host}${c4.reset}`],
|
|
8443
|
+
`${c4.dim}${displayDir}${c4.reset}`
|
|
7993
8444
|
];
|
|
7994
8445
|
const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
|
|
7995
8446
|
stdout.write("\n");
|
|
@@ -8003,11 +8454,11 @@ function printWelcome(endpoint, projectDir) {
|
|
|
8003
8454
|
}
|
|
8004
8455
|
function colorActivity(line) {
|
|
8005
8456
|
const m = line.match(/^(\s*)([✓✗⛔])\s?([\s\S]*)$/);
|
|
8006
|
-
if (!m) return `${
|
|
8457
|
+
if (!m) return `${c4.dim}${line}${c4.reset}`;
|
|
8007
8458
|
const [, indent, glyph, rest] = m;
|
|
8008
8459
|
if (glyph === "\u2713") {
|
|
8009
|
-
const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${
|
|
8010
|
-
return `${indent}${
|
|
8460
|
+
const body = rest.replace(/\s(\([^()]*\))\s*$/, ` ${c4.dim}$1${c4.reset}`);
|
|
8461
|
+
return `${indent}${c4.green}\u2713${c4.reset} ${body}`;
|
|
8011
8462
|
}
|
|
8012
8463
|
if (glyph === "\u2717") {
|
|
8013
8464
|
const ERR_MAX_LINES = 7;
|
|
@@ -8015,15 +8466,15 @@ function colorActivity(line) {
|
|
|
8015
8466
|
const shown = lines.slice(0, ERR_MAX_LINES);
|
|
8016
8467
|
const hidden = lines.length - shown.length;
|
|
8017
8468
|
const body = shown.map(
|
|
8018
|
-
(l, i) => i === 0 ? `${indent}${
|
|
8469
|
+
(l, i) => i === 0 ? `${indent}${c4.red}\u2717 ${l}${c4.reset}` : `${indent}${c4.red}${c4.dim}${l}${c4.reset}`
|
|
8019
8470
|
).join("\n");
|
|
8020
8471
|
if (hidden > 0) {
|
|
8021
8472
|
return `${body}
|
|
8022
|
-
${indent}${
|
|
8473
|
+
${indent}${c4.dim}\u2026 +${hidden} more line${hidden === 1 ? "" : "s"}${c4.reset}`;
|
|
8023
8474
|
}
|
|
8024
8475
|
return body;
|
|
8025
8476
|
}
|
|
8026
|
-
return `${indent}${
|
|
8477
|
+
return `${indent}${c4.yellow}\u26D4 ${rest}${c4.reset}`;
|
|
8027
8478
|
}
|
|
8028
8479
|
async function main() {
|
|
8029
8480
|
if (process.argv[2] === "daemon") {
|
|
@@ -8034,7 +8485,7 @@ async function main() {
|
|
|
8034
8485
|
try {
|
|
8035
8486
|
cliArgs = parseArgs2(process.argv.slice(2));
|
|
8036
8487
|
} catch (error) {
|
|
8037
|
-
stdout.write(`${
|
|
8488
|
+
stdout.write(`${c4.red}error:${c4.reset} ${error instanceof Error ? error.message : String(error)}
|
|
8038
8489
|
`);
|
|
8039
8490
|
printUsage();
|
|
8040
8491
|
process.exit(1);
|
|
@@ -8046,7 +8497,7 @@ async function main() {
|
|
|
8046
8497
|
const endpointArg = cliArgs.endpoint;
|
|
8047
8498
|
const endpointOverride = cliArgs.promptEndpoint || typeof endpointArg === "string" && endpointArg.trim() !== "";
|
|
8048
8499
|
const dirArg = cliArgs.dir;
|
|
8049
|
-
|
|
8500
|
+
let projectDir = path4.resolve(dirArg || process.cwd());
|
|
8050
8501
|
const machine = os8.hostname();
|
|
8051
8502
|
const reader = { rl: null };
|
|
8052
8503
|
let handoffClosing = false;
|
|
@@ -8061,7 +8512,7 @@ async function main() {
|
|
|
8061
8512
|
}
|
|
8062
8513
|
preflightArmed = true;
|
|
8063
8514
|
stdout.write(`
|
|
8064
|
-
${
|
|
8515
|
+
${c4.dim}Press Control-C again to exit${c4.reset}
|
|
8065
8516
|
`);
|
|
8066
8517
|
preflightTimer = setTimeout(() => {
|
|
8067
8518
|
preflightArmed = false;
|
|
@@ -8083,10 +8534,10 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8083
8534
|
const askEndpoint = async () => {
|
|
8084
8535
|
for (; ; ) {
|
|
8085
8536
|
const answer = (await ask(
|
|
8086
|
-
`${
|
|
8537
|
+
`${c4.cyan}Standard Agents instance URL${c4.reset} (e.g. http://localhost:5178): `
|
|
8087
8538
|
)).trim();
|
|
8088
8539
|
if (answer) return answer;
|
|
8089
|
-
stdout.write(`${
|
|
8540
|
+
stdout.write(`${c4.dim}An endpoint URL is required.${c4.reset}
|
|
8090
8541
|
`);
|
|
8091
8542
|
}
|
|
8092
8543
|
};
|
|
@@ -8101,7 +8552,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8101
8552
|
const tlsRelaxed = relaxTlsForLocalEndpoint(endpoint);
|
|
8102
8553
|
printWelcome(endpoint, projectDir);
|
|
8103
8554
|
if (tlsRelaxed) {
|
|
8104
|
-
stdout.write(`${
|
|
8555
|
+
stdout.write(`${c4.dim} TLS verification relaxed for local endpoint.${c4.reset}
|
|
8105
8556
|
|
|
8106
8557
|
`);
|
|
8107
8558
|
}
|
|
@@ -8113,7 +8564,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8113
8564
|
loading.stop();
|
|
8114
8565
|
const applied = consumeAppliedUpdate(version);
|
|
8115
8566
|
if (applied) {
|
|
8116
|
-
stdout.write(` ${
|
|
8567
|
+
stdout.write(` ${c4.green}\u2713${c4.reset} ${c4.dim}Standard Code updated to v${version}.${c4.reset}
|
|
8117
8568
|
|
|
8118
8569
|
`);
|
|
8119
8570
|
}
|
|
@@ -8122,21 +8573,21 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8122
8573
|
const decision = decideAutoUpdate(updateAvailable, { state: readAutoUpdateState(), pm });
|
|
8123
8574
|
if (decision === "start" && pm && startBackgroundUpdate(updateAvailable.latest, pm)) {
|
|
8124
8575
|
stdout.write(
|
|
8125
|
-
` ${
|
|
8576
|
+
` ${c4.teal}\u27F3${c4.reset} ${c4.dim}Standard Code ${c4.reset}${c4.bold}v${updateAvailable.latest}${c4.reset}${c4.dim} is installing in the background \u2014 it applies on your next launch.${c4.reset}
|
|
8126
8577
|
|
|
8127
8578
|
`
|
|
8128
8579
|
);
|
|
8129
8580
|
} else if (decision === "in_flight") {
|
|
8130
8581
|
stdout.write(
|
|
8131
|
-
` ${
|
|
8582
|
+
` ${c4.teal}\u27F3${c4.reset} ${c4.dim}Standard Code v${updateAvailable.latest} is still installing in the background.${c4.reset}
|
|
8132
8583
|
|
|
8133
8584
|
`
|
|
8134
8585
|
);
|
|
8135
8586
|
} else {
|
|
8136
8587
|
const display = updateCommand(pm ?? "npm").display;
|
|
8137
8588
|
stdout.write(
|
|
8138
|
-
` ${
|
|
8139
|
-
${
|
|
8589
|
+
` ${c4.teal}\u25C7${c4.reset} ${c4.dim}Update available:${c4.reset} ${c4.dim}v${updateAvailable.current}${c4.reset} \u2192 ${c4.bold}v${updateAvailable.latest}${c4.reset}
|
|
8590
|
+
${c4.dim}Run ${c4.reset}${c4.bold}${display}${c4.reset}${c4.dim} to update${c4.reset}
|
|
8140
8591
|
|
|
8141
8592
|
`
|
|
8142
8593
|
);
|
|
@@ -8154,39 +8605,39 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8154
8605
|
if (!api || !storedCheck?.ok) {
|
|
8155
8606
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
8156
8607
|
if (storedCheck && !storedCheck.ok) {
|
|
8157
|
-
stdout.write(`${
|
|
8608
|
+
stdout.write(`${c4.red}\u2717${c4.reset} ${c4.dim}Saved sign-in for this endpoint failed:${c4.reset} ${storedCheck.reason}
|
|
8158
8609
|
`);
|
|
8159
|
-
if (storedCheck.hint) stdout.write(` ${
|
|
8610
|
+
if (storedCheck.hint) stdout.write(` ${c4.dim}${storedCheck.hint}${c4.reset}
|
|
8160
8611
|
`);
|
|
8161
8612
|
stdout.write("\n");
|
|
8162
8613
|
}
|
|
8163
8614
|
const explainFailure = (result, prefix) => {
|
|
8164
|
-
stdout.write(`${
|
|
8615
|
+
stdout.write(`${c4.red}\u2717${c4.reset} ${prefix}${result.reason}
|
|
8165
8616
|
`);
|
|
8166
|
-
if (result.hint) stdout.write(` ${
|
|
8617
|
+
if (result.hint) stdout.write(` ${c4.dim}${result.hint}${c4.reset}
|
|
8167
8618
|
`);
|
|
8168
8619
|
};
|
|
8169
|
-
stdout.write(`${
|
|
8620
|
+
stdout.write(`${c4.bold}${gradientText("Sign in to Standard Code")}${c4.reset}
|
|
8170
8621
|
`);
|
|
8171
8622
|
if (`https://${host}` !== PRODUCTION_ENDPOINT) {
|
|
8172
|
-
stdout.write(`${
|
|
8623
|
+
stdout.write(`${c4.dim}Connecting to${c4.reset} ${c4.teal}${host}${c4.reset}
|
|
8173
8624
|
`);
|
|
8174
8625
|
}
|
|
8175
8626
|
stdout.write(
|
|
8176
|
-
`${
|
|
8627
|
+
`${c4.dim}You'll only need to do this once on this machine.${c4.reset}
|
|
8177
8628
|
|
|
8178
8629
|
`
|
|
8179
8630
|
);
|
|
8180
8631
|
stdout.write(
|
|
8181
|
-
`${
|
|
8632
|
+
`${c4.white}Press ${c4.bold}Enter${c4.reset}${c4.white} to open your browser and sign in.${c4.reset} ${c4.dim}(or paste an API token)${c4.reset}
|
|
8182
8633
|
|
|
8183
8634
|
`
|
|
8184
8635
|
);
|
|
8185
8636
|
for (; ; ) {
|
|
8186
|
-
const token = (await ask(`${
|
|
8637
|
+
const token = (await ask(`${c4.teal}\u276F${c4.reset} `)).trim();
|
|
8187
8638
|
if (!token) {
|
|
8188
8639
|
const got = await deviceLogin(endpoint).catch((e) => {
|
|
8189
|
-
stdout.write(`${
|
|
8640
|
+
stdout.write(`${c4.red}\u2717${c4.reset} ${c4.dim}${e instanceof Error ? e.message : String(e)}${c4.reset}
|
|
8190
8641
|
`);
|
|
8191
8642
|
return null;
|
|
8192
8643
|
});
|
|
@@ -8200,7 +8651,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8200
8651
|
{ endpoint, access_token: got, token_type: "Bearer", saved_at: Date.now() },
|
|
8201
8652
|
{ updateDefault: !endpointOverride }
|
|
8202
8653
|
);
|
|
8203
|
-
stdout.write(`${
|
|
8654
|
+
stdout.write(`${c4.green}\u2713${c4.reset} Connected to ${c4.teal}${host}${c4.reset}
|
|
8204
8655
|
`);
|
|
8205
8656
|
break;
|
|
8206
8657
|
}
|
|
@@ -8216,7 +8667,7 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8216
8667
|
{ endpoint, access_token: token, token_type: "Bearer", saved_at: Date.now() },
|
|
8217
8668
|
{ updateDefault: !endpointOverride }
|
|
8218
8669
|
);
|
|
8219
|
-
stdout.write(`${
|
|
8670
|
+
stdout.write(`${c4.green}\u2713${c4.reset} Connected to ${c4.teal}${host}${c4.reset}
|
|
8220
8671
|
`);
|
|
8221
8672
|
break;
|
|
8222
8673
|
}
|
|
@@ -8232,92 +8683,30 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8232
8683
|
void registerProject(api, identity, projectDir).catch(() => {
|
|
8233
8684
|
});
|
|
8234
8685
|
const tui = new Tui(1);
|
|
8235
|
-
let
|
|
8686
|
+
let agentOverride;
|
|
8236
8687
|
if (cliArgs.agent) {
|
|
8237
8688
|
const wanted = cliArgs.agent.toLowerCase();
|
|
8238
8689
|
if (["sama", "opensama", "sama_one", OPENSAMA_AGENT_ID].includes(wanted)) {
|
|
8239
|
-
|
|
8690
|
+
agentOverride = OPENSAMA_AGENT_ID;
|
|
8240
8691
|
} else if (["unlimited", "standard", "unlimited_one", AGENT_ID].includes(wanted)) {
|
|
8241
|
-
|
|
8692
|
+
agentOverride = AGENT_ID;
|
|
8242
8693
|
} else {
|
|
8243
|
-
stdout.write(`${
|
|
8694
|
+
stdout.write(`${c4.red}error:${c4.reset} Unknown agent "${cliArgs.agent}". Use unlimited or sama.
|
|
8244
8695
|
`);
|
|
8245
8696
|
process.exit(1);
|
|
8246
8697
|
}
|
|
8247
|
-
} else {
|
|
8248
|
-
const picked = await tui.select(
|
|
8249
|
-
`${c3.bold}${gradientText("Which agent?")}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
|
|
8250
|
-
AGENT_CHOICES.map((choice) => ({
|
|
8251
|
-
label: choice.title,
|
|
8252
|
-
hint: choice.description,
|
|
8253
|
-
value: choice.id,
|
|
8254
|
-
disabled: choice.disabled
|
|
8255
|
-
}))
|
|
8256
|
-
);
|
|
8257
|
-
if (!picked) process.exit(0);
|
|
8258
|
-
selectedAgent = picked;
|
|
8259
8698
|
}
|
|
8260
|
-
|
|
8261
|
-
const ok = await ensureSamaAuth(tui, api);
|
|
8262
|
-
if (!ok) process.exit(0);
|
|
8263
|
-
}
|
|
8264
|
-
const home = os8.homedir();
|
|
8265
|
-
const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
8266
|
-
const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
|
|
8699
|
+
let selectedAgent = agentOverride ?? AGENT_ID;
|
|
8267
8700
|
const session = { mode: "local", identity };
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
[
|
|
8278
|
-
{ label: `\u{1F5A5}\uFE0F This machine \u2014 ${shortDir}`, hint: "tools run locally", value: null },
|
|
8279
|
-
...remoteTargets.map((m) => ({
|
|
8280
|
-
label: `${machineIcon(m)} ${m.name}`,
|
|
8281
|
-
hint: `${m.hostname} \xB7 daemon online${m.daemon ? ` \xB7 v${m.daemon.version}` : ""}`,
|
|
8282
|
-
value: m
|
|
8283
|
-
}))
|
|
8284
|
-
]
|
|
8285
|
-
);
|
|
8286
|
-
if (where) {
|
|
8287
|
-
const remotePath = await pickRemoteProject(tui, api, where);
|
|
8288
|
-
if (remotePath) {
|
|
8289
|
-
session.mode = "remote";
|
|
8290
|
-
session.runner = where;
|
|
8291
|
-
session.remotePath = remotePath;
|
|
8292
|
-
}
|
|
8293
|
-
}
|
|
8294
|
-
}
|
|
8295
|
-
}
|
|
8296
|
-
let tags;
|
|
8297
|
-
let resumeTags;
|
|
8298
|
-
if (session.mode === "remote" && session.runner && session.remotePath) {
|
|
8299
|
-
tags = [
|
|
8300
|
-
`path:${session.remotePath}`,
|
|
8301
|
-
`machine:${session.runner.hostname || session.runner.name}`,
|
|
8302
|
-
`runner:${session.runner.id}`
|
|
8303
|
-
];
|
|
8304
|
-
resumeTags = [`path:${session.remotePath}`, `runner:${session.runner.id}`];
|
|
8305
|
-
} else {
|
|
8306
|
-
tags = [`path:${projectDir}`, `machine:${machine}`, `runner:${identity.machine_id}`];
|
|
8307
|
-
resumeTags = [`path:${projectDir}`, `machine:${machine}`];
|
|
8308
|
-
}
|
|
8309
|
-
const loadingSessions = startLoader("Loading sessions");
|
|
8310
|
-
let existing = [];
|
|
8311
|
-
try {
|
|
8312
|
-
existing = await api.listThreads(
|
|
8313
|
-
selectedAgent === OPENSAMA_AGENT_ID ? [OPENSAMA_AGENT_ID] : AGENT_ID_VARIANTS,
|
|
8314
|
-
resumeTags
|
|
8315
|
-
);
|
|
8316
|
-
} catch {
|
|
8317
|
-
existing = [];
|
|
8318
|
-
}
|
|
8319
|
-
const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
|
|
8320
|
-
loadingSessions.stop();
|
|
8701
|
+
const loadingMachines = startLoader("Checking your machines");
|
|
8702
|
+
const machines = await loadMachines(api).catch(() => []);
|
|
8703
|
+
loadingMachines.stop();
|
|
8704
|
+
session.suggestDaemonInstall = machines.every((m) => !m.daemon);
|
|
8705
|
+
const remoteTargets = machines.filter((m) => m.id !== identity.machine_id && daemonOnline(m));
|
|
8706
|
+
const self = machines.find((m) => m.id === identity.machine_id);
|
|
8707
|
+
const launchDir = projectDir;
|
|
8708
|
+
let tags = [];
|
|
8709
|
+
let resumeTags = [];
|
|
8321
8710
|
const createSessionThread = async () => {
|
|
8322
8711
|
const id = await api.createThread(selectedAgent, tags);
|
|
8323
8712
|
if (session.mode === "remote" && session.runner && session.remotePath) {
|
|
@@ -8330,33 +8719,161 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
|
|
|
8330
8719
|
}
|
|
8331
8720
|
return id;
|
|
8332
8721
|
};
|
|
8333
|
-
let threadId;
|
|
8722
|
+
let threadId = "";
|
|
8334
8723
|
let resumed = false;
|
|
8335
8724
|
let historySeed;
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8725
|
+
let existing = [];
|
|
8726
|
+
let hadResumeMenu = false;
|
|
8727
|
+
let where = null;
|
|
8728
|
+
let step = remoteTargets.length > 0 ? "machine" : "thread";
|
|
8729
|
+
flow: for (; ; ) {
|
|
8730
|
+
if (step === "machine") {
|
|
8731
|
+
const picked = await tui.select(
|
|
8732
|
+
`${c4.bold}${gradientText("Where should this session run?")}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8733
|
+
[
|
|
8734
|
+
{
|
|
8735
|
+
label: `This machine \u2014 ${self ? machineDisplayName(self) : machine}`,
|
|
8736
|
+
detail: "tools run locally",
|
|
8737
|
+
value: null
|
|
8738
|
+
},
|
|
8739
|
+
...remoteTargets.map((m) => ({
|
|
8740
|
+
label: machineDisplayName(m),
|
|
8741
|
+
detail: `${m.hostname} \xB7 daemon online${m.daemon ? ` \xB7 v${m.daemon.version}` : ""}`,
|
|
8742
|
+
value: m
|
|
8743
|
+
}))
|
|
8744
|
+
],
|
|
8745
|
+
{ spaced: true }
|
|
8746
|
+
);
|
|
8747
|
+
if (picked === void 0) process.exit(0);
|
|
8748
|
+
where = picked;
|
|
8749
|
+
step = "project";
|
|
8750
|
+
} else if (step === "project") {
|
|
8751
|
+
session.mode = "local";
|
|
8752
|
+
session.runner = void 0;
|
|
8753
|
+
session.remotePath = void 0;
|
|
8754
|
+
projectDir = launchDir;
|
|
8755
|
+
if (where) {
|
|
8756
|
+
const remotePath = await pickRemoteProject(tui, api, where);
|
|
8757
|
+
if (!remotePath) {
|
|
8758
|
+
step = "machine";
|
|
8759
|
+
continue;
|
|
8760
|
+
}
|
|
8761
|
+
session.mode = "remote";
|
|
8762
|
+
session.runner = where;
|
|
8763
|
+
session.remotePath = remotePath;
|
|
8764
|
+
} else {
|
|
8765
|
+
const localPath = await pickLocalProject(tui, api, self, launchDir, machine);
|
|
8766
|
+
if (localPath === null) {
|
|
8767
|
+
step = "machine";
|
|
8768
|
+
continue;
|
|
8769
|
+
}
|
|
8770
|
+
if (localPath !== launchDir) {
|
|
8771
|
+
projectDir = localPath;
|
|
8772
|
+
void registerProject(api, identity, projectDir).catch(() => {
|
|
8773
|
+
});
|
|
8774
|
+
}
|
|
8775
|
+
}
|
|
8776
|
+
step = "thread";
|
|
8777
|
+
} else if (step === "thread") {
|
|
8778
|
+
if (session.mode === "remote" && session.runner && session.remotePath) {
|
|
8779
|
+
tags = [
|
|
8780
|
+
`path:${session.remotePath}`,
|
|
8781
|
+
`machine:${session.runner.hostname || session.runner.name}`,
|
|
8782
|
+
`runner:${session.runner.id}`
|
|
8783
|
+
];
|
|
8784
|
+
resumeTags = [`path:${session.remotePath}`, `runner:${session.runner.id}`];
|
|
8785
|
+
} else {
|
|
8786
|
+
tags = [`path:${projectDir}`, `machine:${machine}`, `runner:${identity.machine_id}`];
|
|
8787
|
+
resumeTags = [`path:${projectDir}`, `machine:${machine}`];
|
|
8788
|
+
}
|
|
8789
|
+
const loadingSessions = startLoader("Loading sessions");
|
|
8790
|
+
try {
|
|
8791
|
+
const listIds = agentOverride ? agentOverride === OPENSAMA_AGENT_ID ? [OPENSAMA_AGENT_ID] : AGENT_ID_VARIANTS : [...AGENT_ID_VARIANTS, OPENSAMA_AGENT_ID];
|
|
8792
|
+
existing = await api.listThreads(listIds, resumeTags);
|
|
8793
|
+
existing.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0));
|
|
8794
|
+
} catch {
|
|
8795
|
+
existing = [];
|
|
8796
|
+
}
|
|
8797
|
+
const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
|
|
8798
|
+
loadingSessions.stop();
|
|
8799
|
+
if (existing.length === 0) {
|
|
8800
|
+
hadResumeMenu = false;
|
|
8801
|
+
step = "agent";
|
|
8802
|
+
continue;
|
|
8803
|
+
}
|
|
8804
|
+
hadResumeMenu = true;
|
|
8805
|
+
const home = os8.homedir();
|
|
8806
|
+
const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
8807
|
+
const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
|
|
8808
|
+
const items = summaries.map((s) => ({
|
|
8809
|
+
label: s.label,
|
|
8810
|
+
hint: s.hint,
|
|
8811
|
+
value: s.id
|
|
8812
|
+
}));
|
|
8813
|
+
items.push({ label: "\uFF0B Start a new session", value: null });
|
|
8814
|
+
const whereLabel = session.mode === "remote" && session.runner ? `${session.runner.name}:${shortenPath(session.remotePath ?? "")}` : shortDir;
|
|
8815
|
+
const picked = await tui.select(
|
|
8816
|
+
`${c4.bold}${gradientText("Resume a session")}${c4.reset} ${c4.gray}${whereLabel}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8817
|
+
items
|
|
8818
|
+
);
|
|
8819
|
+
if (picked === void 0) {
|
|
8820
|
+
if (remoteTargets.length > 0) {
|
|
8821
|
+
step = "project";
|
|
8822
|
+
continue;
|
|
8823
|
+
}
|
|
8824
|
+
process.exit(0);
|
|
8825
|
+
}
|
|
8826
|
+
if (typeof picked === "string") {
|
|
8827
|
+
threadId = picked;
|
|
8828
|
+
resumed = true;
|
|
8829
|
+
const threadAgent = existing.find((t) => t.id === picked)?.agent_id;
|
|
8830
|
+
selectedAgent = threadAgent === OPENSAMA_AGENT_ID ? OPENSAMA_AGENT_ID : AGENT_ID;
|
|
8831
|
+
if (selectedAgent === OPENSAMA_AGENT_ID) {
|
|
8832
|
+
const ok = await ensureSamaAuth(tui, api);
|
|
8833
|
+
if (!ok) continue;
|
|
8834
|
+
}
|
|
8835
|
+
break flow;
|
|
8836
|
+
}
|
|
8837
|
+
historySeed = existing[0]?.id;
|
|
8838
|
+
step = "agent";
|
|
8351
8839
|
} else {
|
|
8840
|
+
if (!agentOverride) {
|
|
8841
|
+
const picked = await tui.select(
|
|
8842
|
+
`${c4.bold}${gradientText("Which agent?")}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8843
|
+
AGENT_CHOICES.map((choice) => ({
|
|
8844
|
+
label: choice.title,
|
|
8845
|
+
hint: choice.description,
|
|
8846
|
+
value: choice.id,
|
|
8847
|
+
disabled: choice.disabled
|
|
8848
|
+
}))
|
|
8849
|
+
);
|
|
8850
|
+
if (!picked) {
|
|
8851
|
+
if (hadResumeMenu) {
|
|
8852
|
+
step = "thread";
|
|
8853
|
+
continue;
|
|
8854
|
+
}
|
|
8855
|
+
if (remoteTargets.length > 0) {
|
|
8856
|
+
step = "project";
|
|
8857
|
+
continue;
|
|
8858
|
+
}
|
|
8859
|
+
process.exit(0);
|
|
8860
|
+
}
|
|
8861
|
+
selectedAgent = picked;
|
|
8862
|
+
}
|
|
8863
|
+
if (selectedAgent === OPENSAMA_AGENT_ID) {
|
|
8864
|
+
const ok = await ensureSamaAuth(tui, api);
|
|
8865
|
+
if (!ok) {
|
|
8866
|
+
if (agentOverride) process.exit(0);
|
|
8867
|
+
continue;
|
|
8868
|
+
}
|
|
8869
|
+
}
|
|
8352
8870
|
threadId = await createSessionThread();
|
|
8353
|
-
|
|
8871
|
+
break flow;
|
|
8354
8872
|
}
|
|
8355
|
-
} else {
|
|
8356
|
-
threadId = await createSessionThread();
|
|
8357
8873
|
}
|
|
8358
8874
|
for (; ; ) {
|
|
8359
|
-
|
|
8875
|
+
const agentTitle = AGENT_CHOICES.find((a) => a.id === selectedAgent)?.title ?? selectedAgent;
|
|
8876
|
+
await runInteractive(tui, api, threadId, projectDir, machine, resumed, session, agentTitle, historySeed);
|
|
8360
8877
|
historySeed = threadId;
|
|
8361
8878
|
threadId = await createSessionThread();
|
|
8362
8879
|
await api.kvSet(threadId, "lease_supersedes", historySeed);
|
|
@@ -8372,7 +8889,7 @@ async function ensureSamaAuth(tui, api) {
|
|
|
8372
8889
|
checking.stop();
|
|
8373
8890
|
if (already) return true;
|
|
8374
8891
|
const picked = await tui.select(
|
|
8375
|
-
`${
|
|
8892
|
+
`${c4.bold}${gradientText("Authenticate with ChatGPT")}${c4.reset} ${c4.dim}Sama One runs on OpenAI using your own ChatGPT Pro account${c4.reset}`,
|
|
8376
8893
|
[
|
|
8377
8894
|
{
|
|
8378
8895
|
label: "Continue with ChatGPT",
|
|
@@ -8385,7 +8902,7 @@ async function ensureSamaAuth(tui, api) {
|
|
|
8385
8902
|
if (picked !== "continue") return false;
|
|
8386
8903
|
openUrl("https://standardcode.ai/app?connect=sama");
|
|
8387
8904
|
tui.print(
|
|
8388
|
-
`${
|
|
8905
|
+
`${c4.dim}Finish connecting ChatGPT in the browser \u2014 waiting here for the authorization to land on your account\u2026${c4.reset}`
|
|
8389
8906
|
);
|
|
8390
8907
|
const waiting = startLoader("Waiting for your ChatGPT authorization");
|
|
8391
8908
|
const deadline = Date.now() + 5 * 60 * 1e3;
|
|
@@ -8394,20 +8911,20 @@ async function ensureSamaAuth(tui, api) {
|
|
|
8394
8911
|
if (await api.openSamaStatus().catch(() => false)) {
|
|
8395
8912
|
waiting.stop();
|
|
8396
8913
|
tui.print(
|
|
8397
|
-
`${
|
|
8914
|
+
`${c4.green}\u2713${c4.reset} ChatGPT connected \u2014 Sama One is now unlocked on your account (terminal, web, and macOS app).`
|
|
8398
8915
|
);
|
|
8399
8916
|
return true;
|
|
8400
8917
|
}
|
|
8401
8918
|
}
|
|
8402
8919
|
waiting.stop();
|
|
8403
8920
|
tui.print(
|
|
8404
|
-
`${
|
|
8921
|
+
`${c4.yellow}Still not connected.${c4.reset} Finish the flow at ${c4.teal}standardcode.ai/app${c4.reset} and pick Sama One again.`
|
|
8405
8922
|
);
|
|
8406
8923
|
return false;
|
|
8407
8924
|
}
|
|
8408
8925
|
async function runAgentSwitchMenu(tui, api, threadId) {
|
|
8409
8926
|
const picked = await tui.select(
|
|
8410
|
-
`${
|
|
8927
|
+
`${c4.bold}${gradientText("Switch agent")}${c4.reset} ${c4.dim}takes effect on the next message${c4.reset}`,
|
|
8411
8928
|
AGENT_CHOICES.map((choice) => ({
|
|
8412
8929
|
label: choice.title,
|
|
8413
8930
|
hint: choice.description,
|
|
@@ -8423,41 +8940,68 @@ async function runAgentSwitchMenu(tui, api, threadId) {
|
|
|
8423
8940
|
const title = AGENT_CHOICES.find((choice) => choice.id === picked)?.title ?? picked;
|
|
8424
8941
|
try {
|
|
8425
8942
|
await api.setThreadAgent(threadId, picked);
|
|
8426
|
-
tui.
|
|
8943
|
+
tui.setAgentLabel(title);
|
|
8944
|
+
tui.print(`${c4.green}\u2713${c4.reset} Session handed to ${c4.bold}${title}${c4.reset} \u2014 applies from your next message.`);
|
|
8427
8945
|
} catch (e) {
|
|
8428
8946
|
tui.print(
|
|
8429
|
-
`${
|
|
8947
|
+
`${c4.red}\u2717 couldn't switch agent:${c4.reset} ${c4.gray}${e instanceof Error ? e.message : String(e)}${c4.reset}`
|
|
8430
8948
|
);
|
|
8431
8949
|
}
|
|
8432
8950
|
}
|
|
8951
|
+
async function pickLocalProject(tui, api, self, cwd, machineName) {
|
|
8952
|
+
const NEW = "__new__";
|
|
8953
|
+
const label = self ? machineDisplayName(self) : machineName;
|
|
8954
|
+
const others = Object.entries(self?.projects ?? {}).filter(([dir2]) => dir2 !== cwd).sort((a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0));
|
|
8955
|
+
const labels = projectDisplayLabels({ ...self?.projects ?? {}, [cwd]: self?.projects?.[cwd] });
|
|
8956
|
+
const items = [
|
|
8957
|
+
{
|
|
8958
|
+
label: labels.get(cwd) ?? projectDisplayName(cwd, self?.projects?.[cwd]),
|
|
8959
|
+
hint: "current directory",
|
|
8960
|
+
detail: shortenPath(cwd, 60),
|
|
8961
|
+
value: cwd
|
|
8962
|
+
},
|
|
8963
|
+
...others.map(([dir2, p]) => ({
|
|
8964
|
+
label: labels.get(dir2) ?? projectDisplayName(dir2, p),
|
|
8965
|
+
hint: p?.last_used_at ? relativeTime(p.last_used_at / 1e3) : "",
|
|
8966
|
+
detail: shortenPath(dir2, 60),
|
|
8967
|
+
value: dir2
|
|
8968
|
+
})),
|
|
8969
|
+
{ label: "\uFF0B New project", hint: "browse or create a directory", value: NEW }
|
|
8970
|
+
];
|
|
8971
|
+
const picked = await tui.select(
|
|
8972
|
+
`${c4.bold}${gradientText(`Project on ${label}`)}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8973
|
+
items,
|
|
8974
|
+
{ spaced: true }
|
|
8975
|
+
);
|
|
8976
|
+
if (!picked) return null;
|
|
8977
|
+
if (picked !== NEW) return picked;
|
|
8978
|
+
return await pickDirectory(tui, localBrowseBackend(label), { startPath: cwd, loader: startLoader });
|
|
8979
|
+
}
|
|
8433
8980
|
async function pickRemoteProject(tui, api, runner) {
|
|
8434
8981
|
const ENTER_PATH = "__enter_path__";
|
|
8435
8982
|
const projects = Object.entries(runner.projects).sort(
|
|
8436
8983
|
(a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
|
|
8437
8984
|
);
|
|
8438
|
-
const
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8985
|
+
const labels = projectDisplayLabels(runner.projects);
|
|
8986
|
+
const items = projects.map(
|
|
8987
|
+
([dir2, p]) => ({
|
|
8988
|
+
label: labels.get(dir2) ?? projectDisplayName(dir2, p),
|
|
8989
|
+
hint: p?.last_used_at ? relativeTime(p.last_used_at / 1e3) : "",
|
|
8990
|
+
detail: shortenPath(dir2, 60),
|
|
8991
|
+
value: dir2
|
|
8992
|
+
})
|
|
8993
|
+
);
|
|
8994
|
+
items.push({ label: "\uFF0B New project", hint: "browse or create a directory", value: ENTER_PATH });
|
|
8444
8995
|
const picked = await tui.select(
|
|
8445
|
-
`${
|
|
8446
|
-
items
|
|
8996
|
+
`${c4.bold}${gradientText(`Project on ${runner.name}`)}${c4.reset} ${c4.dim}\u2191\u2193 \xB7 enter \xB7 esc${c4.reset}`,
|
|
8997
|
+
items,
|
|
8998
|
+
{ spaced: true }
|
|
8447
8999
|
);
|
|
8448
9000
|
if (!picked) return null;
|
|
8449
9001
|
if (picked !== ENTER_PATH) return picked;
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
);
|
|
8454
|
-
if (!typed) return null;
|
|
8455
|
-
const trimmed = typed.trim();
|
|
8456
|
-
if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
|
|
8457
|
-
tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
|
|
8458
|
-
return null;
|
|
8459
|
-
}
|
|
8460
|
-
return await resolveRemoteProjectPath(api, runner.id, trimmed);
|
|
9002
|
+
return await pickDirectory(tui, remoteBrowseBackend(api, runner.id, runner.name), {
|
|
9003
|
+
loader: startLoader
|
|
9004
|
+
});
|
|
8461
9005
|
}
|
|
8462
9006
|
function isSilentMessage(m) {
|
|
8463
9007
|
return m?.silent === true || m?.metadata?.silent === true;
|
|
@@ -8474,14 +9018,15 @@ async function summarizeThreads(api, threads) {
|
|
|
8474
9018
|
}
|
|
8475
9019
|
const label = preview ? preview.length > 64 ? preview.slice(0, 63) + "\u2026" : preview : "(empty session)";
|
|
8476
9020
|
const when = t.created_at ? relativeTime(t.created_at) : "";
|
|
8477
|
-
const
|
|
9021
|
+
const agentTag = t.agent_id === OPENSAMA_AGENT_ID ? AGENT_CHOICES.find((a) => a.id === OPENSAMA_AGENT_ID)?.title ?? "Sama One" : "";
|
|
9022
|
+
const hint = [t.id.slice(0, 8), when, agentTag].filter(Boolean).join(" \xB7 ");
|
|
8478
9023
|
return { id: t.id, label, hint };
|
|
8479
9024
|
})
|
|
8480
9025
|
);
|
|
8481
9026
|
}
|
|
8482
9027
|
function subagentLabel(s, titles) {
|
|
8483
9028
|
const agentName = (s.agent_name || "").trim();
|
|
8484
|
-
const title = (s.title || "").trim() || titles.get(agentName) || (agentName ? agentName.replace(/[_-]+/g, " ").replace(/\b\w/g, (
|
|
9029
|
+
const title = (s.title || "").trim() || titles.get(agentName) || (agentName ? agentName.replace(/[_-]+/g, " ").replace(/\b\w/g, (c5) => c5.toUpperCase()) : "Subagent");
|
|
8485
9030
|
const tagged = (s.threadName || "").trim();
|
|
8486
9031
|
return tagged ? `${title} \xB7 ${tagged}` : title;
|
|
8487
9032
|
}
|
|
@@ -8504,8 +9049,8 @@ async function printHistory(api, threadId, tui) {
|
|
|
8504
9049
|
).sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
|
|
8505
9050
|
if (!convo.length) return;
|
|
8506
9051
|
const shown = convo.slice(-24);
|
|
8507
|
-
tui.print(`${
|
|
8508
|
-
if (shown.length < convo.length) tui.print(`${
|
|
9052
|
+
tui.print(`${c4.dim}\u2500\u2500 resuming session \xB7 ${convo.length} message${convo.length === 1 ? "" : "s"} \u2500\u2500${c4.reset}`);
|
|
9053
|
+
if (shown.length < convo.length) tui.print(`${c4.dim} \u2026 earlier messages omitted${c4.reset}`);
|
|
8509
9054
|
for (const m of shown) {
|
|
8510
9055
|
if (m.metadata?.user_command) {
|
|
8511
9056
|
printCommandBlock(
|
|
@@ -8522,9 +9067,12 @@ async function printHistory(api, threadId, tui) {
|
|
|
8522
9067
|
else printAssistant(tui, text);
|
|
8523
9068
|
}
|
|
8524
9069
|
}
|
|
8525
|
-
async function runInteractive(tui, api, threadId, projectDir, machine, resumed, session, historySeedThreadId) {
|
|
9070
|
+
async function runInteractive(tui, api, threadId, projectDir, machine, resumed, session, agentTitle, historySeedThreadId) {
|
|
8526
9071
|
const remote = session.mode === "remote";
|
|
8527
9072
|
const runnerName = session.runner?.name ?? "the remote machine";
|
|
9073
|
+
let currentOwner = null;
|
|
9074
|
+
let ownershipKnown = false;
|
|
9075
|
+
const describeExecOwner = (o) => o?.client_kind === "daemon" ? "the daemon on this machine" : o?.client_name || "another client";
|
|
8528
9076
|
const registry = new ProcessRegistry(api, threadId, machine);
|
|
8529
9077
|
const refreshBgCount = () => {
|
|
8530
9078
|
void registry.runningCount().then((n) => tui.setBackgroundCount(n)).catch(() => {
|
|
@@ -8550,6 +9098,7 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
8550
9098
|
saveApprovals(api, threadId, perm);
|
|
8551
9099
|
const attaching = startLoader("Attaching to thread");
|
|
8552
9100
|
let busy = false;
|
|
9101
|
+
let optimisticBusyUntil = 0;
|
|
8553
9102
|
let sharedMessaging = emptySharedMessagingSnapshot();
|
|
8554
9103
|
let mirroredDraftRefs = [];
|
|
8555
9104
|
let sharedMessagingReady = false;
|
|
@@ -8609,26 +9158,28 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
|
|
|
8609
9158
|
if (detail) for (const d of detail) tui.print(d);
|
|
8610
9159
|
refreshBgCount();
|
|
8611
9160
|
},
|
|
8612
|
-
onOwnership: (isOwner,
|
|
9161
|
+
onOwnership: (isOwner, owner2) => {
|
|
9162
|
+
const wasKnown = ownershipKnown;
|
|
9163
|
+
ownershipKnown = true;
|
|
9164
|
+
currentOwner = owner2;
|
|
8613
9165
|
if (isOwner) {
|
|
8614
9166
|
bridge?.setClaim("takeover");
|
|
8615
9167
|
exec?.writeSessionInfo();
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
);
|
|
9168
|
+
if (wasKnown) tui.print(`${c4.dim}Tool execution moved to this terminal.${c4.reset}`);
|
|
9169
|
+
} else if (wasKnown) {
|
|
9170
|
+
tui.print(`${c4.dim}Tool execution moved to ${describeExecOwner(owner2)}.${c4.reset}`);
|
|
8620
9171
|
}
|
|
8621
9172
|
},
|
|
8622
9173
|
onClaimRefused: (reason) => {
|
|
8623
9174
|
if (reason === "in_flight") {
|
|
8624
9175
|
tui.print(
|
|
8625
|
-
`${
|
|
9176
|
+
`${c4.dim}The session's current client is mid-operation \u2014 execution stays there until it finishes.${c4.reset}`
|
|
8626
9177
|
);
|
|
8627
9178
|
}
|
|
8628
9179
|
},
|
|
8629
9180
|
onSuperseded: () => {
|
|
8630
9181
|
tui.print(
|
|
8631
|
-
`${
|
|
9182
|
+
`${c4.yellow}\u26A0 Another Standard Code process on this machine took over this session \u2014 this terminal is watching.${c4.reset}`
|
|
8632
9183
|
);
|
|
8633
9184
|
},
|
|
8634
9185
|
onStatus: (id, summary) => {
|
|
@@ -8767,7 +9318,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8767
9318
|
const sessionEnded = new Promise((r) => endSession = r);
|
|
8768
9319
|
const quit = async () => {
|
|
8769
9320
|
tui.end();
|
|
8770
|
-
const stopped2 = bridge?.isOwner ?? false ? api.
|
|
9321
|
+
const stopped2 = bridge?.isOwner ?? false ? api.stopThread(threadId).catch(() => {
|
|
8771
9322
|
}) : Promise.resolve();
|
|
8772
9323
|
const procsStopped2 = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
|
|
8773
9324
|
bridge?.close();
|
|
@@ -8786,7 +9337,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8786
9337
|
const logout = async () => {
|
|
8787
9338
|
deleteCredential(api.origin);
|
|
8788
9339
|
const instanceHost = api.origin.replace(/^https?:\/\//, "");
|
|
8789
|
-
tui.print(`${
|
|
9340
|
+
tui.print(`${c4.gray}Signed out \u2014 removed the saved token for ${c4.teal}${instanceHost}${c4.reset}${c4.gray}. Run standardcode to sign in again.${c4.reset}`);
|
|
8790
9341
|
await quit();
|
|
8791
9342
|
};
|
|
8792
9343
|
const bgMgr = {
|
|
@@ -8794,7 +9345,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8794
9345
|
stop: async (id) => {
|
|
8795
9346
|
if (!host) {
|
|
8796
9347
|
tui.print(
|
|
8797
|
-
`${
|
|
9348
|
+
`${c4.gray}That process runs on ${runnerName} \u2014 ask the agent to stop it (it manages processes there).${c4.reset}`
|
|
8798
9349
|
);
|
|
8799
9350
|
return;
|
|
8800
9351
|
}
|
|
@@ -8875,7 +9426,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8875
9426
|
} catch (error) {
|
|
8876
9427
|
if (!sharedMessagingReady && !sharedMessagingUnavailableShown) {
|
|
8877
9428
|
sharedMessagingUnavailableShown = true;
|
|
8878
|
-
tui.print(`${
|
|
9429
|
+
tui.print(`${c4.dim}shared messaging unavailable: ${error instanceof Error ? error.message : String(error)}${c4.reset}`);
|
|
8879
9430
|
}
|
|
8880
9431
|
}
|
|
8881
9432
|
};
|
|
@@ -8885,7 +9436,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8885
9436
|
applySharedMessaging(snapshot, false);
|
|
8886
9437
|
return true;
|
|
8887
9438
|
}).catch((error) => {
|
|
8888
|
-
tui.print(`${
|
|
9439
|
+
tui.print(`${c4.dim}shared messaging failed: ${error instanceof Error ? error.message : String(error)}${c4.reset}`);
|
|
8889
9440
|
return false;
|
|
8890
9441
|
});
|
|
8891
9442
|
const appendSharedPending = (text, images, refs = []) => applySharedMutation(api.appendPendingInput(threadId, {
|
|
@@ -8893,11 +9444,6 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8893
9444
|
attachments: [...refs, ...toSharedAttachments(images)],
|
|
8894
9445
|
...messagingOrigin
|
|
8895
9446
|
}));
|
|
8896
|
-
const steerSharedInput = (text, images, refs = []) => applySharedMutation(api.steerInput(threadId, {
|
|
8897
|
-
content: text,
|
|
8898
|
-
attachments: [...refs, ...toSharedAttachments(images)],
|
|
8899
|
-
...messagingOrigin
|
|
8900
|
-
}));
|
|
8901
9447
|
const editSharedPending = (item, text, images, refs) => applySharedMutation(api.updatePendingInput(threadId, item.id, {
|
|
8902
9448
|
content: text,
|
|
8903
9449
|
attachments: [
|
|
@@ -8907,34 +9453,52 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8907
9453
|
...messagingOrigin
|
|
8908
9454
|
}));
|
|
8909
9455
|
const dismissSharedPending = (item) => applySharedMutation(api.dismissPendingInput(threadId, item.id, messagingOrigin));
|
|
8910
|
-
const promoteSharedPending = (item) =>
|
|
9456
|
+
const promoteSharedPending = async (item) => {
|
|
9457
|
+
if (busy) {
|
|
9458
|
+
await Promise.all([
|
|
9459
|
+
api.stopThread(threadId).catch(() => {
|
|
9460
|
+
}),
|
|
9461
|
+
...[...activeSubagents.keys()].map((childId) => api.stopThread(childId).catch(() => {
|
|
9462
|
+
}))
|
|
9463
|
+
]);
|
|
9464
|
+
const promoted = await applySharedMutation(api.steerPendingInput(threadId, item.id, messagingOrigin));
|
|
9465
|
+
if (promoted) await api.continueThread(threadId).catch(() => {
|
|
9466
|
+
});
|
|
9467
|
+
return promoted;
|
|
9468
|
+
}
|
|
9469
|
+
return applySharedMutation(api.steerPendingInput(threadId, item.id, messagingOrigin));
|
|
9470
|
+
};
|
|
8911
9471
|
const sendNow = async (text, images = [], refs = []) => {
|
|
8912
9472
|
lastSent = { text, images, refs };
|
|
8913
9473
|
tui.printUserMessage(text || `\u{1F4CE} ${refs.length + images.length} attachment(s)`);
|
|
8914
9474
|
const key = text.trim();
|
|
8915
9475
|
pendingSent.set(key, (pendingSent.get(key) ?? 0) + 1);
|
|
9476
|
+
busy = true;
|
|
9477
|
+
optimisticBusyUntil = Date.now() + 8e3;
|
|
9478
|
+
tui.setWorking(true);
|
|
8916
9479
|
try {
|
|
8917
9480
|
await api.sendMessage(threadId, text, [...refs, ...toAttachments(images)]);
|
|
8918
9481
|
} catch (e) {
|
|
8919
9482
|
const n = (pendingSent.get(key) ?? 1) - 1;
|
|
8920
9483
|
if (n > 0) pendingSent.set(key, n);
|
|
8921
9484
|
else pendingSent.delete(key);
|
|
8922
|
-
|
|
9485
|
+
busy = false;
|
|
9486
|
+
optimisticBusyUntil = 0;
|
|
9487
|
+
tui.setWorking(false);
|
|
9488
|
+
tui.print(`${c4.dim}failed to send: ${e instanceof Error ? e.message : String(e)}${c4.reset}`);
|
|
8923
9489
|
return false;
|
|
8924
9490
|
}
|
|
8925
|
-
busy = true;
|
|
8926
|
-
tui.setWorking(true);
|
|
8927
9491
|
return true;
|
|
8928
9492
|
};
|
|
8929
9493
|
const whereLabel = remote ? runnerName : "this machine";
|
|
8930
9494
|
let bangRunning = false;
|
|
8931
9495
|
const runBangCommand = async (command) => {
|
|
8932
9496
|
if (bangRunning) {
|
|
8933
|
-
tui.print(`${
|
|
9497
|
+
tui.print(`${c4.dim}a command is already running \u2014 one at a time.${c4.reset}`);
|
|
8934
9498
|
return;
|
|
8935
9499
|
}
|
|
8936
9500
|
bangRunning = true;
|
|
8937
|
-
tui.print(`${
|
|
9501
|
+
tui.print(`${c4.magenta}!${c4.reset} ${c4.dim}running on ${whereLabel}\u2026${c4.reset}`);
|
|
8938
9502
|
try {
|
|
8939
9503
|
const res = await api.runCommand(threadId, command);
|
|
8940
9504
|
if (res.messageId) shownIds.add(res.messageId);
|
|
@@ -8948,7 +9512,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8948
9512
|
const openPendingMenu = async () => {
|
|
8949
9513
|
const items = sharedMessaging.pending.items;
|
|
8950
9514
|
if (!items.length) {
|
|
8951
|
-
tui.print(`${
|
|
9515
|
+
tui.print(`${c4.dim}No pending messages.${c4.reset}`);
|
|
8952
9516
|
return;
|
|
8953
9517
|
}
|
|
8954
9518
|
const picked = await tui.select("Pending messages", items.map((item, index) => ({
|
|
@@ -8978,16 +9542,16 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8978
9542
|
try {
|
|
8979
9543
|
await api.compact(threadId);
|
|
8980
9544
|
} catch (err) {
|
|
8981
|
-
tui.print(`${
|
|
9545
|
+
tui.print(`${c4.red}\u2717${c4.reset} couldn't start compaction: ${err.message}`);
|
|
8982
9546
|
}
|
|
8983
9547
|
};
|
|
8984
9548
|
const runAccountCommand = async () => {
|
|
8985
|
-
tui.print(`${
|
|
9549
|
+
tui.print(`${c4.gray}Opening your account\u2026${c4.reset}`);
|
|
8986
9550
|
const link = await api.accountLink(threadId).catch(() => null);
|
|
8987
9551
|
const target = link?.url ?? "https://standardcode.ai/account";
|
|
8988
9552
|
openUrl(target);
|
|
8989
9553
|
tui.print(
|
|
8990
|
-
link?.preauthed ? `${
|
|
9554
|
+
link?.preauthed ? `${c4.gray}\u2192 account dashboard opened in your browser (signed in)${c4.reset}` : `${c4.gray}\u2192 opened ${target} \u2014 sign in with your account email${c4.reset}`
|
|
8991
9555
|
);
|
|
8992
9556
|
};
|
|
8993
9557
|
const ordinal = (n) => {
|
|
@@ -8998,24 +9562,24 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
8998
9562
|
const renderUpgradePanel = (q) => {
|
|
8999
9563
|
const dots = [];
|
|
9000
9564
|
for (let i = 0; i < q.max; i++) {
|
|
9001
|
-
if (i < q.current) dots.push(`${
|
|
9002
|
-
else if (i === q.current) dots.push(`${
|
|
9003
|
-
else dots.push(`${
|
|
9565
|
+
if (i < q.current) dots.push(`${c4.teal}\u25CF${c4.reset}`);
|
|
9566
|
+
else if (i === q.current) dots.push(`${c4.bold}${gradientText("\uFF0B")}${c4.reset}`);
|
|
9567
|
+
else dots.push(`${c4.dim}\xB7${c4.reset}`);
|
|
9004
9568
|
}
|
|
9005
9569
|
const cost = fmtCost(q);
|
|
9006
9570
|
const lines = [
|
|
9007
9571
|
"",
|
|
9008
|
-
`${
|
|
9572
|
+
`${c4.bold}${gradientText("\u2726 Add a parallel session")}${c4.reset}`,
|
|
9009
9573
|
"",
|
|
9010
|
-
`${dots.join(" ")} ${
|
|
9574
|
+
`${dots.join(" ")} ${c4.dim}${q.current} of ${q.current} session${q.current === 1 ? "" : "s"} in use${c4.reset}`
|
|
9011
9575
|
];
|
|
9012
9576
|
if (q.ends_trial) {
|
|
9013
9577
|
lines.push(
|
|
9014
|
-
`${
|
|
9015
|
-
`${
|
|
9578
|
+
`${c4.yellow}Your $5 trial covers 1 session. Adding a ${ordinal(q.sessions)} ends the trial${c4.reset}`,
|
|
9579
|
+
`${c4.yellow}and starts your $49/mo plan now${cost ? ` \u2014 ${c4.bold}${cost} charged today${c4.reset}${c4.yellow}` : ""}.${c4.reset}`
|
|
9016
9580
|
);
|
|
9017
9581
|
} else if (cost) {
|
|
9018
|
-
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${
|
|
9582
|
+
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 ${c4.bold}${cost} charged now${c4.reset}.`);
|
|
9019
9583
|
} else {
|
|
9020
9584
|
lines.push(`Add a ${ordinal(q.sessions)} session at $49/mo \u2014 prorated on your next invoice.`);
|
|
9021
9585
|
}
|
|
@@ -9028,7 +9592,7 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
9028
9592
|
try {
|
|
9029
9593
|
if (opts.auto) {
|
|
9030
9594
|
tui.print(
|
|
9031
|
-
`${
|
|
9595
|
+
`${c4.yellow}You're out of simultaneous sessions \u2014 another Standard Code session is using your slot.${c4.reset}`
|
|
9032
9596
|
);
|
|
9033
9597
|
}
|
|
9034
9598
|
const quote = await api.sessionsQuote(threadId);
|
|
@@ -9036,16 +9600,16 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
9036
9600
|
const link = await api.accountLink(threadId).catch(() => null);
|
|
9037
9601
|
const target = link?.url ?? "https://standardcode.ai/account";
|
|
9038
9602
|
tui.print(
|
|
9039
|
-
`${
|
|
9603
|
+
`${c4.gray}Close the other session (its slot frees within ~90s) \u2014 or add another simultaneous session to your plan, then resend your message.${c4.reset}`
|
|
9040
9604
|
);
|
|
9041
9605
|
openUrl(target);
|
|
9042
|
-
tui.print(`${
|
|
9606
|
+
tui.print(`${c4.gray}\u2192 opened ${target} to manage your plan${c4.reset}`);
|
|
9043
9607
|
return;
|
|
9044
9608
|
}
|
|
9045
9609
|
if (quote.current >= quote.max) {
|
|
9046
9610
|
tui.print(
|
|
9047
|
-
`${
|
|
9048
|
-
${
|
|
9611
|
+
`${c4.yellow}You're at the maximum of ${quote.max} parallel session${quote.max === 1 ? "" : "s"}.${c4.reset}
|
|
9612
|
+
${c4.gray}Close another session (its slot frees within ~90s), then resend your message.${c4.reset}`
|
|
9049
9613
|
);
|
|
9050
9614
|
return;
|
|
9051
9615
|
}
|
|
@@ -9057,25 +9621,25 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9057
9621
|
{ label: "Not now", value: "no" }
|
|
9058
9622
|
]);
|
|
9059
9623
|
if (choice !== "go") {
|
|
9060
|
-
tui.print(`${
|
|
9624
|
+
tui.print(`${c4.gray}No change made \u2014 you can upgrade anytime with /upgrade.${c4.reset}`);
|
|
9061
9625
|
return;
|
|
9062
9626
|
}
|
|
9063
|
-
tui.print(`${
|
|
9627
|
+
tui.print(`${c4.gray}Applying\u2026${c4.reset}`);
|
|
9064
9628
|
let applied;
|
|
9065
9629
|
try {
|
|
9066
9630
|
applied = await api.sessionsUpgrade(threadId, quote.sessions);
|
|
9067
9631
|
} catch (e) {
|
|
9068
|
-
tui.print(`${
|
|
9632
|
+
tui.print(`${c4.red}\u2717${c4.reset} Upgrade failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
9069
9633
|
return;
|
|
9070
9634
|
}
|
|
9071
9635
|
if (!applied?.ok) {
|
|
9072
|
-
tui.print(`${
|
|
9636
|
+
tui.print(`${c4.red}\u2717${c4.reset} Upgrade failed: ${applied?.error ?? "unknown error"}`);
|
|
9073
9637
|
return;
|
|
9074
9638
|
}
|
|
9075
9639
|
const n = applied.sessions ?? quote.sessions;
|
|
9076
|
-
tui.print(`${
|
|
9640
|
+
tui.print(`${c4.green}\u2713${c4.reset} ${c4.bold}${gradientText(`You now have ${n} parallel session${n === 1 ? "" : "s"}.`)}${c4.reset}`);
|
|
9077
9641
|
if (opts.auto && lastSent) {
|
|
9078
|
-
tui.print(`${
|
|
9642
|
+
tui.print(`${c4.gray}Continuing\u2026${c4.reset}`);
|
|
9079
9643
|
await sendNow(lastSent.text, lastSent.images, lastSent.refs);
|
|
9080
9644
|
}
|
|
9081
9645
|
} finally {
|
|
@@ -9217,7 +9781,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9217
9781
|
const submitComposer = async (text, images, steer) => {
|
|
9218
9782
|
const draftRefs = mirroredDraftRefs;
|
|
9219
9783
|
if (!sharedMessagingReady && (busy || steer || editingPendingId !== null)) {
|
|
9220
|
-
tui.print(`${
|
|
9784
|
+
tui.print(`${c4.dim}Restoring shared message state \u2014 try again in a moment.${c4.reset}`);
|
|
9221
9785
|
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
9222
9786
|
tui.setInput(text, images);
|
|
9223
9787
|
return;
|
|
@@ -9240,11 +9804,11 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9240
9804
|
const item = sharedMessaging.pending.items.find((candidate) => candidate.id === pendingId);
|
|
9241
9805
|
editingPendingId = null;
|
|
9242
9806
|
if (!item) {
|
|
9243
|
-
tui.print(`${
|
|
9807
|
+
tui.print(`${c4.dim}That pending message was already dispatched or dismissed.${c4.reset}`);
|
|
9244
9808
|
return;
|
|
9245
9809
|
}
|
|
9246
9810
|
const updated = await editSharedPending(item, text, images, draftRefs);
|
|
9247
|
-
const promoted = !steer || !updated ? updated : await
|
|
9811
|
+
const promoted = !steer || !updated ? updated : await promoteSharedPending({ ...item, id: pendingId });
|
|
9248
9812
|
if (!promoted) {
|
|
9249
9813
|
mirroredDraftRefs = item.attachments.filter(isSharedAttachmentRef);
|
|
9250
9814
|
tui.setExternalAttachmentNames(mirroredDraftRefs.map((attachment) => attachment.name));
|
|
@@ -9253,8 +9817,14 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9253
9817
|
return;
|
|
9254
9818
|
}
|
|
9255
9819
|
if (steer) {
|
|
9256
|
-
tui.print(`${
|
|
9257
|
-
|
|
9820
|
+
tui.print(`${c4.yellow}\u21AA steering now \u2014 stopping the current step${c4.reset}`);
|
|
9821
|
+
await Promise.all([
|
|
9822
|
+
api.stopThread(threadId).catch(() => {
|
|
9823
|
+
}),
|
|
9824
|
+
...[...activeSubagents.keys()].map((childId) => api.stopThread(childId).catch(() => {
|
|
9825
|
+
}))
|
|
9826
|
+
]);
|
|
9827
|
+
if (!await sendNow(text, images, draftRefs)) {
|
|
9258
9828
|
mirroredDraftRefs = draftRefs;
|
|
9259
9829
|
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
9260
9830
|
tui.setInput(text, images);
|
|
@@ -9263,7 +9833,7 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9263
9833
|
}
|
|
9264
9834
|
if (busy) {
|
|
9265
9835
|
if (await appendSharedPending(text, images, draftRefs)) {
|
|
9266
|
-
tui.print(`${
|
|
9836
|
+
tui.print(`${c4.gray}\u23F3 pending:${c4.reset} ${text} ${c4.dim}(/queue to edit, steer, or dismiss)${c4.reset}`);
|
|
9267
9837
|
} else {
|
|
9268
9838
|
mirroredDraftRefs = draftRefs;
|
|
9269
9839
|
tui.setExternalAttachmentNames(draftRefs.map((attachment) => attachment.name));
|
|
@@ -9286,18 +9856,20 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9286
9856
|
tui.onInterrupt = () => {
|
|
9287
9857
|
const firstPending = sharedMessaging.pending.items[0];
|
|
9288
9858
|
if (!busy && firstPending) {
|
|
9289
|
-
tui.print(`${
|
|
9859
|
+
tui.print(`${c4.yellow}\u21AA steering the first pending message\u2026${c4.reset}`);
|
|
9290
9860
|
void promoteSharedPending(firstPending);
|
|
9291
9861
|
return;
|
|
9292
9862
|
}
|
|
9293
9863
|
if (busy) {
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9864
|
+
const queued = sharedMessaging.pending.items.length;
|
|
9865
|
+
tui.print(`${c4.yellow}\u25A0 stopping now${queued > 0 ? ` \u2014 ${queued} queued message${queued === 1 ? "" : "s"} kept` : ""}${c4.reset}`);
|
|
9866
|
+
const stops = [api.stopThread(threadId).catch(() => {
|
|
9867
|
+
})];
|
|
9868
|
+
for (const childId of activeSubagents.keys()) {
|
|
9869
|
+
stops.push(api.stopThread(childId).catch(() => {
|
|
9870
|
+
}));
|
|
9297
9871
|
}
|
|
9298
|
-
|
|
9299
|
-
tui.print(`${c3.yellow}${advancing ? "[stopping; next pending message will run]" : "[stopping at the next safe boundary]"}${c3.reset}`);
|
|
9300
|
-
void applySharedMutation(api.requestSharedStop(threadId, messagingOrigin));
|
|
9872
|
+
void Promise.all(stops);
|
|
9301
9873
|
}
|
|
9302
9874
|
};
|
|
9303
9875
|
tui.onBgBadge = () => {
|
|
@@ -9320,20 +9892,32 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9320
9892
|
void api.getGoal(threadId).then((g) => tui.setGoal(g)).catch(() => {
|
|
9321
9893
|
});
|
|
9322
9894
|
attaching.stop();
|
|
9895
|
+
const header = `${c4.bold}${c4.magenta}Standard Code${c4.reset} ${c4.dim}\u2014 ${agentTitle}${c4.reset}`;
|
|
9896
|
+
const remoteDaemonV = session.runner?.daemon?.version;
|
|
9897
|
+
const owner = currentOwner;
|
|
9898
|
+
let localOwnerDesc = ownsExecution ? "this terminal" : describeExecOwner(owner);
|
|
9899
|
+
if (!remote && !ownsExecution && owner?.client_kind === "daemon") {
|
|
9900
|
+
const selfRec = await loadMachine(api, session.identity.machine_id).catch(() => null);
|
|
9901
|
+
if (selfRec?.daemon?.version) localOwnerDesc = `the daemon on this machine (v${selfRec.daemon.version})`;
|
|
9902
|
+
}
|
|
9903
|
+
const execLine = remote ? `${c4.gray}tool execution:${c4.reset} daemon${remoteDaemonV ? ` v${remoteDaemonV}` : ""} on ${runnerName}` : `${c4.gray}tool execution:${c4.reset} ${localOwnerDesc}`;
|
|
9904
|
+
tui.setAgentLabel(agentTitle);
|
|
9323
9905
|
tui.banner(
|
|
9324
9906
|
remote ? [
|
|
9325
|
-
|
|
9326
|
-
`${
|
|
9327
|
-
`${
|
|
9907
|
+
header,
|
|
9908
|
+
`${c4.gray}project:${c4.reset} ${session.remotePath ?? "?"} ${c4.teal}on ${runnerName}${c4.reset}`,
|
|
9909
|
+
`${c4.gray}machine:${c4.reset} ${runnerName} ${c4.gray}thread:${c4.reset} ${threadId.slice(0, 8)}`,
|
|
9910
|
+
execLine
|
|
9328
9911
|
] : [
|
|
9329
|
-
|
|
9330
|
-
`${
|
|
9331
|
-
`${
|
|
9912
|
+
header,
|
|
9913
|
+
`${c4.gray}project:${c4.reset} ${projectDir}`,
|
|
9914
|
+
`${c4.gray}machine:${c4.reset} ${machine} ${c4.gray}thread:${c4.reset} ${threadId.slice(0, 8)}`,
|
|
9915
|
+
execLine
|
|
9332
9916
|
]
|
|
9333
9917
|
);
|
|
9334
9918
|
if (!remote && session.suggestDaemonInstall && process.platform !== "win32" && !serviceStatus().installed) {
|
|
9335
9919
|
tui.print(
|
|
9336
|
-
`${
|
|
9920
|
+
`${c4.dim}Tip: install the always-on daemon (${c4.reset}standardcode daemon install${c4.dim}) to start sessions on this machine from anywhere.${c4.reset}`
|
|
9337
9921
|
);
|
|
9338
9922
|
}
|
|
9339
9923
|
if (resumed) await printHistory(api, threadId, tui);
|
|
@@ -9344,17 +9928,17 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9344
9928
|
const runningProcs = (await registry.list()).filter((p) => p.status === "running");
|
|
9345
9929
|
if (runningProcs.length) {
|
|
9346
9930
|
tui.print(
|
|
9347
|
-
`${
|
|
9931
|
+
`${c4.cyan}\u2699 ${runningProcs.length} background process${runningProcs.length === 1 ? "" : "es"} running:${c4.reset}`
|
|
9348
9932
|
);
|
|
9349
|
-
for (const p of runningProcs) tui.print(`${
|
|
9933
|
+
for (const p of runningProcs) tui.print(`${c4.gray} ${p.id} ${p.description || p.command}${c4.reset}`);
|
|
9350
9934
|
}
|
|
9351
9935
|
refreshBgCount();
|
|
9352
9936
|
if (exec && ownsExecution) {
|
|
9353
9937
|
for (const res of await exec.connectEnabledMcpServers()) {
|
|
9354
9938
|
if (res.ok) {
|
|
9355
|
-
tui.print(`${
|
|
9939
|
+
tui.print(`${c4.cyan}\u26A1 MCP "${res.name}" connected${c4.reset} ${c4.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c4.reset}`);
|
|
9356
9940
|
} else {
|
|
9357
|
-
tui.print(`${
|
|
9941
|
+
tui.print(`${c4.red}\u26A0 MCP "${res.name}" failed:${c4.reset} ${c4.gray}${res.error}${c4.reset}`);
|
|
9358
9942
|
}
|
|
9359
9943
|
}
|
|
9360
9944
|
}
|
|
@@ -9370,8 +9954,8 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
9370
9954
|
try {
|
|
9371
9955
|
const { choice, reason } = await tui.approval(
|
|
9372
9956
|
`${request.summary}${request.permission ? `
|
|
9373
|
-
${
|
|
9374
|
-
${
|
|
9957
|
+
${c4.bold}why: ${request.permission}${c4.reset}` : ""}
|
|
9958
|
+
${c4.dim}runs on ${request.machine || runnerName}${c4.reset}`,
|
|
9375
9959
|
request.risk
|
|
9376
9960
|
);
|
|
9377
9961
|
answeredApprovals.add(request.tool_call_id);
|
|
@@ -9422,7 +10006,7 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
9422
10006
|
continue;
|
|
9423
10007
|
}
|
|
9424
10008
|
if (m.role === "assistant" && text) printAssistant(tui, text);
|
|
9425
|
-
else if (m.role === "system" && text) tui.print(`${
|
|
10009
|
+
else if (m.role === "system" && text) tui.print(`${c4.dim}${text}${c4.reset}`);
|
|
9426
10010
|
else if (m.role === "user" && text) {
|
|
9427
10011
|
const pending = pendingSent.get(text) ?? 0;
|
|
9428
10012
|
if (pending > 0) {
|
|
@@ -9444,7 +10028,8 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
9444
10028
|
activeSteps.clear();
|
|
9445
10029
|
refreshStatus();
|
|
9446
10030
|
}
|
|
9447
|
-
|
|
10031
|
+
if (polledBusy) optimisticBusyUntil = 0;
|
|
10032
|
+
busy = polledBusy || Date.now() < optimisticBusyUntil;
|
|
9448
10033
|
tui.setWorking(busy);
|
|
9449
10034
|
if (!busy) {
|
|
9450
10035
|
if (activeSteps.size) activeSteps.clear();
|
|
@@ -9495,7 +10080,7 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
9495
10080
|
clearInterval(pollTimer);
|
|
9496
10081
|
clearInterval(heartbeatPoll);
|
|
9497
10082
|
process.off("SIGCONT", onTerminalResume);
|
|
9498
|
-
const stopped = bridge?.isOwner ?? false ? api.
|
|
10083
|
+
const stopped = bridge?.isOwner ?? false ? api.stopThread(threadId).catch(() => {
|
|
9499
10084
|
}) : Promise.resolve();
|
|
9500
10085
|
const procsStopped = host ? host.stopAllLocalProcesses().catch(() => 0) : Promise.resolve(0);
|
|
9501
10086
|
bridge?.close();
|
|
@@ -9515,16 +10100,16 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
9515
10100
|
tui.setStep(null, 0);
|
|
9516
10101
|
tui.setBackgroundCount(0);
|
|
9517
10102
|
if (killed > 0) {
|
|
9518
|
-
tui.print(`${
|
|
10103
|
+
tui.print(`${c4.cyan}\u2699${c4.reset} Stopped ${killed} background process${killed === 1 ? "" : "es"}.`);
|
|
9519
10104
|
}
|
|
9520
|
-
tui.print(`${
|
|
10105
|
+
tui.print(`${c4.dim}\u2500\u2500 conversation cleared \u2014 starting a fresh session \u2500\u2500${c4.reset}`);
|
|
9521
10106
|
}
|
|
9522
10107
|
async function runSkillsMenu(tui, skills) {
|
|
9523
10108
|
let list;
|
|
9524
10109
|
try {
|
|
9525
10110
|
list = await skills.list();
|
|
9526
10111
|
} catch (e) {
|
|
9527
|
-
tui.print(`${
|
|
10112
|
+
tui.print(`${c4.red}\u2717 couldn't load skills:${c4.reset} ${c4.gray}${e instanceof Error ? e.message : String(e)}${c4.reset}`);
|
|
9528
10113
|
return;
|
|
9529
10114
|
}
|
|
9530
10115
|
const INSTALL = "__install__";
|
|
@@ -9535,7 +10120,7 @@ async function runSkillsMenu(tui, skills) {
|
|
|
9535
10120
|
}));
|
|
9536
10121
|
items.push({ label: "\uFF0B Install a skill\u2026", hint: "find & install", value: INSTALL });
|
|
9537
10122
|
const picked = await tui.select(
|
|
9538
|
-
`${
|
|
10123
|
+
`${c4.bold}Agent skills${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c4.reset}`,
|
|
9539
10124
|
items
|
|
9540
10125
|
);
|
|
9541
10126
|
if (!picked) return;
|
|
@@ -9548,8 +10133,8 @@ async function runSkillsMenu(tui, skills) {
|
|
|
9548
10133
|
return;
|
|
9549
10134
|
}
|
|
9550
10135
|
const skill = list.find((s) => s.name === picked);
|
|
9551
|
-
tui.print(`${
|
|
9552
|
-
const action = await tui.select(`${
|
|
10136
|
+
tui.print(`${c4.cyan}${skill.name}${c4.reset}${skill.version ? ` ${c4.dim}v${skill.version}${c4.reset}` : ""} ${c4.gray}\u2014 ${skill.description}${c4.reset}`);
|
|
10137
|
+
const action = await tui.select(`${c4.bold}${picked}${c4.reset}`, [
|
|
9553
10138
|
skill.enabled ? { label: "Disable (hide from the agent)", value: "disable" } : { label: "Enable", value: "enable" },
|
|
9554
10139
|
{ label: "View files", value: "files" },
|
|
9555
10140
|
{ label: "Remove this skill", value: "remove" },
|
|
@@ -9558,20 +10143,20 @@ async function runSkillsMenu(tui, skills) {
|
|
|
9558
10143
|
try {
|
|
9559
10144
|
if (action === "enable" || action === "disable") {
|
|
9560
10145
|
await skills.setEnabled(picked, action === "enable");
|
|
9561
|
-
tui.print(`${
|
|
10146
|
+
tui.print(`${c4.gray}${action}d ${picked}${c4.reset}`);
|
|
9562
10147
|
} else if (action === "files") {
|
|
9563
|
-
for (const f of skill.files) tui.print(` ${
|
|
10148
|
+
for (const f of skill.files) tui.print(` ${c4.gray}${f}${c4.reset}`);
|
|
9564
10149
|
} else if (action === "remove") {
|
|
9565
10150
|
await skills.remove(picked);
|
|
9566
|
-
tui.print(`${
|
|
10151
|
+
tui.print(`${c4.gray}removed ${picked}${c4.reset}`);
|
|
9567
10152
|
}
|
|
9568
10153
|
} catch (e) {
|
|
9569
|
-
tui.print(`${
|
|
10154
|
+
tui.print(`${c4.red}\u2717 ${e instanceof Error ? e.message : String(e)}${c4.reset}`);
|
|
9570
10155
|
}
|
|
9571
10156
|
}
|
|
9572
10157
|
async function runLevelMenu(tui, perm) {
|
|
9573
10158
|
const picked = await tui.select(
|
|
9574
|
-
`${
|
|
10159
|
+
`${c4.bold}Auto-accept level${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter \xB7 shift-tab cycles)${c4.reset}`,
|
|
9575
10160
|
LEVELS.map((l) => ({
|
|
9576
10161
|
label: levelLabel(l),
|
|
9577
10162
|
hint: l === tui.level ? "current" : "",
|
|
@@ -9588,16 +10173,16 @@ async function runMachinesMenu(tui, api, self) {
|
|
|
9588
10173
|
try {
|
|
9589
10174
|
machines = await loadMachines(api);
|
|
9590
10175
|
} catch (e) {
|
|
9591
|
-
tui.print(`${
|
|
10176
|
+
tui.print(`${c4.red}\u2717 couldn't load machines:${c4.reset} ${c4.gray}${e instanceof Error ? e.message : String(e)}${c4.reset}`);
|
|
9592
10177
|
return;
|
|
9593
10178
|
}
|
|
9594
10179
|
if (!machines.length) {
|
|
9595
|
-
tui.print(`${
|
|
10180
|
+
tui.print(`${c4.gray}No machines registered yet. Run standardcode on a machine (or install its daemon) to register it.${c4.reset}`);
|
|
9596
10181
|
return;
|
|
9597
10182
|
}
|
|
9598
10183
|
machines.sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0));
|
|
9599
10184
|
const picked = await tui.select(
|
|
9600
|
-
`${
|
|
10185
|
+
`${c4.bold}Your machines${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter \xB7 esc)${c4.reset}`,
|
|
9601
10186
|
machines.map((m) => {
|
|
9602
10187
|
const isSelf = m.id === self.machine_id;
|
|
9603
10188
|
const online = daemonOnline(m);
|
|
@@ -9631,26 +10216,26 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
9631
10216
|
options.push({ label: "Back", value: "back" });
|
|
9632
10217
|
if (!isSelf && !machine.daemon) {
|
|
9633
10218
|
tui.print(
|
|
9634
|
-
`${
|
|
10219
|
+
`${c4.dim}${machine.name} has no daemon \u2014 you can rename it here; update and project changes need its daemon installed.${c4.reset}`
|
|
9635
10220
|
);
|
|
9636
10221
|
} else if (!isSelf && machine.daemon && !online) {
|
|
9637
10222
|
tui.print(
|
|
9638
|
-
`${
|
|
10223
|
+
`${c4.dim}${machine.name}'s daemon is offline \u2014 queued changes apply when it next comes online.${c4.reset}`
|
|
9639
10224
|
);
|
|
9640
10225
|
}
|
|
9641
|
-
const action = await tui.select(`${
|
|
10226
|
+
const action = await tui.select(`${c4.bold}${machineIcon(machine)} ${machine.name}${c4.reset}`, options);
|
|
9642
10227
|
if (!action || action === "back") return;
|
|
9643
10228
|
if (action === "icon") {
|
|
9644
10229
|
const current = machine.icon ?? "";
|
|
9645
10230
|
const emoji = await tui.prompt(
|
|
9646
|
-
`${
|
|
10231
|
+
`${c4.bold}Icon for ${machine.name}${c4.reset} ${c4.dim}(paste an emoji, blank to reset)${c4.reset}`,
|
|
9647
10232
|
current
|
|
9648
10233
|
);
|
|
9649
10234
|
if (emoji !== null) {
|
|
9650
10235
|
const trimmed = emoji.trim();
|
|
9651
10236
|
await setMachineIcon(api, machine.id, trimmed);
|
|
9652
10237
|
machine.icon = trimmed || void 0;
|
|
9653
|
-
tui.print(`${
|
|
10238
|
+
tui.print(`${c4.green}\u2713${c4.reset} icon ${trimmed ? `set to ${trimmed}` : "reset"} for ${machine.name}`);
|
|
9654
10239
|
}
|
|
9655
10240
|
return manageMachine(tui, api, self, machine);
|
|
9656
10241
|
}
|
|
@@ -9666,7 +10251,7 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
9666
10251
|
const name = await tui.prompt(`New name for ${machine.name}`, machine.name);
|
|
9667
10252
|
if (name === null || !name.trim()) return;
|
|
9668
10253
|
await setMachineName(api, machine.id, name.trim());
|
|
9669
|
-
tui.print(`${
|
|
10254
|
+
tui.print(`${c4.green}\u2713${c4.reset} Renamed ${c4.bold}${machine.name}${c4.reset} \u2192 ${c4.bold}${name.trim()}${c4.reset}.`);
|
|
9670
10255
|
} else if (action === "update") {
|
|
9671
10256
|
if (isSelf) {
|
|
9672
10257
|
await runUpdateCommand(tui);
|
|
@@ -9677,7 +10262,7 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
9677
10262
|
]);
|
|
9678
10263
|
if (go !== "yes") return;
|
|
9679
10264
|
await dispatch("update");
|
|
9680
|
-
tui.print(`${
|
|
10265
|
+
tui.print(`${c4.green}\u2713${c4.reset} Update ${c4.gray}${applyNote} (its daemon updates and restarts on the new version).${c4.reset}`);
|
|
9681
10266
|
}
|
|
9682
10267
|
} else if (action === "projects") {
|
|
9683
10268
|
await manageMachineProjects(tui, api, self, machine, dispatch, applyNote);
|
|
@@ -9686,78 +10271,103 @@ async function manageMachine(tui, api, self, machine) {
|
|
|
9686
10271
|
async function manageMachineProjects(tui, api, self, machine, dispatch, applyNote) {
|
|
9687
10272
|
const ADD = "__add__";
|
|
9688
10273
|
const paths = Object.keys(machine.projects).sort();
|
|
10274
|
+
const projLabels = projectDisplayLabels(machine.projects);
|
|
9689
10275
|
const picked = await tui.select(
|
|
9690
|
-
`${
|
|
10276
|
+
`${c4.bold}Projects on ${machine.name}${c4.reset} ${c4.dim}(\u2191\u2193 \xB7 enter \xB7 esc)${c4.reset}`,
|
|
9691
10277
|
[
|
|
9692
|
-
...paths.map((p) => ({
|
|
10278
|
+
...paths.map((p) => ({
|
|
10279
|
+
label: projLabels.get(p) ?? projectDisplayName(p, machine.projects[p]),
|
|
10280
|
+
detail: p,
|
|
10281
|
+
value: p
|
|
10282
|
+
})),
|
|
9693
10283
|
{ label: "\uFF0B Add a project directory\u2026", hint: "absolute path", value: ADD }
|
|
9694
|
-
]
|
|
10284
|
+
],
|
|
10285
|
+
{ spaced: true }
|
|
9695
10286
|
);
|
|
9696
10287
|
if (!picked) return;
|
|
9697
10288
|
if (picked === ADD) {
|
|
9698
|
-
const
|
|
9699
|
-
|
|
9700
|
-
|
|
10289
|
+
const isSelf = machine.id === self.machine_id;
|
|
10290
|
+
const backend = isSelf ? localBrowseBackend(machine.name) : remoteBrowseBackend(api, machine.id, machine.name);
|
|
10291
|
+
const chosen = await pickDirectory(tui, backend, {
|
|
10292
|
+
startPath: isSelf ? process.cwd() : null,
|
|
10293
|
+
loader: startLoader
|
|
10294
|
+
});
|
|
10295
|
+
if (!chosen) return;
|
|
10296
|
+
await dispatch("add_project", { path: chosen });
|
|
10297
|
+
tui.print(`${c4.green}\u2713${c4.reset} Add ${chosen} ${c4.gray}${applyNote}.${c4.reset}`);
|
|
10298
|
+
return;
|
|
10299
|
+
}
|
|
10300
|
+
const project = machine.projects[picked];
|
|
10301
|
+
const displayName = projectDisplayName(picked, project);
|
|
10302
|
+
const action = await tui.select(`${c4.bold}${displayName}${c4.reset} ${c4.dim}${shortenPath(picked, 48)}${c4.reset}`, [
|
|
10303
|
+
{ label: "Rename", hint: "display name only \u2014 the directory is untouched", value: "rename" },
|
|
10304
|
+
{ label: "Remove from this machine's projects", hint: "doesn't delete the directory", value: "remove" },
|
|
10305
|
+
{ label: "Back", value: "back" }
|
|
10306
|
+
]);
|
|
10307
|
+
if (!action || action === "back") return;
|
|
10308
|
+
if (action === "rename") {
|
|
10309
|
+
const name = await tui.prompt(
|
|
10310
|
+
`New name for ${displayName} (blank resets to the directory name)`,
|
|
10311
|
+
displayName
|
|
9701
10312
|
);
|
|
9702
|
-
if (
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
}
|
|
9708
|
-
await dispatch("add_project", { path: trimmed });
|
|
9709
|
-
tui.print(`${c3.green}\u2713${c3.reset} Add ${trimmed} ${c3.gray}${applyNote}.${c3.reset}`);
|
|
10313
|
+
if (name === null) return;
|
|
10314
|
+
await setProjectName(api, machine.id, picked, name);
|
|
10315
|
+
const now = name.trim() || projectDisplayName(picked, null);
|
|
10316
|
+
if (project) project.name = now;
|
|
10317
|
+
tui.print(`${c4.green}\u2713${c4.reset} Renamed ${c4.bold}${displayName}${c4.reset} \u2192 ${c4.bold}${now}${c4.reset}.`);
|
|
9710
10318
|
} else {
|
|
9711
10319
|
await dispatch("remove_project", { path: picked });
|
|
9712
|
-
tui.print(`${
|
|
10320
|
+
tui.print(`${c4.green}\u2713${c4.reset} Remove ${picked} ${c4.gray}${applyNote}.${c4.reset}`);
|
|
9713
10321
|
}
|
|
9714
10322
|
}
|
|
9715
10323
|
function showDaemonInfo(tui, session) {
|
|
9716
10324
|
if (session.mode === "remote" && session.runner) {
|
|
9717
10325
|
tui.print(
|
|
9718
|
-
`${
|
|
10326
|
+
`${c4.gray}This session runs on${c4.reset} ${c4.bold}${session.runner.name}${c4.reset} ${c4.gray}(${session.runner.hostname}) \u2014 its daemon executes the tools.${c4.reset}`
|
|
9719
10327
|
);
|
|
9720
10328
|
} else {
|
|
9721
|
-
tui.print(`${
|
|
10329
|
+
tui.print(`${c4.gray}This session runs on this machine.${c4.reset}`);
|
|
9722
10330
|
}
|
|
9723
10331
|
const status = serviceStatus();
|
|
9724
10332
|
tui.print(
|
|
9725
|
-
`${
|
|
10333
|
+
`${c4.gray}Daemon on this machine:${c4.reset} ${status.installed ? status.detail : "not installed"}`
|
|
9726
10334
|
);
|
|
9727
10335
|
if (!status.installed) {
|
|
9728
10336
|
tui.print(
|
|
9729
|
-
`${
|
|
10337
|
+
`${c4.gray}Install it to start sessions on this machine from anywhere:${c4.reset} ${c4.bold}standardcode daemon install${c4.reset}`
|
|
9730
10338
|
);
|
|
9731
10339
|
tui.print(
|
|
9732
|
-
`${
|
|
10340
|
+
`${c4.dim}The daemon keeps running after you close the terminal \u2014 it self-restarts, self-updates, and executes sessions you start from other machines.${c4.reset}`
|
|
9733
10341
|
);
|
|
9734
10342
|
} else {
|
|
9735
|
-
tui.print(`${
|
|
10343
|
+
tui.print(`${c4.dim}Manage it with standardcode daemon status | uninstall | add-project <path>.${c4.reset}`);
|
|
9736
10344
|
}
|
|
9737
10345
|
}
|
|
9738
10346
|
function showKeybindings(tui) {
|
|
9739
|
-
tui.print(`${
|
|
9740
|
-
tui.print(`${
|
|
9741
|
-
tui.print(`${
|
|
9742
|
-
tui.print(`${
|
|
9743
|
-
tui.print(`${
|
|
9744
|
-
tui.print(`${
|
|
9745
|
-
tui.print(`${
|
|
9746
|
-
tui.print(`${
|
|
10347
|
+
tui.print(`${c4.gray}shortcuts:${c4.reset}`);
|
|
10348
|
+
tui.print(`${c4.gray} shift-tab${c4.reset} cycle auto-accept level (1\u20135)`);
|
|
10349
|
+
tui.print(`${c4.gray} shift-\u23CE${c4.reset} insert a newline (multiline input)`);
|
|
10350
|
+
tui.print(`${c4.gray} option-\u23CE${c4.reset} steer now \u2014 stops the current step and plays your message`);
|
|
10351
|
+
tui.print(`${c4.gray} /${c4.reset} open the command palette (type to filter)`);
|
|
10352
|
+
tui.print(`${c4.gray} !cmd${c4.reset} run a shell command on the session's machine (e.g. !ls); !! to send a literal !`);
|
|
10353
|
+
tui.print(`${c4.gray} ctrl-v${c4.reset} paste an image from the clipboard ([#Image 1])`);
|
|
10354
|
+
tui.print(`${c4.gray} \u2191 / \u2193${c4.reset} cycle past messages (on the input's top line)`);
|
|
10355
|
+
tui.print(`${c4.gray} \u2190${c4.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);
|
|
10356
|
+
tui.print(`${c4.gray} ctrl-c${c4.reset} quit`);
|
|
9747
10357
|
}
|
|
9748
10358
|
async function runUpdateCommand(tui) {
|
|
9749
10359
|
const version = readVersion();
|
|
9750
10360
|
const result = await forceCheckForUpdate(version);
|
|
9751
10361
|
if (!result) {
|
|
9752
|
-
tui.print(`${
|
|
10362
|
+
tui.print(`${c4.green}\u2713${c4.reset} ${c4.gray}@standardagents/code${c4.reset} is up to date (v${version})`);
|
|
9753
10363
|
return;
|
|
9754
10364
|
}
|
|
9755
10365
|
const { latest } = result;
|
|
9756
10366
|
tui.print(`
|
|
9757
|
-
${
|
|
10367
|
+
${c4.yellow}\u27F3${c4.reset} Update available: ${c4.gray}v${version}${c4.reset} \u2192 ${c4.green}v${latest}${c4.reset}`);
|
|
9758
10368
|
const pm = detectPackageManager();
|
|
9759
10369
|
if (!pm) {
|
|
9760
|
-
tui.print(` ${
|
|
10370
|
+
tui.print(` ${c4.gray}This is a source checkout \u2014 pull the repo to update.${c4.reset}`);
|
|
9761
10371
|
return;
|
|
9762
10372
|
}
|
|
9763
10373
|
const { display } = updateCommand(pm);
|
|
@@ -9766,28 +10376,28 @@ async function runUpdateCommand(tui) {
|
|
|
9766
10376
|
{ label: "No, skip", value: "no" }
|
|
9767
10377
|
]);
|
|
9768
10378
|
if (choice === "yes") {
|
|
9769
|
-
tui.print(` ${
|
|
10379
|
+
tui.print(` ${c4.gray}Running ${display}\u2026${c4.reset}`);
|
|
9770
10380
|
const { ok, output: pmOutput } = await runUpdate(pm);
|
|
9771
10381
|
if (ok) {
|
|
9772
|
-
tui.print(` ${
|
|
10382
|
+
tui.print(` ${c4.green}\u2713${c4.reset} Updated to v${latest}. Restart to use the new version.`);
|
|
9773
10383
|
} else {
|
|
9774
|
-
tui.print(` ${
|
|
10384
|
+
tui.print(` ${c4.red}\u2717${c4.reset} Update failed:`);
|
|
9775
10385
|
for (const line of pmOutput.trim().split("\n").slice(-6)) {
|
|
9776
|
-
tui.print(` ${
|
|
10386
|
+
tui.print(` ${c4.dim}${line}${c4.reset}`);
|
|
9777
10387
|
}
|
|
9778
10388
|
}
|
|
9779
10389
|
} else {
|
|
9780
|
-
tui.print(` ${
|
|
10390
|
+
tui.print(` ${c4.gray}Skipped. Run /update later.${c4.reset}`);
|
|
9781
10391
|
}
|
|
9782
10392
|
}
|
|
9783
10393
|
async function runProcessMenu(tui, bg) {
|
|
9784
10394
|
const procs = await bg.list();
|
|
9785
10395
|
if (!procs.length) {
|
|
9786
|
-
tui.print(`${
|
|
10396
|
+
tui.print(`${c4.gray}No background processes for this session.${c4.reset}`);
|
|
9787
10397
|
return;
|
|
9788
10398
|
}
|
|
9789
10399
|
const items = procs.map((p) => {
|
|
9790
|
-
const status = p.status === "running" ? `${
|
|
10400
|
+
const status = p.status === "running" ? `${c4.green}running${c4.reset}` : `${c4.gray}${p.status}${typeof p.exitCode === "number" ? ` (exit ${p.exitCode})` : ""}${c4.reset}`;
|
|
9791
10401
|
return {
|
|
9792
10402
|
label: `${p.description || p.command}`,
|
|
9793
10403
|
hint: `${p.id} \xB7 ${status}`,
|
|
@@ -9795,22 +10405,22 @@ async function runProcessMenu(tui, bg) {
|
|
|
9795
10405
|
};
|
|
9796
10406
|
});
|
|
9797
10407
|
const picked = await tui.select(
|
|
9798
|
-
`${
|
|
10408
|
+
`${c4.bold}Background processes${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter to manage \xB7 esc to close)${c4.reset}`,
|
|
9799
10409
|
items
|
|
9800
10410
|
);
|
|
9801
10411
|
if (!picked) return;
|
|
9802
10412
|
const proc = procs.find((p) => p.id === picked);
|
|
9803
10413
|
if (!proc || proc.status !== "running") {
|
|
9804
|
-
tui.print(`${
|
|
10414
|
+
tui.print(`${c4.gray}${picked} is not running.${c4.reset}`);
|
|
9805
10415
|
return;
|
|
9806
10416
|
}
|
|
9807
|
-
const action = await tui.select(`${
|
|
10417
|
+
const action = await tui.select(`${c4.bold}${proc.description || proc.command}${c4.reset}`, [
|
|
9808
10418
|
{ label: "Stop this process", value: "stop" },
|
|
9809
10419
|
{ label: "Leave it running", value: "leave" }
|
|
9810
10420
|
]);
|
|
9811
10421
|
if (action === "stop") {
|
|
9812
10422
|
await bg.stop(picked);
|
|
9813
|
-
tui.print(`${
|
|
10423
|
+
tui.print(`${c4.gray}stopped ${picked}${c4.reset}`);
|
|
9814
10424
|
}
|
|
9815
10425
|
}
|
|
9816
10426
|
async function runApprovalsMenu(tui, perm, save) {
|
|
@@ -9818,7 +10428,7 @@ async function runApprovalsMenu(tui, perm, save) {
|
|
|
9818
10428
|
const risks = Array.from(perm.allowRisk).sort((a, b) => a - b);
|
|
9819
10429
|
if (!tools.length && !risks.length) {
|
|
9820
10430
|
tui.print(
|
|
9821
|
-
`${
|
|
10431
|
+
`${c4.gray}No pre-approved commands. At a permission prompt, choose "Always allow this tool" or "Allow all level N" to add some.${c4.reset}`
|
|
9822
10432
|
);
|
|
9823
10433
|
return;
|
|
9824
10434
|
}
|
|
@@ -9828,22 +10438,22 @@ async function runApprovalsMenu(tui, perm, save) {
|
|
|
9828
10438
|
{ label: "Clear all approvals", hint: "", value: "clear" }
|
|
9829
10439
|
];
|
|
9830
10440
|
const picked = await tui.select(
|
|
9831
|
-
`${
|
|
10441
|
+
`${c4.bold}Approved commands${c4.reset} ${c4.dim}(enter to revoke \xB7 esc to close)${c4.reset}`,
|
|
9832
10442
|
items
|
|
9833
10443
|
);
|
|
9834
10444
|
if (!picked) return;
|
|
9835
10445
|
if (picked === "clear") {
|
|
9836
10446
|
perm.alwaysAllow.clear();
|
|
9837
10447
|
perm.allowRisk.clear();
|
|
9838
|
-
tui.print(`${
|
|
10448
|
+
tui.print(`${c4.gray}cleared all approvals${c4.reset}`);
|
|
9839
10449
|
} else if (picked.startsWith("tool:")) {
|
|
9840
10450
|
const t = picked.slice(5);
|
|
9841
10451
|
perm.alwaysAllow.delete(t);
|
|
9842
|
-
tui.print(`${
|
|
10452
|
+
tui.print(`${c4.gray}revoked tool ${t}${c4.reset}`);
|
|
9843
10453
|
} else if (picked.startsWith("risk:")) {
|
|
9844
10454
|
const r = Number(picked.slice(5));
|
|
9845
10455
|
perm.allowRisk.delete(r);
|
|
9846
|
-
tui.print(`${
|
|
10456
|
+
tui.print(`${c4.gray}revoked level ${r}${c4.reset}`);
|
|
9847
10457
|
}
|
|
9848
10458
|
save();
|
|
9849
10459
|
}
|
|
@@ -9861,7 +10471,7 @@ async function runMcpMenu(tui, mcp) {
|
|
|
9861
10471
|
items.push({ label: "\uFF0B Install a new MCP server\u2026", hint: "find & install", value: INSTALL });
|
|
9862
10472
|
items.push({ label: "Add manually (name: command)\u2026", hint: "advanced", value: ADD_MANUAL });
|
|
9863
10473
|
const picked = await tui.select(
|
|
9864
|
-
`${
|
|
10474
|
+
`${c4.bold}MCP servers${c4.reset} ${c4.dim}(\u2191/\u2193 \xB7 enter \xB7 esc to close)${c4.reset}`,
|
|
9865
10475
|
items
|
|
9866
10476
|
);
|
|
9867
10477
|
if (!picked) return;
|
|
@@ -9875,7 +10485,7 @@ async function runMcpMenu(tui, mcp) {
|
|
|
9875
10485
|
}
|
|
9876
10486
|
const server = configured.find((s) => s.name === picked);
|
|
9877
10487
|
const isConnected = connected.has(picked);
|
|
9878
|
-
const action = await tui.select(`${
|
|
10488
|
+
const action = await tui.select(`${c4.bold}${picked}${c4.reset}`, [
|
|
9879
10489
|
{ label: "View tools", value: "tools" },
|
|
9880
10490
|
isConnected ? { label: "Disconnect", value: "disconnect" } : { label: "Connect", value: "connect" },
|
|
9881
10491
|
server.enabled ? { label: "Disable (don't auto-connect)", value: "disable" } : { label: "Enable (auto-connect on start)", value: "enable" },
|
|
@@ -9885,29 +10495,29 @@ async function runMcpMenu(tui, mcp) {
|
|
|
9885
10495
|
if (action === "tools") {
|
|
9886
10496
|
const entry = mcp.catalog().servers.find((e) => e.name === picked);
|
|
9887
10497
|
if (!entry || entry.status !== "connected") {
|
|
9888
|
-
tui.print(`${
|
|
10498
|
+
tui.print(`${c4.gray}${picked} is not connected \u2014 connect it to list tools.${c4.reset}`);
|
|
9889
10499
|
return;
|
|
9890
10500
|
}
|
|
9891
|
-
if (!entry.tools.length) tui.print(`${
|
|
9892
|
-
for (const t of entry.tools) tui.print(` ${
|
|
9893
|
-
if (entry.resources.length) tui.print(` ${
|
|
10501
|
+
if (!entry.tools.length) tui.print(`${c4.gray}${picked} exposes no tools.${c4.reset}`);
|
|
10502
|
+
for (const t of entry.tools) tui.print(` ${c4.cyan}${t.name}${c4.reset}${t.description ? ` ${c4.gray}\u2014 ${t.description}${c4.reset}` : ""}`);
|
|
10503
|
+
if (entry.resources.length) tui.print(` ${c4.gray}${entry.resources.length} resource(s)${c4.reset}`);
|
|
9894
10504
|
} else if (action === "connect") {
|
|
9895
10505
|
const res = await mcp.connect(server);
|
|
9896
|
-
tui.print(res.ok ? `${
|
|
10506
|
+
tui.print(res.ok ? `${c4.cyan}\u26A1 connected (${res.tools} tools)${c4.reset}` : `${c4.red}\u26A0 ${res.error}${c4.reset}`);
|
|
9897
10507
|
} else if (action === "disconnect") {
|
|
9898
10508
|
mcp.disconnect(picked);
|
|
9899
|
-
tui.print(`${
|
|
10509
|
+
tui.print(`${c4.gray}disconnected ${picked}${c4.reset}`);
|
|
9900
10510
|
} else if (action === "enable") {
|
|
9901
10511
|
mcp.setEnabled(picked, true);
|
|
9902
10512
|
const res = await mcp.connect(server);
|
|
9903
|
-
tui.print(res.ok ? `${
|
|
10513
|
+
tui.print(res.ok ? `${c4.cyan}\u26A1 enabled + connected (${res.tools} tools)${c4.reset}` : `${c4.red}\u26A0 enabled but failed: ${res.error}${c4.reset}`);
|
|
9904
10514
|
} else if (action === "disable") {
|
|
9905
10515
|
mcp.setEnabled(picked, false);
|
|
9906
10516
|
mcp.disconnect(picked);
|
|
9907
|
-
tui.print(`${
|
|
10517
|
+
tui.print(`${c4.gray}disabled + disconnected ${picked}${c4.reset}`);
|
|
9908
10518
|
} else if (action === "remove") {
|
|
9909
10519
|
mcp.remove(picked);
|
|
9910
|
-
tui.print(`${
|
|
10520
|
+
tui.print(`${c4.gray}removed ${picked}${c4.reset}`);
|
|
9911
10521
|
}
|
|
9912
10522
|
}
|
|
9913
10523
|
async function addMcpServer(tui, mcp) {
|
|
@@ -9918,13 +10528,13 @@ async function addMcpServer(tui, mcp) {
|
|
|
9918
10528
|
if (!spec) return;
|
|
9919
10529
|
const cfg = parseServerSpec(spec);
|
|
9920
10530
|
if (!cfg) {
|
|
9921
|
-
tui.print(`${
|
|
10531
|
+
tui.print(`${c4.yellow}couldn't parse that. Use name: command [args]${c4.reset}`);
|
|
9922
10532
|
return;
|
|
9923
10533
|
}
|
|
9924
|
-
tui.print(`${
|
|
10534
|
+
tui.print(`${c4.gray}connecting MCP "${cfg.name}" (${cfg.command} ${cfg.args.join(" ")})\u2026${c4.reset}`);
|
|
9925
10535
|
const res = await mcp.add(cfg);
|
|
9926
|
-
if (res.ok) tui.print(`${
|
|
9927
|
-
else tui.print(`${
|
|
10536
|
+
if (res.ok) tui.print(`${c4.cyan}\u26A1 MCP "${cfg.name}" connected${c4.reset} ${c4.gray}(${res.tools} tool${res.tools === 1 ? "" : "s"})${c4.reset}`);
|
|
10537
|
+
else tui.print(`${c4.red}\u26A0 MCP "${cfg.name}" failed:${c4.reset} ${c4.gray}${res.error}${c4.reset} ${c4.dim}(saved; retry from the MCP menu)${c4.reset}`);
|
|
9928
10538
|
}
|
|
9929
10539
|
async function installMcpServerFlow(tui, mcp) {
|
|
9930
10540
|
const query = await tui.prompt(
|