@vascend/talos 0.1.3 → 0.1.5
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/LICENSE +22 -0
- package/README.md +49 -13
- package/dist/core/agent.js +58 -38
- package/dist/core/session-naming.d.ts +3 -0
- package/dist/core/session-naming.js +43 -0
- package/dist/core/session-usage.js +4 -1
- package/dist/core/session.d.ts +1 -0
- package/dist/core/session.js +9 -3
- package/dist/core/storage-health.d.ts +24 -0
- package/dist/core/storage-health.js +72 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/tui/agent-sessions-screen.d.ts +25 -0
- package/dist/tui/agent-sessions-screen.js +132 -0
- package/dist/tui/agent-sessions.d.ts +39 -0
- package/dist/tui/agent-sessions.js +175 -0
- package/dist/tui/bottom-input.d.ts +11 -0
- package/dist/tui/bottom-input.js +106 -10
- package/dist/tui/commands.js +2 -1
- package/dist/tui/i18n-catalog.js +39 -1
- package/dist/tui/logo.js +3 -0
- package/dist/tui/model-picker.d.ts +2 -0
- package/dist/tui/model-picker.js +49 -0
- package/dist/tui/native-scrollback.d.ts +2 -1
- package/dist/tui/native-scrollback.js +26 -6
- package/dist/tui/prompt-queue-store.d.ts +1 -0
- package/dist/tui/prompt-queue-store.js +4 -0
- package/dist/tui/queued-questions.js +2 -4
- package/dist/tui/quick-help.d.ts +1 -1
- package/dist/tui/quick-help.js +3 -0
- package/dist/tui/repl.d.ts +3 -0
- package/dist/tui/repl.js +110 -24
- package/dist/tui/review-store.d.ts +7 -1
- package/dist/tui/review-store.js +36 -4
- package/dist/tui/scrollback-archive.d.ts +16 -0
- package/dist/tui/scrollback-archive.js +90 -0
- package/dist/tui/selector.d.ts +4 -0
- package/dist/tui/selector.js +9 -2
- package/dist/tui/session-naming-notice.d.ts +14 -0
- package/dist/tui/session-naming-notice.js +57 -0
- package/dist/tui/session-picker.d.ts +2 -0
- package/dist/tui/session-picker.js +32 -0
- package/dist/tui/storage-warning.d.ts +2 -0
- package/dist/tui/storage-warning.js +35 -0
- package/dist/vascend/context-feedback-loop.js +2 -0
- package/dist/vascend/context-snapshot.d.ts +1 -0
- package/dist/vascend/context-snapshot.js +4 -3
- package/dist/vascend/context-window-compiler.d.ts +1 -0
- package/dist/vascend/context-window-compiler.js +1 -0
- package/dist/vascend/granular-kingdom.js +32 -4
- package/dist/vascend/kingdom.js +4 -0
- package/dist/vascend/memory-store.d.ts +1 -0
- package/dist/vascend/memory-store.js +12 -4
- package/dist/vascend/room-memory.d.ts +3 -1
- package/dist/vascend/room-memory.js +6 -4
- package/package.json +2 -2
package/dist/tui/repl.js
CHANGED
|
@@ -9,6 +9,9 @@ import { createUserInbox, pendingText } from "../core/user-inbox.js";
|
|
|
9
9
|
import { renderDiff } from "./diff.js";
|
|
10
10
|
import { renderMarkdown, wrapPlain } from "./markdown.js";
|
|
11
11
|
import { SessionUsageState } from "./session-usage.js";
|
|
12
|
+
import { readSessionUsage } from "../core/session-usage.js";
|
|
13
|
+
import { storageHealth } from "../core/storage-health.js";
|
|
14
|
+
import { storageWarning } from "./storage-warning.js";
|
|
12
15
|
import { ContextMeter, contextDetails } from "./context-meter.js";
|
|
13
16
|
import { buildUsageFooter, subscriptionDetails, SESSION_USAGE_EXPLANATION } from "./usage-footer.js";
|
|
14
17
|
import { copyText, lastAssistantText } from "./clipboard-text.js";
|
|
@@ -76,6 +79,7 @@ import { VascendPlanStore } from "../vascend/plan-store.js";
|
|
|
76
79
|
import { reviewOptions, shouldReview, buildRoomMetricsDigest, buildReviewerPrompt } from "../vascend/reviewer.js";
|
|
77
80
|
import { Session, listSessions, sessionFilePath } from "../core/session.js";
|
|
78
81
|
import { sessionTitle } from "../core/session-title.js";
|
|
82
|
+
import { nameSession } from "../core/session-naming.js";
|
|
79
83
|
import { ToolRegistry } from "../tools/registry.js";
|
|
80
84
|
import { saveGlobalSettings } from "../core/settings.js";
|
|
81
85
|
import { autoApproves, nextPermMode, permModeLabel } from "./perm-mode.js";
|
|
@@ -324,6 +328,7 @@ export async function startRepl(deps) {
|
|
|
324
328
|
stats.restore(harness.config.sessionDir, harness.session.id, harness.session.tree().find(n => n.message.role === "assistant")?.ts);
|
|
325
329
|
const review = new ReviewStore();
|
|
326
330
|
review.reset(sessionConversation(harness.session));
|
|
331
|
+
review.sessions.restoreMain(readSessionUsage(harness.config.sessionDir, harness.session.id));
|
|
327
332
|
const reviewContext = () => {
|
|
328
333
|
const cost = costTracker.snapshot(harness.config.model);
|
|
329
334
|
const tasks = vascend.state().kingdom.castles.flatMap(c => c.tasks);
|
|
@@ -447,6 +452,7 @@ export async function startRepl(deps) {
|
|
|
447
452
|
const resetSessionUi = (ctl) => {
|
|
448
453
|
ctl.clearConversation?.();
|
|
449
454
|
review.reset(sessionConversation(harness.session));
|
|
455
|
+
review.sessions.restoreMain(readSessionUsage(harness.config.sessionDir, harness.session.id));
|
|
450
456
|
imageRegistry.clear();
|
|
451
457
|
pasteRegistry.reset();
|
|
452
458
|
ctl.restoreQueue?.();
|
|
@@ -671,6 +677,7 @@ export async function startRepl(deps) {
|
|
|
671
677
|
const providerId = harness.providers.resolve(harness.config.model)?.provider.id ?? "";
|
|
672
678
|
return buildUsageFooter(statusData(), theme, stats, providerId, statusOpts()).slice(0, 1);
|
|
673
679
|
},
|
|
680
|
+
warningLines: () => storageWarning(storageHealth.snapshot(harness.config.sessionDir), terminalColumns(), theme.yellow),
|
|
674
681
|
completion,
|
|
675
682
|
hint: slashHint,
|
|
676
683
|
statusStyle: (s) => theme.dark(s),
|
|
@@ -782,7 +789,13 @@ export async function startRepl(deps) {
|
|
|
782
789
|
input.write(`\n ${mark} background ${t.id} ` +
|
|
783
790
|
theme.dim(`[${t.status}${t.exitCode != null ? ` · uscita ${t.exitCode}` : ""}] ${truncate(t.command, 50)} · /bg ${t.id} per l'output\n`));
|
|
784
791
|
});
|
|
785
|
-
|
|
792
|
+
const unsubscribeStorage = storageHealth.subscribe(() => input.refresh());
|
|
793
|
+
try {
|
|
794
|
+
await input.start();
|
|
795
|
+
}
|
|
796
|
+
finally {
|
|
797
|
+
unsubscribeStorage();
|
|
798
|
+
}
|
|
786
799
|
return;
|
|
787
800
|
}
|
|
788
801
|
// Fallback (non-TTY o TALOS_CLASSIC_INPUT=1): loop readline classico inline.
|
|
@@ -790,6 +803,13 @@ export async function startRepl(deps) {
|
|
|
790
803
|
if (harness.session.head)
|
|
791
804
|
replaySession(harness.session, { write: text => stdout.write(text) }, theme, terminalColumns());
|
|
792
805
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
806
|
+
const reportStorage = () => {
|
|
807
|
+
const rows = storageWarning(storageHealth.snapshot(harness.config.sessionDir), terminalColumns(), theme.yellow);
|
|
808
|
+
if (rows.length)
|
|
809
|
+
stdout.write("\n" + rows.join("\n") + "\n");
|
|
810
|
+
};
|
|
811
|
+
const unsubscribeStorage = storageHealth.subscribe(reportStorage);
|
|
812
|
+
reportStorage();
|
|
793
813
|
const ctl = {
|
|
794
814
|
review,
|
|
795
815
|
pause: () => rl.pause(),
|
|
@@ -848,6 +868,7 @@ export async function startRepl(deps) {
|
|
|
848
868
|
void lineQueue.enqueue(raw);
|
|
849
869
|
});
|
|
850
870
|
rl.on("close", () => {
|
|
871
|
+
unsubscribeStorage();
|
|
851
872
|
lineQueue.close();
|
|
852
873
|
harness.dispose?.();
|
|
853
874
|
void harness.host.emit("session_shutdown", {});
|
|
@@ -1057,7 +1078,7 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
|
|
|
1057
1078
|
const turnInput = imageParts && imageParts.length
|
|
1058
1079
|
? [{ type: "text", text: input }, ...imageParts]
|
|
1059
1080
|
: input;
|
|
1060
|
-
await runTurn({ ...deps.harness.deps(confirm), ask, spawnKingdom, inbox }, turnInput, wrappedSink, signal);
|
|
1081
|
+
await runTurn({ ...deps.harness.deps(confirm), env: { ...env, TALOS_AUTO_TITLE: "1" }, ask, spawnKingdom, inbox }, turnInput, wrappedSink, signal);
|
|
1061
1082
|
if (turnFailed || signal.aborted)
|
|
1062
1083
|
return;
|
|
1063
1084
|
// REVISORE di team: per i task complessi (molte stanze) con stanze fatte e
|
|
@@ -1404,7 +1425,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
|
|
|
1404
1425
|
ctl.write(preview.lines.join("\n") + "\n");
|
|
1405
1426
|
commandCards.set(event.call.id, { index: ctl.logLength() - preview.lines.length,
|
|
1406
1427
|
firstRow: preview.firstRow, columns, end: ctl.logLength(), header: preview.lines[0] });
|
|
1407
|
-
ctl.setStatus(tr("
|
|
1428
|
+
ctl.setStatus(tr("Esecuzione comando", "Running command"));
|
|
1408
1429
|
ctl.setStatusTool?.(event.call.name);
|
|
1409
1430
|
break;
|
|
1410
1431
|
}
|
|
@@ -1474,6 +1495,9 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
|
|
|
1474
1495
|
ctl.setStatus(tr("elaboro", "working"));
|
|
1475
1496
|
stopDot(); // ferma l'animazione del bullet: il tool e' finito
|
|
1476
1497
|
const call = calls.get(event.result.callId);
|
|
1498
|
+
const before = preContent.get(event.result.callId);
|
|
1499
|
+
calls.delete(event.result.callId);
|
|
1500
|
+
preContent.delete(event.result.callId);
|
|
1477
1501
|
const toolCard = toolCards.get(event.result.callId);
|
|
1478
1502
|
toolCards.delete(event.result.callId);
|
|
1479
1503
|
if (event.parentId) {
|
|
@@ -1548,7 +1572,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
|
|
|
1548
1572
|
break;
|
|
1549
1573
|
}
|
|
1550
1574
|
if (call && !event.result.isError && call.name === "write_file") {
|
|
1551
|
-
ctl.review?.change(call.id, { before: call.input["append"] === true ? "" :
|
|
1575
|
+
ctl.review?.change(call.id, { before: call.input["append"] === true ? "" : before,
|
|
1552
1576
|
after: String(call.input["content"] ?? ""), scope: call.input["append"] === true ? "append" : "file" });
|
|
1553
1577
|
}
|
|
1554
1578
|
// Token per stanza/castello (solo con vascend attivo). Snapshot a
|
|
@@ -1593,13 +1617,24 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
|
|
|
1593
1617
|
}
|
|
1594
1618
|
// Keep the change preview alongside its operation.
|
|
1595
1619
|
if (call && !event.result.isError) {
|
|
1596
|
-
const diff = diffForCall(call,
|
|
1620
|
+
const diff = diffForCall(call, before, event.result.output, theme, terminalColumns());
|
|
1597
1621
|
if (diff) {
|
|
1598
1622
|
ctl.write(previewDiff(diff, theme, terminalColumns() - 1));
|
|
1599
1623
|
}
|
|
1600
1624
|
}
|
|
1601
1625
|
break;
|
|
1602
1626
|
}
|
|
1627
|
+
case "session_naming":
|
|
1628
|
+
ctl.setSessionNaming?.(event.phase, event.title);
|
|
1629
|
+
break;
|
|
1630
|
+
case "compacting":
|
|
1631
|
+
stopDot();
|
|
1632
|
+
flushText();
|
|
1633
|
+
ctl.setCompacting?.(true);
|
|
1634
|
+
break;
|
|
1635
|
+
case "compaction_end":
|
|
1636
|
+
ctl.setCompacting?.(false);
|
|
1637
|
+
break;
|
|
1603
1638
|
case "compacted":
|
|
1604
1639
|
stopDot();
|
|
1605
1640
|
flushText();
|
|
@@ -2061,6 +2096,7 @@ function sessionOptions(list) {
|
|
|
2061
2096
|
label: sessionTitle(s.name ?? "") || s.preview || s.id,
|
|
2062
2097
|
detail: `${fmtAge(s.updatedAt)} · ${s.messages} msg · ${s.id}`,
|
|
2063
2098
|
value: s.id,
|
|
2099
|
+
age: fmtAge(s.updatedAt),
|
|
2064
2100
|
}));
|
|
2065
2101
|
}
|
|
2066
2102
|
// Riprende una sessione: la carica nell'harness (le closure vedono il nuovo
|
|
@@ -2318,8 +2354,45 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
|
|
|
2318
2354
|
return null;
|
|
2319
2355
|
}
|
|
2320
2356
|
case "rename": {
|
|
2321
|
-
|
|
2322
|
-
if (!title
|
|
2357
|
+
let title = args.trim();
|
|
2358
|
+
if (!title) {
|
|
2359
|
+
const choice = await selectOption({
|
|
2360
|
+
title: tr("Rinomina chat", "Rename chat"),
|
|
2361
|
+
question: true,
|
|
2362
|
+
options: [
|
|
2363
|
+
{ key: "generate", label: tr("Rigenera dall'ultimo prompt", "Regenerate from the latest prompt"),
|
|
2364
|
+
detail: tr("Usa il modello selezionato per scegliere il titolo.", "Use the selected model to choose the title."), value: "generate" },
|
|
2365
|
+
{ key: "manual", label: tr("Imposta manualmente", "Set manually"),
|
|
2366
|
+
detail: tr("Scrivi il nuovo nome della chat.", "Type the new chat name."), value: "manual" },
|
|
2367
|
+
],
|
|
2368
|
+
ctl, theme, terminalColumns,
|
|
2369
|
+
});
|
|
2370
|
+
if (!choice || signal.aborted)
|
|
2371
|
+
return null;
|
|
2372
|
+
if (choice.value === "manual") {
|
|
2373
|
+
const entered = await ctl.promptInput(tr("Nome della chat (Invio conferma · Esc annulla)", "Chat name (Enter confirms · Esc cancels)"));
|
|
2374
|
+
if (!entered?.trim() || signal.aborted)
|
|
2375
|
+
return null;
|
|
2376
|
+
title = entered.trim();
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
if (!title) {
|
|
2380
|
+
const last = [...harness.session.history()].reverse().find(message => message.role === "user");
|
|
2381
|
+
const prompt = last?.content.filter(part => part.type === "text").map(part => part.text).join(" ") || "";
|
|
2382
|
+
if (!prompt) {
|
|
2383
|
+
notifyStatus(ctl, "Invia prima un messaggio per generare il titolo.");
|
|
2384
|
+
return null;
|
|
2385
|
+
}
|
|
2386
|
+
await nameSession(harness.deps(async () => false), prompt, event => {
|
|
2387
|
+
if (event.type === "usage")
|
|
2388
|
+
stats.observe?.(event);
|
|
2389
|
+
if (event.type === "session_naming")
|
|
2390
|
+
ctl.setSessionNaming?.(event.phase, event.title);
|
|
2391
|
+
}, signal, true);
|
|
2392
|
+
ctl.refresh?.();
|
|
2393
|
+
return null;
|
|
2394
|
+
}
|
|
2395
|
+
if (title.length > 120) {
|
|
2323
2396
|
ctl.write(theme.dim("\n Usa /rename Titolo della chat (da 1 a 120 caratteri).\n"));
|
|
2324
2397
|
return null;
|
|
2325
2398
|
}
|
|
@@ -3007,44 +3080,52 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
|
|
|
3007
3080
|
case "vascend-compact":
|
|
3008
3081
|
case "compact": {
|
|
3009
3082
|
const hasModel = Boolean(harness.providers.resolve(harness.config.model));
|
|
3083
|
+
let feedback = "Checkpoint salvato; nessun modello configurato per compattare.";
|
|
3084
|
+
let failed = false;
|
|
3010
3085
|
// 1) Compattazione LLM del contesto della sessione.
|
|
3011
3086
|
if (hasModel) {
|
|
3012
3087
|
const meter = stats.context ?? new ContextMeter();
|
|
3013
3088
|
const before = meter.read(harness).tokens;
|
|
3014
|
-
ctl.write(theme.dim(`\n compatto ~${formatTokenCount(before)} token attivi; preservo richiesta, attivita aperte e lavoro recente...\n`));
|
|
3015
3089
|
const controller = new AbortController();
|
|
3016
3090
|
const onSigint = () => controller.abort();
|
|
3091
|
+
if (signal.aborted)
|
|
3092
|
+
controller.abort();
|
|
3093
|
+
signal.addEventListener("abort", onSigint, { once: true });
|
|
3017
3094
|
process.once("SIGINT", onSigint);
|
|
3018
3095
|
try {
|
|
3019
3096
|
const done = await compactNow(harness.deps(async () => false), controller.signal, (e) => {
|
|
3020
3097
|
if (e.type === "usage")
|
|
3021
3098
|
stats.observe?.(e);
|
|
3099
|
+
if (e.type === "compacting")
|
|
3100
|
+
ctl.setCompacting?.(true);
|
|
3101
|
+
if (e.type === "compaction_end")
|
|
3102
|
+
ctl.setCompacting?.(false);
|
|
3022
3103
|
}, args || undefined);
|
|
3023
3104
|
if (done) {
|
|
3024
3105
|
const after = meter.read(harness);
|
|
3025
|
-
|
|
3026
|
-
|
|
3106
|
+
feedback = `Contesto compattato: ~${formatTokenCount(before)} -> ~${formatTokenCount(after.tokens)} token`;
|
|
3107
|
+
}
|
|
3108
|
+
else {
|
|
3109
|
+
feedback = controller.signal.aborted
|
|
3110
|
+
? "Compattazione interrotta; contesto conservato."
|
|
3111
|
+
: "Nessuna compattazione necessaria; contesto conservato.";
|
|
3027
3112
|
}
|
|
3028
|
-
if (!done)
|
|
3029
|
-
ctl.write(theme.dim(controller.signal.aborted
|
|
3030
|
-
? " compattazione interrotta; contesto conservato.\n"
|
|
3031
|
-
: " nessuna compattazione applicata; contesto conservato.\n"));
|
|
3032
3113
|
}
|
|
3033
3114
|
catch (err) {
|
|
3034
|
-
|
|
3115
|
+
feedback = `Compattazione non riuscita: ${err.message}`;
|
|
3116
|
+
failed = true;
|
|
3035
3117
|
}
|
|
3036
3118
|
finally {
|
|
3037
3119
|
process.removeListener("SIGINT", onSigint);
|
|
3120
|
+
signal.removeEventListener("abort", onSigint);
|
|
3121
|
+
ctl.setCompacting?.(false);
|
|
3122
|
+
ctl.setStatus(null);
|
|
3038
3123
|
}
|
|
3039
3124
|
}
|
|
3040
|
-
else {
|
|
3041
|
-
ctl.write(theme.dim("\n nessun modello configurato: salvo solo il checkpoint Vascend.\n"));
|
|
3042
|
-
}
|
|
3043
3125
|
// 2) Checkpoint Vascend/Danilov sullo stato (gia' compattato) della sessione.
|
|
3044
3126
|
vascend.loadPlan(harness.config.sessionDir, harness.config.planStoreId ?? harness.config.roomStoreId ?? harness.session.id);
|
|
3045
|
-
|
|
3046
|
-
ctl
|
|
3047
|
-
ctl.write(theme.dim(` piano e contesto pronti per la ripresa (${checkpoint.text.split("\n").length} righe)\n`));
|
|
3127
|
+
vascend.compact(harness.session.history());
|
|
3128
|
+
notifyStatus(ctl, feedback, failed);
|
|
3048
3129
|
return null;
|
|
3049
3130
|
}
|
|
3050
3131
|
case "tree": {
|
|
@@ -3200,9 +3281,11 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
|
|
|
3200
3281
|
return null;
|
|
3201
3282
|
}
|
|
3202
3283
|
const selected = await selectOption({
|
|
3203
|
-
title:
|
|
3284
|
+
title: "Resume a previous session",
|
|
3204
3285
|
hint: tr("Scrivi per cercare · Invio apre · Esc torna alla chat corrente", "Type to search · Enter open · Esc return to the current chat"),
|
|
3205
3286
|
filterable: true,
|
|
3287
|
+
sessionList: true,
|
|
3288
|
+
fullHeight: true,
|
|
3206
3289
|
options: sessionOptions(list).map((option) => ({ ...option,
|
|
3207
3290
|
detail: option.detail + (option.value === harness.session.id ? " · " + tr("corrente", "current") : ""),
|
|
3208
3291
|
})),
|
|
@@ -3805,7 +3888,9 @@ async function pickModel(harness, ctl, theme, terminalColumns) {
|
|
|
3805
3888
|
}
|
|
3806
3889
|
const current = canonicalModel(harness.config.model);
|
|
3807
3890
|
const selected = await selectOption({
|
|
3808
|
-
title: "Modelli",
|
|
3891
|
+
title: tr("Modelli", "Models"),
|
|
3892
|
+
modelList: true,
|
|
3893
|
+
fullHeight: true,
|
|
3809
3894
|
options,
|
|
3810
3895
|
currentKey: current?.fullModel,
|
|
3811
3896
|
ctl,
|
|
@@ -3862,7 +3947,8 @@ function modelOptions(providers, dynamic) {
|
|
|
3862
3947
|
const detail = dynById.get(model)?.detail;
|
|
3863
3948
|
out.push({
|
|
3864
3949
|
key: full,
|
|
3865
|
-
label:
|
|
3950
|
+
label: model,
|
|
3951
|
+
group: spec.label,
|
|
3866
3952
|
detail: `${authSummary(spec.id, authProviders)}${detail ? ` · ${detail}` : ""}`,
|
|
3867
3953
|
value: { providerId: spec.id, modelId: model, fullModel: full },
|
|
3868
3954
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentEvent, Message, ToolCallPart } from "../core/types.js";
|
|
2
|
+
import { AgentSessions } from "./agent-sessions.js";
|
|
2
3
|
export type ReviewState = "running" | "ok" | "error" | "stopped" | "info";
|
|
3
4
|
export type ReviewKind = "tool" | "file" | "team" | "notice";
|
|
4
5
|
export interface ReviewChange {
|
|
@@ -27,7 +28,11 @@ export declare function reviewTitle(name: string): string;
|
|
|
27
28
|
export declare function callTarget(call: ToolCallPart): string;
|
|
28
29
|
export declare class ReviewStore {
|
|
29
30
|
private readonly capacity;
|
|
31
|
+
private readonly maxChars;
|
|
32
|
+
readonly sessions: AgentSessions;
|
|
30
33
|
private items;
|
|
34
|
+
private sizes;
|
|
35
|
+
private retainedChars;
|
|
31
36
|
private callIndex;
|
|
32
37
|
private actorText;
|
|
33
38
|
private sequence;
|
|
@@ -37,7 +42,7 @@ export declare class ReviewStore {
|
|
|
37
42
|
private _running;
|
|
38
43
|
private _label;
|
|
39
44
|
private _goal;
|
|
40
|
-
constructor(capacity?: number);
|
|
45
|
+
constructor(capacity?: number, maxChars?: number);
|
|
41
46
|
get revision(): number;
|
|
42
47
|
get evicted(): number;
|
|
43
48
|
get running(): boolean;
|
|
@@ -57,4 +62,5 @@ export declare class ReviewStore {
|
|
|
57
62
|
private callId;
|
|
58
63
|
private notice;
|
|
59
64
|
private put;
|
|
65
|
+
private remove;
|
|
60
66
|
}
|
package/dist/tui/review-store.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { oneLine } from "./terminal-text.js";
|
|
2
|
+
import { AgentSessions } from "./agent-sessions.js";
|
|
2
3
|
const LABELS = {
|
|
3
4
|
read_file: "Lettura", list_dir: "Cartella", grep: "Ricerca", glob: "Ricerca file",
|
|
4
5
|
write_file: "Scrittura", edit_file: "Modifica", bash: "Comando", bash_bg: "Comando in background",
|
|
@@ -9,6 +10,14 @@ const LABELS = {
|
|
|
9
10
|
kingdom_send: "Messaggio al team", kingdom_inbox: "Messaggi del team", kingdom_wait: "Attesa collaboratori",
|
|
10
11
|
};
|
|
11
12
|
export function reviewTitle(name) { return LABELS[name] ?? name; }
|
|
13
|
+
function textSize(value, seen = new Set()) {
|
|
14
|
+
if (typeof value === "string")
|
|
15
|
+
return value.length;
|
|
16
|
+
if (!value || typeof value !== "object" || seen.has(value))
|
|
17
|
+
return 0;
|
|
18
|
+
seen.add(value);
|
|
19
|
+
return Object.values(value).reduce((size, child) => size + textSize(child, seen), 0);
|
|
20
|
+
}
|
|
12
21
|
export function callTarget(call) {
|
|
13
22
|
const i = call.input;
|
|
14
23
|
return oneLine(String(i["path"] ?? i["command"] ?? i["pattern"] ?? i["query"] ??
|
|
@@ -18,7 +27,11 @@ export function callTarget(call) {
|
|
|
18
27
|
// della chat viene compattato. Nessuna lettura del filesystem dal renderer.
|
|
19
28
|
export class ReviewStore {
|
|
20
29
|
capacity;
|
|
30
|
+
maxChars;
|
|
31
|
+
sessions = new AgentSessions();
|
|
21
32
|
items = new Map();
|
|
33
|
+
sizes = new Map();
|
|
34
|
+
retainedChars = 0;
|
|
22
35
|
callIndex = new Map();
|
|
23
36
|
actorText = new Map();
|
|
24
37
|
sequence = 0;
|
|
@@ -28,8 +41,9 @@ export class ReviewStore {
|
|
|
28
41
|
_running = false;
|
|
29
42
|
_label = "Pronto";
|
|
30
43
|
_goal = "";
|
|
31
|
-
constructor(capacity = 2000) {
|
|
44
|
+
constructor(capacity = 2000, maxChars = 8_000_000) {
|
|
32
45
|
this.capacity = capacity;
|
|
46
|
+
this.maxChars = maxChars;
|
|
33
47
|
}
|
|
34
48
|
get revision() { return this._revision; }
|
|
35
49
|
get evicted() { return this._evicted; }
|
|
@@ -39,6 +53,7 @@ export class ReviewStore {
|
|
|
39
53
|
entries() { return [...this.items.values()]; }
|
|
40
54
|
get(id) { return this.items.get(id); }
|
|
41
55
|
begin(goal) {
|
|
56
|
+
this.sessions.begin(goal);
|
|
42
57
|
this.run += 1;
|
|
43
58
|
this._running = true;
|
|
44
59
|
this._label = "Avvio";
|
|
@@ -46,6 +61,7 @@ export class ReviewStore {
|
|
|
46
61
|
this._revision += 1;
|
|
47
62
|
}
|
|
48
63
|
finish(interrupted = false) {
|
|
64
|
+
this.sessions.finish();
|
|
49
65
|
for (const entry of this.items.values()) {
|
|
50
66
|
if (entry.state === "running" && entry.actor === "Talos")
|
|
51
67
|
this.put({ ...entry, state: "stopped", endedAt: Date.now() });
|
|
@@ -55,7 +71,10 @@ export class ReviewStore {
|
|
|
55
71
|
this._revision += 1;
|
|
56
72
|
}
|
|
57
73
|
reset(history = []) {
|
|
74
|
+
this.sessions.reset();
|
|
58
75
|
this.items.clear();
|
|
76
|
+
this.sizes.clear();
|
|
77
|
+
this.retainedChars = 0;
|
|
59
78
|
this.callIndex.clear();
|
|
60
79
|
this.actorText.clear();
|
|
61
80
|
this.sequence = 0;
|
|
@@ -87,6 +106,7 @@ export class ReviewStore {
|
|
|
87
106
|
this.put({ ...entry, change });
|
|
88
107
|
}
|
|
89
108
|
record(event, at = Date.now(), actor = "Talos") {
|
|
109
|
+
this.sessions.record(event, at, actor);
|
|
90
110
|
switch (event.type) {
|
|
91
111
|
case "status":
|
|
92
112
|
this._label = oneLine(event.label);
|
|
@@ -149,6 +169,8 @@ export class ReviewStore {
|
|
|
149
169
|
}
|
|
150
170
|
}
|
|
151
171
|
recordActor(actor, event) {
|
|
172
|
+
if (event.type === "status" || event.type === "text")
|
|
173
|
+
this.sessions.record(event, Date.now(), actor);
|
|
152
174
|
if (event.type === "status") {
|
|
153
175
|
this.put({ id: `status:${this.run}:${actor}`, kind: "notice", actor, title: "Stato",
|
|
154
176
|
target: oneLine(event.label), output: event.label, state: "info", startedAt: Date.now() });
|
|
@@ -166,7 +188,7 @@ export class ReviewStore {
|
|
|
166
188
|
if (text?.trim())
|
|
167
189
|
this.notice("Aggiornamento", text, "info", Date.now(), actor);
|
|
168
190
|
this.actorText.delete(actor);
|
|
169
|
-
this.
|
|
191
|
+
this.remove(`stream:${this.run}:${actor}`);
|
|
170
192
|
}
|
|
171
193
|
if (event.type === "turn_start" || event.type === "done" || event.type === "error") {
|
|
172
194
|
this.record({ type: "kingdom", phase: "vassal", slug: actor,
|
|
@@ -254,17 +276,27 @@ export class ReviewStore {
|
|
|
254
276
|
this.put({ id: `notice:${++this.sequence}`, kind: "notice", title, target: oneLine(output).slice(0, 200), actor, state, output, startedAt: at });
|
|
255
277
|
}
|
|
256
278
|
put(entry) {
|
|
279
|
+
const size = textSize(entry);
|
|
280
|
+
this.retainedChars += size - (this.sizes.get(entry.id) ?? 0);
|
|
281
|
+
this.sizes.set(entry.id, size);
|
|
257
282
|
this.items.set(entry.id, entry);
|
|
258
|
-
|
|
283
|
+
while (this.items.size > this.capacity || this.retainedChars > this.maxChars) {
|
|
259
284
|
const oldest = [...this.items.values()].find(e => e.state !== "running");
|
|
260
285
|
if (oldest) {
|
|
261
|
-
this.
|
|
286
|
+
this.remove(oldest.id);
|
|
262
287
|
for (const [key, id] of this.callIndex)
|
|
263
288
|
if (id === oldest.id)
|
|
264
289
|
this.callIndex.delete(key);
|
|
265
290
|
this._evicted += 1;
|
|
266
291
|
}
|
|
292
|
+
else
|
|
293
|
+
break;
|
|
267
294
|
}
|
|
268
295
|
this._revision += 1;
|
|
269
296
|
}
|
|
297
|
+
remove(id) {
|
|
298
|
+
this.retainedChars -= this.sizes.get(id) ?? 0;
|
|
299
|
+
this.sizes.delete(id);
|
|
300
|
+
this.items.delete(id);
|
|
301
|
+
}
|
|
270
302
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ScrollbackLine } from "./native-scrollback.js";
|
|
2
|
+
export declare class ScrollbackArchive implements Iterable<ScrollbackLine> {
|
|
3
|
+
private readonly maxChars;
|
|
4
|
+
private rows;
|
|
5
|
+
private chars;
|
|
6
|
+
private directory?;
|
|
7
|
+
private fd?;
|
|
8
|
+
private bytes;
|
|
9
|
+
constructor(maxChars?: number);
|
|
10
|
+
get retainedChars(): number;
|
|
11
|
+
push(line: ScrollbackLine): void;
|
|
12
|
+
private write;
|
|
13
|
+
private spill;
|
|
14
|
+
[Symbol.iterator](): Iterator<ScrollbackLine>;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { closeSync, mkdtempSync, openSync, readSync, rmdirSync, unlinkSync, writeSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { StringDecoder } from "node:string_decoder";
|
|
5
|
+
// This extra copy is used for resize, not model context. Spill old rows to
|
|
6
|
+
// disk instead of retaining another complete transcript in the JavaScript heap.
|
|
7
|
+
export class ScrollbackArchive {
|
|
8
|
+
maxChars;
|
|
9
|
+
rows = [];
|
|
10
|
+
chars = 0;
|
|
11
|
+
directory;
|
|
12
|
+
fd;
|
|
13
|
+
bytes = 0;
|
|
14
|
+
constructor(maxChars = 1_000_000) {
|
|
15
|
+
this.maxChars = maxChars;
|
|
16
|
+
}
|
|
17
|
+
get retainedChars() { return this.chars; }
|
|
18
|
+
push(line) {
|
|
19
|
+
if (this.rows.length >= 5000 || this.chars + line.text.length > this.maxChars)
|
|
20
|
+
this.spill();
|
|
21
|
+
if (line.text.length > this.maxChars)
|
|
22
|
+
this.write(line);
|
|
23
|
+
else {
|
|
24
|
+
this.rows.push(line);
|
|
25
|
+
this.chars += line.text.length;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
write(line) {
|
|
29
|
+
if (this.fd === undefined) {
|
|
30
|
+
this.directory = mkdtempSync(join(tmpdir(), "talos-scrollback-"));
|
|
31
|
+
this.fd = openSync(join(this.directory, "rows.jsonl"), "w+", 0o600);
|
|
32
|
+
}
|
|
33
|
+
const data = Buffer.from(JSON.stringify(line) + "\n");
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (offset < data.length) {
|
|
36
|
+
const written = writeSync(this.fd, data, offset, data.length - offset, this.bytes);
|
|
37
|
+
if (!written)
|
|
38
|
+
throw new Error("Unable to write terminal scrollback");
|
|
39
|
+
offset += written;
|
|
40
|
+
this.bytes += written;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
spill() {
|
|
44
|
+
for (const row of this.rows)
|
|
45
|
+
this.write(row);
|
|
46
|
+
this.rows = [];
|
|
47
|
+
this.chars = 0;
|
|
48
|
+
}
|
|
49
|
+
*[Symbol.iterator]() {
|
|
50
|
+
if (this.fd !== undefined) {
|
|
51
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
52
|
+
const decoder = new StringDecoder("utf8");
|
|
53
|
+
let position = 0;
|
|
54
|
+
let pending = "";
|
|
55
|
+
while (position < this.bytes) {
|
|
56
|
+
const size = readSync(this.fd, buffer, 0, Math.min(buffer.length, this.bytes - position), position);
|
|
57
|
+
if (!size)
|
|
58
|
+
throw new Error("Unable to read terminal scrollback");
|
|
59
|
+
position += size;
|
|
60
|
+
pending += decoder.write(buffer.subarray(0, size));
|
|
61
|
+
let start = 0;
|
|
62
|
+
let end;
|
|
63
|
+
while ((end = pending.indexOf("\n", start)) >= 0) {
|
|
64
|
+
yield JSON.parse(pending.slice(start, end));
|
|
65
|
+
start = end + 1;
|
|
66
|
+
}
|
|
67
|
+
pending = pending.slice(start);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
yield* this.rows;
|
|
71
|
+
}
|
|
72
|
+
clear() {
|
|
73
|
+
this.rows = [];
|
|
74
|
+
this.chars = 0;
|
|
75
|
+
this.bytes = 0;
|
|
76
|
+
if (this.fd !== undefined) {
|
|
77
|
+
closeSync(this.fd);
|
|
78
|
+
this.fd = undefined;
|
|
79
|
+
}
|
|
80
|
+
if (this.directory) {
|
|
81
|
+
const directory = this.directory;
|
|
82
|
+
this.directory = undefined;
|
|
83
|
+
try {
|
|
84
|
+
unlinkSync(join(directory, "rows.jsonl"));
|
|
85
|
+
rmdirSync(directory);
|
|
86
|
+
}
|
|
87
|
+
catch { /* A scanner may hold the disposable cache open on Windows. */ }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/tui/selector.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface SelectOption<T> {
|
|
|
6
6
|
readonly group?: string;
|
|
7
7
|
readonly hint?: string;
|
|
8
8
|
readonly value: T;
|
|
9
|
+
readonly age?: string;
|
|
9
10
|
}
|
|
10
11
|
interface SelectorControl {
|
|
11
12
|
pause(): void;
|
|
@@ -24,6 +25,8 @@ interface SelectorOptions<T> {
|
|
|
24
25
|
readonly question?: boolean;
|
|
25
26
|
readonly selectedDetailsOnly?: boolean;
|
|
26
27
|
readonly mouse?: boolean;
|
|
28
|
+
readonly sessionList?: boolean;
|
|
29
|
+
readonly modelList?: boolean;
|
|
27
30
|
}
|
|
28
31
|
export interface SelectorRenderOptions<T> extends SelectorOptions<T> {
|
|
29
32
|
readonly index: number;
|
|
@@ -33,6 +36,7 @@ export interface SelectorRenderOptions<T> extends SelectorOptions<T> {
|
|
|
33
36
|
readonly totalCount?: number;
|
|
34
37
|
readonly details?: boolean;
|
|
35
38
|
readonly detailOffset?: number;
|
|
39
|
+
readonly rows?: number;
|
|
36
40
|
}
|
|
37
41
|
export declare function pickerPageSize(rows: number, fullHeight?: boolean): number;
|
|
38
42
|
export declare function filterOptions<T extends {
|
package/dist/tui/selector.js
CHANGED
|
@@ -6,6 +6,8 @@ import { tr } from "./i18n.js";
|
|
|
6
6
|
import { SelectorMouseInput, selectorMouseIndex } from "./selector-mouse.js";
|
|
7
7
|
import { fitLine, oneLine, padLine, plainText, terminalWidth, wrapText } from "./terminal-text.js";
|
|
8
8
|
import { renderQuestionChoices } from "./question.js";
|
|
9
|
+
import { renderSessionPicker } from "./session-picker.js";
|
|
10
|
+
import { renderModelPicker } from "./model-picker.js";
|
|
9
11
|
export function pickerPageSize(rows, fullHeight = false) {
|
|
10
12
|
const available = Math.max(1, Math.floor(rows) - 6);
|
|
11
13
|
return Math.min(available, fullHeight ? 26 : 7);
|
|
@@ -42,6 +44,10 @@ function detailLines(opts, option, width) {
|
|
|
42
44
|
];
|
|
43
45
|
}
|
|
44
46
|
export function renderSelector(opts) {
|
|
47
|
+
if (opts.modelList && !opts.details)
|
|
48
|
+
return renderModelPicker(opts);
|
|
49
|
+
if (opts.sessionList && !opts.details)
|
|
50
|
+
return renderSessionPicker(opts);
|
|
45
51
|
const t = opts.theme;
|
|
46
52
|
const width = selectorWidth(opts.columns, opts.wide);
|
|
47
53
|
const page = Math.max(1, opts.pageSize);
|
|
@@ -132,7 +138,8 @@ export async function selectOption(opts) {
|
|
|
132
138
|
let renderedRows = 0;
|
|
133
139
|
const wasRaw = stdin.isRaw;
|
|
134
140
|
const wasPaused = stdin.isPaused();
|
|
135
|
-
const pageSize = () =>
|
|
141
|
+
const pageSize = () => opts.modelList ? Math.max(1, (stdout.rows || 24) - 13)
|
|
142
|
+
: opts.sessionList ? Math.max(1, (stdout.rows || 24) - 8) : pickerPageSize(stdout.rows || 24, opts.fullHeight);
|
|
136
143
|
const detailMax = () => Math.max(0, detailLines(opts, filtered[index], selectorWidth(opts.terminalColumns(), opts.wide) - 2).length - pageSize());
|
|
137
144
|
const applyFilter = () => {
|
|
138
145
|
const previous = filtered[index];
|
|
@@ -152,7 +159,7 @@ export async function selectOption(opts) {
|
|
|
152
159
|
detailOffset = Math.min(detailOffset, detailMax());
|
|
153
160
|
const lines = renderSelector({ ...opts, options: filtered, index,
|
|
154
161
|
pageSize: pageSize(), columns: opts.terminalColumns(), query: opts.filterable ? query : undefined,
|
|
155
|
-
totalCount: opts.options.length, details, detailOffset,
|
|
162
|
+
totalCount: opts.options.length, details, detailOffset, rows: stdout.rows || 24,
|
|
156
163
|
});
|
|
157
164
|
renderedRows = lines.length;
|
|
158
165
|
opts.ctl.setOverlay(lines);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class SessionNamingNotice {
|
|
2
|
+
private readonly refresh;
|
|
3
|
+
private title;
|
|
4
|
+
private startedAt;
|
|
5
|
+
private pending;
|
|
6
|
+
private timer?;
|
|
7
|
+
constructor(refresh: () => void);
|
|
8
|
+
show(title: string): void;
|
|
9
|
+
start(): void;
|
|
10
|
+
finish(title: string): void;
|
|
11
|
+
lines(now?: number): readonly string[];
|
|
12
|
+
line(now?: number): string;
|
|
13
|
+
clear(): void;
|
|
14
|
+
}
|