@vascend/talos 0.1.4 → 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 +37 -12
- 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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { tr } from "./i18n.js";
|
|
2
|
+
import { PLAIN_QUEUE_STYLE } from "./queue-screen.js";
|
|
3
|
+
import { fitLine, oneLine, terminalWidth, wrapText } from "./terminal-text.js";
|
|
4
|
+
export function sessionDuration(ms) {
|
|
5
|
+
const seconds = Math.floor(Math.max(0, ms) / 1000);
|
|
6
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
7
|
+
}
|
|
8
|
+
function tokenCount(n) {
|
|
9
|
+
return n < 1000 ? String(n) : n < 1_000_000 ? `${(n / 1000).toFixed(1)}k` : `${(n / 1_000_000).toFixed(1)}m`;
|
|
10
|
+
}
|
|
11
|
+
function metrics(session) {
|
|
12
|
+
const total = session.inputTokens + session.outputTokens;
|
|
13
|
+
return `${sessionDuration(session.elapsedMs)} · ${session.usageMissing ? "≥" : ""}${session.usageKnown ? tokenCount(total) : "—"} tokens`;
|
|
14
|
+
}
|
|
15
|
+
// One selector is shared by the chat strip and the worker viewport. Moving the
|
|
16
|
+
// cursor never switches the viewed session until Enter confirms the selection.
|
|
17
|
+
export class AgentSessionsScreen {
|
|
18
|
+
store;
|
|
19
|
+
style;
|
|
20
|
+
selected = "main";
|
|
21
|
+
active = "main";
|
|
22
|
+
offset = 0;
|
|
23
|
+
pageSize = 1;
|
|
24
|
+
maxOffset = 0;
|
|
25
|
+
follow = true;
|
|
26
|
+
focused = false;
|
|
27
|
+
constructor(store, style = PLAIN_QUEUE_STYLE) {
|
|
28
|
+
this.store = store;
|
|
29
|
+
this.style = style;
|
|
30
|
+
}
|
|
31
|
+
get selectedId() { this.sync(); return this.selected; }
|
|
32
|
+
get activeId() { this.sync(); return this.active; }
|
|
33
|
+
get available() { return this.store.hasAgents; }
|
|
34
|
+
sync() {
|
|
35
|
+
if (!this.store.get(this.active)?.live) {
|
|
36
|
+
this.active = "main";
|
|
37
|
+
this.selected = "main";
|
|
38
|
+
this.focused = false;
|
|
39
|
+
this.follow = true;
|
|
40
|
+
this.offset = 0;
|
|
41
|
+
}
|
|
42
|
+
if (!this.store.get(this.selected)?.live)
|
|
43
|
+
this.selected = this.active;
|
|
44
|
+
if (!this.available)
|
|
45
|
+
this.focused = false;
|
|
46
|
+
}
|
|
47
|
+
select(delta) {
|
|
48
|
+
this.sync();
|
|
49
|
+
const sessions = this.store.list();
|
|
50
|
+
const index = sessions.findIndex(s => s.id === this.selected);
|
|
51
|
+
this.selected = sessions[Math.max(0, Math.min(sessions.length - 1, index + delta))].id;
|
|
52
|
+
this.focused = true;
|
|
53
|
+
}
|
|
54
|
+
key(_text, key) {
|
|
55
|
+
this.sync();
|
|
56
|
+
if (key.name === "up" || key.name === "down")
|
|
57
|
+
this.select(key.name === "up" ? -1 : 1);
|
|
58
|
+
else if (key.name === "return" || key.name === "enter") {
|
|
59
|
+
this.active = this.selected;
|
|
60
|
+
this.follow = true;
|
|
61
|
+
this.offset = 0;
|
|
62
|
+
if (this.active === "main") {
|
|
63
|
+
this.focused = false;
|
|
64
|
+
return "close";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (key.name === "pageup" || key.name === "pagedown") {
|
|
68
|
+
this.scroll(key.name === "pageup" ? -this.pageSize : this.pageSize);
|
|
69
|
+
}
|
|
70
|
+
else if (key.name === "end") {
|
|
71
|
+
this.follow = true;
|
|
72
|
+
this.offset = this.maxOffset;
|
|
73
|
+
}
|
|
74
|
+
else if (key.name === "home") {
|
|
75
|
+
this.follow = false;
|
|
76
|
+
this.offset = 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
scroll(delta) {
|
|
80
|
+
this.offset = Math.max(0, Math.min(this.maxOffset, this.offset + delta));
|
|
81
|
+
this.follow = this.offset === this.maxOffset;
|
|
82
|
+
}
|
|
83
|
+
preview(width, maxRows = 5) {
|
|
84
|
+
this.sync();
|
|
85
|
+
if (!this.available)
|
|
86
|
+
return [];
|
|
87
|
+
const sessions = this.store.list();
|
|
88
|
+
const capacity = Math.max(1, maxRows - 1);
|
|
89
|
+
const index = Math.max(0, sessions.findIndex(s => s.id === this.selected));
|
|
90
|
+
const start = Math.max(0, Math.min(index - Math.floor(capacity / 2), sessions.length - capacity));
|
|
91
|
+
const help = this.focused
|
|
92
|
+
? tr("↑↓ seleziona · Invio apri · main: sessione principale", "↑↓ select · Enter open · main: primary session")
|
|
93
|
+
: tr("↓ sessioni · Alt+A seleziona", "↓ sessions · Alt+A select");
|
|
94
|
+
const range = sessions.length > capacity ? ` [${start + 1}–${Math.min(sessions.length, start + capacity)}/${sessions.length}]` : "";
|
|
95
|
+
const rows = [this.style.muted(fitLine(` ${help}${range}`, width - 1))];
|
|
96
|
+
for (const s of sessions.slice(start, start + capacity)) {
|
|
97
|
+
const indicator = this.focused && s.id === this.selected ? ">" : " ";
|
|
98
|
+
const name = ` ${indicator} ${s.id === this.active ? "*" : " "} ${oneLine(s.actor)}`;
|
|
99
|
+
const tail = ` ${metrics(s)}`;
|
|
100
|
+
const room = Math.max(1, width - 1 - terminalWidth(tail));
|
|
101
|
+
const activity = s.activity ? ` ${oneLine(s.activity)}` : "";
|
|
102
|
+
const label = fitLine(name + activity, room);
|
|
103
|
+
const row = width < 36 ? fitLine(`${name} ${metrics(s)}`, width - 1)
|
|
104
|
+
: label + " ".repeat(Math.max(0, room - terminalWidth(label))) + tail;
|
|
105
|
+
rows.push(s.id === this.selected && this.focused ? this.style.selected(row) : (this.style.text ?? this.style.muted)(row));
|
|
106
|
+
}
|
|
107
|
+
return rows;
|
|
108
|
+
}
|
|
109
|
+
render(width, height) {
|
|
110
|
+
this.sync();
|
|
111
|
+
const h = Math.max(1, height), w = Math.max(1, width - 2);
|
|
112
|
+
const session = this.store.get(this.active);
|
|
113
|
+
const heading = this.style.heading(fitLine(` ${tr("Sessione", "Session")} · ${oneLine(session.actor)} · ${metrics(session)}`, width - 1));
|
|
114
|
+
const list = this.preview(width, Math.min(6, Math.max(2, Math.floor(h / 3))));
|
|
115
|
+
const help = this.style.muted(fitLine(tr("↑↓ seleziona · Invio apri · seleziona main per tornare · PgSu/PgGiù output", "↑↓ select · Enter open · select main to return · PgUp/PgDn output"), width - 1));
|
|
116
|
+
const head = [heading, ...list, ""];
|
|
117
|
+
const usage = session.usageKnown
|
|
118
|
+
? `Input: ${tokenCount(session.inputTokens)} · Output: ${tokenCount(session.outputTokens)}`
|
|
119
|
+
: tr("Token non ancora comunicati dal provider.", "Tokens not yet reported by the provider.");
|
|
120
|
+
const text = [session.goal, usage, ...session.transcript].filter(Boolean).join("\n\n")
|
|
121
|
+
|| tr("In attesa di attività…", "Waiting for activity…");
|
|
122
|
+
const lines = wrapText(text, w, true);
|
|
123
|
+
this.pageSize = Math.max(1, h - head.length - 1);
|
|
124
|
+
this.maxOffset = Math.max(0, lines.length - this.pageSize);
|
|
125
|
+
this.offset = this.follow ? this.maxOffset : Math.min(this.offset, this.maxOffset);
|
|
126
|
+
const body = lines.slice(this.offset, this.offset + this.pageSize).map(line => " " + line);
|
|
127
|
+
const rows = [...head, ...body];
|
|
128
|
+
while (rows.length < h - 1)
|
|
129
|
+
rows.push("");
|
|
130
|
+
return [...rows.slice(0, Math.max(0, h - 1)), help].map(row => fitLine(row, width - 1));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AgentEvent } from "../core/types.js";
|
|
2
|
+
import type { SessionUsageRecord } from "../core/session-usage.js";
|
|
3
|
+
export interface AgentSession {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly actor: string;
|
|
6
|
+
readonly goal: string;
|
|
7
|
+
readonly activity: string;
|
|
8
|
+
readonly live: boolean;
|
|
9
|
+
readonly running: boolean;
|
|
10
|
+
readonly startedAt?: number;
|
|
11
|
+
readonly elapsedMs: number;
|
|
12
|
+
readonly inputTokens: number;
|
|
13
|
+
readonly outputTokens: number;
|
|
14
|
+
readonly usageKnown: boolean;
|
|
15
|
+
readonly usageMissing: boolean;
|
|
16
|
+
readonly transcript: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export declare class AgentSessions {
|
|
19
|
+
private sessions;
|
|
20
|
+
private actors;
|
|
21
|
+
private seenUsage;
|
|
22
|
+
private listeners;
|
|
23
|
+
subscribe(listener: () => void): () => void;
|
|
24
|
+
private changed;
|
|
25
|
+
reset(): void;
|
|
26
|
+
restoreMain(history: {
|
|
27
|
+
records: readonly SessionUsageRecord[];
|
|
28
|
+
incomplete: boolean;
|
|
29
|
+
}): void;
|
|
30
|
+
list(now?: number): readonly AgentSession[];
|
|
31
|
+
get(id: string, now?: number): AgentSession | undefined;
|
|
32
|
+
get hasAgents(): boolean;
|
|
33
|
+
private snapshot;
|
|
34
|
+
begin(goal: string, at?: number): void;
|
|
35
|
+
finish(at?: number): void;
|
|
36
|
+
private stop;
|
|
37
|
+
private log;
|
|
38
|
+
record(event: AgentEvent, at?: number, actor?: string): void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { tr } from "./i18n.js";
|
|
2
|
+
import { oneLine, plainText } from "./terminal-text.js";
|
|
3
|
+
const EMPTY = { input: 0, output: 0 };
|
|
4
|
+
const MAIN = "main";
|
|
5
|
+
const MAX_TEXT = 64_000;
|
|
6
|
+
const count = (n) => Number.isFinite(n) && n >= 0 ? n : 0;
|
|
7
|
+
const tokens = (u) => ({ input: count(u.inputTokens) + count(u.cacheReadTokens) + count(u.cacheWriteTokens), output: count(u.outputTokens) });
|
|
8
|
+
const add = (a, b) => ({ input: a.input + b.input, output: a.output + b.output });
|
|
9
|
+
function fresh(id, actor, goal = "") {
|
|
10
|
+
return { id, actor, goal, activity: "", live: true, running: false, elapsedMs: 0,
|
|
11
|
+
ledger: EMPTY, fallback: EMPTY, ledgerSeen: false, usageKnown: false, usageMissing: false,
|
|
12
|
+
streaming: "", transcript: [] };
|
|
13
|
+
}
|
|
14
|
+
// Session IDs come from the worker lifecycle, not from a tool call or a label.
|
|
15
|
+
// A finished response does not imply that its worker has been despawned.
|
|
16
|
+
export class AgentSessions {
|
|
17
|
+
sessions = new Map([[MAIN, fresh(MAIN, MAIN)]]);
|
|
18
|
+
actors = new Map();
|
|
19
|
+
seenUsage = new Set();
|
|
20
|
+
listeners = new Set();
|
|
21
|
+
subscribe(listener) {
|
|
22
|
+
this.listeners.add(listener);
|
|
23
|
+
return () => { this.listeners.delete(listener); };
|
|
24
|
+
}
|
|
25
|
+
changed() { for (const listener of this.listeners)
|
|
26
|
+
listener(); }
|
|
27
|
+
reset() {
|
|
28
|
+
this.sessions = new Map([[MAIN, fresh(MAIN, MAIN)]]);
|
|
29
|
+
this.actors.clear();
|
|
30
|
+
this.seenUsage.clear();
|
|
31
|
+
this.changed();
|
|
32
|
+
}
|
|
33
|
+
restoreMain(history) {
|
|
34
|
+
for (const record of history.records) {
|
|
35
|
+
if (!record.actor)
|
|
36
|
+
this.record({ type: "usage", record, persisted: true });
|
|
37
|
+
}
|
|
38
|
+
if (history.incomplete)
|
|
39
|
+
this.sessions.set(MAIN, { ...this.sessions.get(MAIN), usageMissing: true });
|
|
40
|
+
this.changed();
|
|
41
|
+
}
|
|
42
|
+
list(now = Date.now()) {
|
|
43
|
+
return [...this.sessions.values()].filter(s => s.live).map(s => this.snapshot(s, now));
|
|
44
|
+
}
|
|
45
|
+
get(id, now = Date.now()) {
|
|
46
|
+
const s = this.sessions.get(id);
|
|
47
|
+
return s ? this.snapshot(s, now) : undefined;
|
|
48
|
+
}
|
|
49
|
+
get hasAgents() { return [...this.sessions.values()].some(s => s.id !== MAIN && s.live); }
|
|
50
|
+
snapshot(s, now) {
|
|
51
|
+
const usage = s.ledgerSeen ? s.ledger : s.fallback;
|
|
52
|
+
return { id: s.id, actor: s.actor, goal: s.goal, activity: s.activity, live: s.live, running: s.running,
|
|
53
|
+
startedAt: s.startedAt, elapsedMs: s.elapsedMs + (s.startedAt === undefined ? 0 : Math.max(0, now - s.startedAt)),
|
|
54
|
+
inputTokens: usage.input, outputTokens: usage.output, usageKnown: s.usageKnown, usageMissing: s.usageMissing,
|
|
55
|
+
transcript: s.streaming ? [...s.transcript, s.streaming] : s.transcript };
|
|
56
|
+
}
|
|
57
|
+
begin(goal, at = Date.now()) {
|
|
58
|
+
const s = this.sessions.get(MAIN);
|
|
59
|
+
this.sessions.set(MAIN, { ...s, goal, startedAt: s.startedAt ?? at, running: true,
|
|
60
|
+
activity: tr("Avvio", "Starting") });
|
|
61
|
+
this.changed();
|
|
62
|
+
}
|
|
63
|
+
finish(at = Date.now()) {
|
|
64
|
+
this.stop(MAIN, at, false);
|
|
65
|
+
this.changed();
|
|
66
|
+
}
|
|
67
|
+
stop(id, at, despawn) {
|
|
68
|
+
const s = this.sessions.get(id);
|
|
69
|
+
if (!s)
|
|
70
|
+
return;
|
|
71
|
+
this.sessions.set(id, { ...s, live: !despawn, running: false, startedAt: undefined,
|
|
72
|
+
elapsedMs: s.elapsedMs + (s.startedAt === undefined ? 0 : Math.max(0, at - s.startedAt)),
|
|
73
|
+
activity: tr("Concluso", "Finished") });
|
|
74
|
+
}
|
|
75
|
+
log(s, text, stream = false) {
|
|
76
|
+
if (s.id === MAIN)
|
|
77
|
+
return s;
|
|
78
|
+
const clean = plainText(text).slice(0, 12_000);
|
|
79
|
+
if (stream)
|
|
80
|
+
return { ...s, streaming: (s.streaming + clean).slice(-12_000) };
|
|
81
|
+
const transcript = [...s.transcript, ...(s.streaming ? [s.streaming] : []), clean];
|
|
82
|
+
let size = transcript.reduce((total, item) => total + item.length, 0);
|
|
83
|
+
while (transcript.length > 100 || size > MAX_TEXT)
|
|
84
|
+
size -= transcript.shift().length;
|
|
85
|
+
return { ...s, streaming: "", transcript };
|
|
86
|
+
}
|
|
87
|
+
record(event, at = Date.now(), actor = "Talos") {
|
|
88
|
+
if (event.type === "kingdom" && event.phase === "session") {
|
|
89
|
+
if (event.state === "spawned") {
|
|
90
|
+
if (!this.sessions.has(event.sessionId)) {
|
|
91
|
+
this.sessions.set(event.sessionId, { ...fresh(event.sessionId, event.slug, event.goal),
|
|
92
|
+
startedAt: event.at, running: true, activity: tr("Avvio", "Starting") });
|
|
93
|
+
}
|
|
94
|
+
this.actors.set(event.slug, event.sessionId);
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
this.stop(event.sessionId, event.at, true);
|
|
98
|
+
// Retain a small completed history, without dropping any live worker.
|
|
99
|
+
const completed = [...this.sessions.values()].filter(s => !s.live);
|
|
100
|
+
for (const s of completed.slice(0, Math.max(0, completed.length - 32)))
|
|
101
|
+
this.sessions.delete(s.id);
|
|
102
|
+
this.changed();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let id = actor === "Talos" ? MAIN : this.actors.get(actor);
|
|
106
|
+
if (event.type === "usage") {
|
|
107
|
+
id = this.sessions.has(event.record.sessionId) ? event.record.sessionId
|
|
108
|
+
: event.record.actor ? this.actors.get(event.record.actor) : id;
|
|
109
|
+
}
|
|
110
|
+
else if (event.type === "kingdom") {
|
|
111
|
+
if ("slug" in event)
|
|
112
|
+
id = this.actors.get(event.slug);
|
|
113
|
+
else if (event.phase === "message")
|
|
114
|
+
id = this.actors.get(event.from);
|
|
115
|
+
else
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!id)
|
|
119
|
+
return;
|
|
120
|
+
let s = this.sessions.get(id);
|
|
121
|
+
if (!s)
|
|
122
|
+
return;
|
|
123
|
+
if (event.type === "usage") {
|
|
124
|
+
if (this.seenUsage.has(event.record.id))
|
|
125
|
+
return;
|
|
126
|
+
this.seenUsage.add(event.record.id);
|
|
127
|
+
s = { ...s, ledgerSeen: s.ledgerSeen || !!event.record.usage, ledger: event.record.usage ? add(s.ledger, tokens(event.record.usage)) : s.ledger,
|
|
128
|
+
usageKnown: s.usageKnown || !!event.record.usage, usageMissing: s.usageMissing || !event.record.usage };
|
|
129
|
+
}
|
|
130
|
+
else if (event.type === "turn_end") {
|
|
131
|
+
s = { ...s, fallback: event.usage ? add(s.fallback, tokens(event.usage)) : s.fallback,
|
|
132
|
+
usageKnown: s.usageKnown || !!event.usage };
|
|
133
|
+
}
|
|
134
|
+
else if (event.type === "status")
|
|
135
|
+
s = { ...s, activity: oneLine(event.label) };
|
|
136
|
+
else if (event.type === "turn_start")
|
|
137
|
+
s = { ...s, activity: tr("Elaborazione", "Thinking") };
|
|
138
|
+
else if (event.type === "text")
|
|
139
|
+
s = { ...this.log(s, event.delta, true), activity: tr("Risposta in corso", "Writing") };
|
|
140
|
+
else if (event.type === "tool_call") {
|
|
141
|
+
const args = event.call.input;
|
|
142
|
+
const target = String(args["path"] ?? args["command"] ?? args["pattern"] ?? args["query"] ?? "");
|
|
143
|
+
const activity = `${event.call.name} ${oneLine(target)}`.trim();
|
|
144
|
+
s = { ...this.log(s, `${event.call.name}\n${target}`), activity };
|
|
145
|
+
}
|
|
146
|
+
else if (event.type === "tool_result")
|
|
147
|
+
s = this.log(s, event.result.output);
|
|
148
|
+
else if (event.type === "user_message")
|
|
149
|
+
s = this.log(s, event.text);
|
|
150
|
+
else if (event.type === "error")
|
|
151
|
+
s = { ...this.log(s, event.message), activity: oneLine(event.message) };
|
|
152
|
+
else if (event.type === "done")
|
|
153
|
+
s = { ...s, activity: tr("Concluso", "Finished") };
|
|
154
|
+
else if (event.type === "kingdom") {
|
|
155
|
+
if (event.phase === "usage")
|
|
156
|
+
s = { ...s, fallback: { input: count(event.inputTokens), output: count(event.outputTokens) }, usageKnown: true };
|
|
157
|
+
else if (event.phase === "tool") {
|
|
158
|
+
const activity = `${event.tool} ${oneLine(event.command ?? event.args ?? "")}`.trim();
|
|
159
|
+
s = event.state === "start" ? { ...this.log(s, `${event.tool}\n${event.command ?? event.args ?? ""}`), activity }
|
|
160
|
+
: this.log(s, event.output ?? event.state);
|
|
161
|
+
}
|
|
162
|
+
else if (event.phase === "vassal")
|
|
163
|
+
s = { ...s,
|
|
164
|
+
activity: oneLine(event.label ?? (event.state === "done" ? tr("Concluso", "Finished") : tr("Elaborazione", "Thinking"))) };
|
|
165
|
+
else if (event.phase === "say" || event.phase === "note" || event.phase === "inbox" || event.phase === "message")
|
|
166
|
+
s = this.log(s, event.text);
|
|
167
|
+
else
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
else
|
|
171
|
+
return;
|
|
172
|
+
this.sessions.set(id, s);
|
|
173
|
+
this.changed();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -18,6 +18,7 @@ export interface BottomInputHandlers {
|
|
|
18
18
|
readonly attachmentStyle?: (s: string) => string;
|
|
19
19
|
readonly footerLines: () => readonly string[];
|
|
20
20
|
readonly belowInputLines?: () => readonly string[];
|
|
21
|
+
readonly warningLines?: () => readonly string[];
|
|
21
22
|
readonly completion: (buffer: string) => string | null;
|
|
22
23
|
readonly hint: (buffer: string) => string;
|
|
23
24
|
readonly statusStyle?: (s: string) => string;
|
|
@@ -84,8 +85,12 @@ export declare class BottomInput {
|
|
|
84
85
|
private kingdomBoard;
|
|
85
86
|
private overlay;
|
|
86
87
|
private screen;
|
|
88
|
+
private readonly agentSessions?;
|
|
89
|
+
private sessionsUnsubscribe?;
|
|
90
|
+
private sessionsTimer?;
|
|
87
91
|
private modal;
|
|
88
92
|
private statusLabel;
|
|
93
|
+
private compactionStartedAt;
|
|
89
94
|
private statusTool;
|
|
90
95
|
private roomStatus;
|
|
91
96
|
private roomTimer?;
|
|
@@ -95,6 +100,7 @@ export declare class BottomInput {
|
|
|
95
100
|
private tokensOut;
|
|
96
101
|
private frame;
|
|
97
102
|
private spinnerTimer?;
|
|
103
|
+
private readonly namingNotice;
|
|
98
104
|
private lastLayout?;
|
|
99
105
|
private liveStartRow;
|
|
100
106
|
private paintedRows;
|
|
@@ -129,6 +135,8 @@ export declare class BottomInput {
|
|
|
129
135
|
private finishQueueEdit;
|
|
130
136
|
showReview(section?: string): void;
|
|
131
137
|
private get screenVisible();
|
|
138
|
+
showAgents(): void;
|
|
139
|
+
private sessionArrow;
|
|
132
140
|
static supported(): boolean;
|
|
133
141
|
start(): Promise<void>;
|
|
134
142
|
write(text: string): void;
|
|
@@ -140,6 +148,9 @@ export declare class BottomInput {
|
|
|
140
148
|
setLogLine(index: number, text: string): void;
|
|
141
149
|
setTokens(newInput: number, cache: number, outTok: number): void;
|
|
142
150
|
setStatus(label: string | null): void;
|
|
151
|
+
setCompacting(active: boolean): void;
|
|
152
|
+
showSessionNaming(title: string): void;
|
|
153
|
+
setSessionNaming(phase: "start" | "end", title: string): void;
|
|
143
154
|
getLogLine(index: number): string | undefined;
|
|
144
155
|
isBusy(): boolean;
|
|
145
156
|
setStatusTool(name: string | null): void;
|
package/dist/tui/bottom-input.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { tr } from "./i18n.js";
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
|
-
import { pulseGlyph, pulseToolName } from "./tool-style.js";
|
|
4
3
|
import { fitLine, plainText, terminalWidth, wrapText } from "./terminal-text.js";
|
|
5
4
|
import { PromptQueue } from "./prompt-queue.js";
|
|
6
5
|
import { QueueScreen, queuePreview } from "./queue-screen.js";
|
|
7
6
|
import { createAttachmentHighlighter } from "./attachment-highlight.js";
|
|
8
7
|
import { NativeScrollback } from "./native-scrollback.js";
|
|
9
8
|
import { QueuedQuestions } from "./queued-questions.js";
|
|
9
|
+
import { AgentSessionsScreen } from "./agent-sessions-screen.js";
|
|
10
|
+
import { SessionNamingNotice } from "./session-naming-notice.js";
|
|
10
11
|
const SYNC_ON = "\x1b[?2026h";
|
|
11
12
|
const SYNC_OFF = "\x1b[?2026l";
|
|
12
13
|
const ALT_ON = "\x1b[?1049h";
|
|
@@ -87,10 +88,14 @@ export class BottomInput {
|
|
|
87
88
|
kingdomBoard = null;
|
|
88
89
|
overlay = null;
|
|
89
90
|
screen = null;
|
|
91
|
+
agentSessions;
|
|
92
|
+
sessionsUnsubscribe;
|
|
93
|
+
sessionsTimer;
|
|
90
94
|
// Domanda a risposta libera (tool ask_user): quando attiva, l'editing va alla
|
|
91
95
|
// risposta e Invio/Esc risolvono la promise invece di inviare un prompt.
|
|
92
96
|
modal = null;
|
|
93
97
|
statusLabel = null;
|
|
98
|
+
compactionStartedAt = null;
|
|
94
99
|
// Nome GREZZO del tool attualmente in corso (es. "read_file"): pilota colore e
|
|
95
100
|
// glyph animati nella status-line (pulse abbinato alla categoria). null quando
|
|
96
101
|
// l'attivita' non e' un singolo tool (es. "elaboro", ondata mista).
|
|
@@ -107,6 +112,8 @@ export class BottomInput {
|
|
|
107
112
|
tokensOut = 0;
|
|
108
113
|
frame = 0;
|
|
109
114
|
spinnerTimer;
|
|
115
|
+
namingNotice = new SessionNamingNotice(() => { if (this.active)
|
|
116
|
+
this.render(); });
|
|
110
117
|
lastLayout;
|
|
111
118
|
liveStartRow = 1;
|
|
112
119
|
paintedRows = [];
|
|
@@ -138,6 +145,8 @@ export class BottomInput {
|
|
|
138
145
|
this.h = h;
|
|
139
146
|
this.nativeScroll = h.nativeScroll !== false;
|
|
140
147
|
this.queue = this.createQueue();
|
|
148
|
+
if (h.review)
|
|
149
|
+
this.agentSessions = new AgentSessionsScreen(h.review.sessions, h.queueStyle);
|
|
141
150
|
}
|
|
142
151
|
createQueue() {
|
|
143
152
|
return new PromptQueue(this.h.prepareQueuedInput, () => this.render(), this.h.queuePersistence?.(), this.h.restoreQueuedInput);
|
|
@@ -232,6 +241,18 @@ export class BottomInput {
|
|
|
232
241
|
this.render();
|
|
233
242
|
}
|
|
234
243
|
get screenVisible() { return this.screen !== null && !this.modal && !this.overlay; }
|
|
244
|
+
showAgents() {
|
|
245
|
+
if (!this.agentSessions?.available || this.modal || this.overlay || this.queueEditor || this.queue.held)
|
|
246
|
+
return;
|
|
247
|
+
this.agentSessions.focused = true;
|
|
248
|
+
this.scrollOffset = 0;
|
|
249
|
+
this.render();
|
|
250
|
+
}
|
|
251
|
+
sessionArrow(name) {
|
|
252
|
+
return !!this.agentSessions?.available && !this.modal && !this.overlay && !this.queueEditor
|
|
253
|
+
&& !this.questions.focused && this.scrollOffset === 0
|
|
254
|
+
&& (this.agentSessions.focused || (!this.buffer && name === "down"));
|
|
255
|
+
}
|
|
235
256
|
static supported() {
|
|
236
257
|
return Boolean(stdout.isTTY && stdin.isTTY && typeof stdin.setRawMode === "function");
|
|
237
258
|
}
|
|
@@ -239,6 +260,12 @@ export class BottomInput {
|
|
|
239
260
|
if (!BottomInput.supported())
|
|
240
261
|
return Promise.resolve();
|
|
241
262
|
this.active = true;
|
|
263
|
+
this.sessionsUnsubscribe = this.h.review?.sessions.subscribe(() => this.refresh());
|
|
264
|
+
this.sessionsTimer = setInterval(() => {
|
|
265
|
+
if (this.agentSessions?.available || this.screen === this.agentSessions)
|
|
266
|
+
this.refresh();
|
|
267
|
+
}, 500);
|
|
268
|
+
this.sessionsTimer.unref();
|
|
242
269
|
this.alternateScreen = !this.nativeScroll;
|
|
243
270
|
stdout.write(this.nativeScroll ? "\x1b[2J\x1b[H" + NATIVE_SCROLL_MODE + PASTE_ON
|
|
244
271
|
: ALT_ON + "\x1b[2J\x1b[H" + NATIVE_SELECTION_MODE + PASTE_ON);
|
|
@@ -300,6 +327,7 @@ export class BottomInput {
|
|
|
300
327
|
}
|
|
301
328
|
// Forget the previous session's display and editor state before any replay.
|
|
302
329
|
clearConversation() {
|
|
330
|
+
this.namingNotice.clear();
|
|
303
331
|
this.questions.clear();
|
|
304
332
|
this.inputGeneration++;
|
|
305
333
|
this.buffer = "";
|
|
@@ -391,6 +419,8 @@ export class BottomInput {
|
|
|
391
419
|
this.render();
|
|
392
420
|
}
|
|
393
421
|
setStatus(label) {
|
|
422
|
+
if (label === null)
|
|
423
|
+
this.compactionStartedAt = null;
|
|
394
424
|
this.statusLabel = label;
|
|
395
425
|
if (label !== null) {
|
|
396
426
|
if (!this.spinnerTimer)
|
|
@@ -403,6 +433,19 @@ export class BottomInput {
|
|
|
403
433
|
if (this.active)
|
|
404
434
|
this.render();
|
|
405
435
|
}
|
|
436
|
+
setCompacting(active) {
|
|
437
|
+
this.compactionStartedAt = active ? this.compactionStartedAt ?? Date.now() : null;
|
|
438
|
+
this.setStatus(this.statusLabel ?? (active ? "Compacting context" : null));
|
|
439
|
+
}
|
|
440
|
+
showSessionNaming(title) {
|
|
441
|
+
this.namingNotice.show(title);
|
|
442
|
+
}
|
|
443
|
+
setSessionNaming(phase, title) {
|
|
444
|
+
if (phase === "start")
|
|
445
|
+
this.namingNotice.start();
|
|
446
|
+
else
|
|
447
|
+
this.namingNotice.finish(title);
|
|
448
|
+
}
|
|
406
449
|
getLogLine(index) { return this.log[index]; }
|
|
407
450
|
// Stato leggibile dall'host per evitare una seconda telemetria statica mentre
|
|
408
451
|
// la riga animata del run sta gia' mostrando token, tempo e coda.
|
|
@@ -775,6 +818,9 @@ export class BottomInput {
|
|
|
775
818
|
render(force = false) {
|
|
776
819
|
if (!this.active || this.resizeTimer || (this.paused && !this.overlay))
|
|
777
820
|
return;
|
|
821
|
+
this.agentSessions?.sync();
|
|
822
|
+
if (this.agentSessions && this.screen === this.agentSessions && this.agentSessions.activeId === "main")
|
|
823
|
+
this.screen = null;
|
|
778
824
|
if (this.scrollRenderTimer) {
|
|
779
825
|
clearTimeout(this.scrollRenderTimer);
|
|
780
826
|
this.scrollRenderTimer = undefined;
|
|
@@ -790,8 +836,10 @@ export class BottomInput {
|
|
|
790
836
|
force = true;
|
|
791
837
|
}
|
|
792
838
|
if (this.screenVisible) {
|
|
793
|
-
const
|
|
794
|
-
const
|
|
839
|
+
const warnings = (this.h.warningLines?.() ?? []).slice(0, Math.max(0, H - 3));
|
|
840
|
+
const bodyHeight = H - warnings.length;
|
|
841
|
+
const content = this.screen.render(W, bodyHeight);
|
|
842
|
+
const rows = Array.from({ length: H }, (_, i) => fitLine((i < bodyHeight ? content[i] : warnings[i - bodyHeight]) ?? "", Math.max(1, W - 1)));
|
|
795
843
|
this.paint(rows, W, H, 1, 1, force, true);
|
|
796
844
|
return;
|
|
797
845
|
}
|
|
@@ -809,9 +857,14 @@ export class BottomInput {
|
|
|
809
857
|
}
|
|
810
858
|
const lay = this.liveContent();
|
|
811
859
|
this.lastLayout = lay;
|
|
860
|
+
if (this.overlay && lay.lines.length >= H - 1) {
|
|
861
|
+
const frame = Array.from({ length: H }, (_, i) => fitLine(lay.lines[i] ?? "", W - 1));
|
|
862
|
+
this.paint(frame, W, H, Math.min(H, lay.cursorRow + 1), lay.cursorCol, force, true);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
812
865
|
if (this.nativeScroll && !alternate) {
|
|
813
866
|
this.renderedLog();
|
|
814
|
-
const output = this.nativeOutput.render(this.renderedEntries, lay, W, H, false, this.h.startupLines?.() ?? this.h.headerLines?.() ?? []);
|
|
867
|
+
const output = this.nativeOutput.render(this.renderedEntries, lay, W, H, false, this.h.startupLines?.() ?? this.h.headerLines?.() ?? [], chunk => { stdout.write(chunk); });
|
|
815
868
|
if (output)
|
|
816
869
|
stdout.write(output);
|
|
817
870
|
this.lastPaintAt = Date.now();
|
|
@@ -894,6 +947,8 @@ export class BottomInput {
|
|
|
894
947
|
const lines = [];
|
|
895
948
|
if (this.statusLabel !== null)
|
|
896
949
|
lines.push(this.statusLine());
|
|
950
|
+
if (this.compactionStartedAt !== null)
|
|
951
|
+
lines.push(fitLine(" Making room to continue…", cols - 1));
|
|
897
952
|
if (this.statusLabel === null && this.roomStatus?.state === "failed") {
|
|
898
953
|
const notice = this.roomSegment().replace(/\s+/g, " ").trim();
|
|
899
954
|
lines.push(...wrapText(notice, Math.max(8, cols - 3)).slice(0, 3).map(line => ` ${line}`));
|
|
@@ -902,6 +957,9 @@ export class BottomInput {
|
|
|
902
957
|
// La riga live descrive gia' il team; la board completa si consulta a parte.
|
|
903
958
|
if (this.kingdomBoard && this.statusLabel === null)
|
|
904
959
|
lines.push(fitLine(this.kingdomBoard[0] ?? "", cols - 1));
|
|
960
|
+
if (!this.modal && !this.queueEditor && !this.questions.focused) {
|
|
961
|
+
lines.push(...(this.agentSessions?.preview(cols, Math.min(5, Math.max(2, this.rows() - 12))) ?? []));
|
|
962
|
+
}
|
|
905
963
|
for (const f of this.h.footerLines())
|
|
906
964
|
lines.push(f);
|
|
907
965
|
if (!this.modal && !this.queueEditor)
|
|
@@ -960,9 +1018,14 @@ export class BottomInput {
|
|
|
960
1018
|
lines.push(fitLine(this.h.statusStyle?.(notice) ?? notice, cols - 1));
|
|
961
1019
|
}
|
|
962
1020
|
if (!this.modal) {
|
|
1021
|
+
for (const naming of this.namingNotice.lines()) {
|
|
1022
|
+
lines.push(fitLine(" " + (this.h.statusStyle?.(naming) ?? naming), cols - 1));
|
|
1023
|
+
}
|
|
963
1024
|
for (const row of this.h.belowInputLines?.() ?? [])
|
|
964
1025
|
lines.push(fitLine(row, cols - 1));
|
|
965
1026
|
}
|
|
1027
|
+
for (const row of this.h.warningLines?.() ?? [])
|
|
1028
|
+
lines.push(fitLine(row, cols - 1));
|
|
966
1029
|
const action = this.ctrlCConfirmation?.action;
|
|
967
1030
|
const warning = action === "stop" ? tr("fermare il lavoro e mettere in pausa la coda", "stop work and pause the queue") : action === "clear" ? tr("svuotare la bozza", "clear the draft") : tr("uscire", "exit");
|
|
968
1031
|
const hint = action
|
|
@@ -998,14 +1061,19 @@ export class BottomInput {
|
|
|
998
1061
|
return style(`${glyph} ${label}`);
|
|
999
1062
|
}
|
|
1000
1063
|
statusLine() {
|
|
1064
|
+
if (this.compactionStartedAt !== null) {
|
|
1065
|
+
const seconds = Math.floor((Date.now() - this.compactionStartedAt) / 1000);
|
|
1066
|
+
return fitLine(` ${SPINNER[this.frame % SPINNER.length]} Compacting context (${seconds}s · esc to interrupt)`, this.cols() - 1);
|
|
1067
|
+
}
|
|
1001
1068
|
const elapsed = Math.round((Date.now() - this.startedAt) / 1000);
|
|
1002
1069
|
const style = this.h.statusStyle ?? ((s) => s);
|
|
1003
1070
|
const label = this.statusLabel ?? tr("Elaborazione", "Working");
|
|
1004
|
-
const glyph =
|
|
1005
|
-
const name =
|
|
1006
|
-
const meta = [
|
|
1007
|
-
const
|
|
1008
|
-
? tr("Alt+S subito · Esc ferma", "Alt+S send now · Esc stop") : tr("Esc ferma", "Esc stop")
|
|
1071
|
+
const glyph = style(SPINNER[this.frame % SPINNER.length]);
|
|
1072
|
+
const name = style(label);
|
|
1073
|
+
const meta = [this.h.runningHint?.() ?? "", this.roomSegment()].filter(Boolean);
|
|
1074
|
+
const shortcut = this.cols() >= 100 && (!this.queue.length || this.modal || this.queueEditor)
|
|
1075
|
+
? tr("Alt+S subito · Esc ferma", "Alt+S send now · Esc stop") : tr("Esc ferma", "Esc stop");
|
|
1076
|
+
const right = style(`${fmtElapsed(elapsed).padStart(7)} · ${shortcut}`);
|
|
1009
1077
|
const budget = Math.max(0, this.cols() - terminalWidth(right) - 5);
|
|
1010
1078
|
let left = ` ${glyph} ${name}`;
|
|
1011
1079
|
for (const segment of meta) {
|
|
@@ -1188,7 +1256,7 @@ export class BottomInput {
|
|
|
1188
1256
|
}
|
|
1189
1257
|
// Freccia su/giu' NUDA: disambigua scroll(touchpad) vs pressione (timer).
|
|
1190
1258
|
if ((name === "up" || name === "down") && !ctrl && !meta && !shift && !this.screenVisible
|
|
1191
|
-
&& !this.questions.focused && (!this.nativeScroll || this.scrollOffset > 0)) {
|
|
1259
|
+
&& !this.questions.focused && !this.sessionArrow(name) && (!this.nativeScroll || this.scrollOffset > 0)) {
|
|
1192
1260
|
this.onArrow(name);
|
|
1193
1261
|
return;
|
|
1194
1262
|
}
|
|
@@ -1268,6 +1336,25 @@ export class BottomInput {
|
|
|
1268
1336
|
this.showQueue();
|
|
1269
1337
|
return;
|
|
1270
1338
|
}
|
|
1339
|
+
if (k.meta && k.name === "a") {
|
|
1340
|
+
this.showAgents();
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (!k.ctrl && !k.meta && !k.shift && this.sessionArrow(k.name ?? "") && (k.name === "up" || k.name === "down")) {
|
|
1344
|
+
this.agentSessions.key(s, k);
|
|
1345
|
+
this.render();
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (this.agentSessions?.focused && !this.modal && !this.overlay && !this.queueEditor) {
|
|
1349
|
+
if (k.name === "return" || k.name === "enter") {
|
|
1350
|
+
this.agentSessions.key(s, k);
|
|
1351
|
+
if (this.agentSessions.activeId !== "main")
|
|
1352
|
+
this.screen = this.agentSessions;
|
|
1353
|
+
this.render();
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
this.agentSessions.focused = false;
|
|
1357
|
+
}
|
|
1271
1358
|
if (k.meta && k.name === "t" && !this.modal && !this.overlay && !this.queueEditor) {
|
|
1272
1359
|
if (this.queue.sendToTeam())
|
|
1273
1360
|
this.startNextQueued();
|
|
@@ -1692,6 +1779,10 @@ export class BottomInput {
|
|
|
1692
1779
|
this.showQueue();
|
|
1693
1780
|
return;
|
|
1694
1781
|
}
|
|
1782
|
+
if (/^\/(?:agents|agenti)\s*$/i.test(line.trim())) {
|
|
1783
|
+
this.showAgents();
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1695
1786
|
const review = /^\/(?:review|revisione)(?:\s+(\S+))?\s*$/i.exec(line.trim());
|
|
1696
1787
|
if (review && this.h.createReviewScreen) {
|
|
1697
1788
|
this.showReview(review[1]);
|
|
@@ -1768,6 +1859,10 @@ export class BottomInput {
|
|
|
1768
1859
|
if (!this.active)
|
|
1769
1860
|
return;
|
|
1770
1861
|
this.active = false;
|
|
1862
|
+
this.namingNotice.clear();
|
|
1863
|
+
this.sessionsUnsubscribe?.();
|
|
1864
|
+
if (this.sessionsTimer)
|
|
1865
|
+
clearInterval(this.sessionsTimer);
|
|
1771
1866
|
this.questions.clear();
|
|
1772
1867
|
this.clearPendingEscape();
|
|
1773
1868
|
this.cancelCtrlC();
|
|
@@ -1802,6 +1897,7 @@ export class BottomInput {
|
|
|
1802
1897
|
if (this.nativeScroll)
|
|
1803
1898
|
stdout.write(this.nativeOutput.release());
|
|
1804
1899
|
stdout.write(PASTE_OFF + NATIVE_SCROLL_MODE + "\x1b[?25h");
|
|
1900
|
+
this.nativeOutput.dispose();
|
|
1805
1901
|
if (typeof stdin.setRawMode === "function")
|
|
1806
1902
|
stdin.setRawMode(false);
|
|
1807
1903
|
this.h.onQuit();
|