@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.
Files changed (55) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +37 -12
  3. package/dist/core/agent.js +58 -38
  4. package/dist/core/session-naming.d.ts +3 -0
  5. package/dist/core/session-naming.js +43 -0
  6. package/dist/core/session-usage.js +4 -1
  7. package/dist/core/session.d.ts +1 -0
  8. package/dist/core/session.js +9 -3
  9. package/dist/core/storage-health.d.ts +24 -0
  10. package/dist/core/storage-health.js +72 -0
  11. package/dist/core/types.d.ts +17 -0
  12. package/dist/tui/agent-sessions-screen.d.ts +25 -0
  13. package/dist/tui/agent-sessions-screen.js +132 -0
  14. package/dist/tui/agent-sessions.d.ts +39 -0
  15. package/dist/tui/agent-sessions.js +175 -0
  16. package/dist/tui/bottom-input.d.ts +11 -0
  17. package/dist/tui/bottom-input.js +106 -10
  18. package/dist/tui/commands.js +2 -1
  19. package/dist/tui/i18n-catalog.js +39 -1
  20. package/dist/tui/logo.js +3 -0
  21. package/dist/tui/model-picker.d.ts +2 -0
  22. package/dist/tui/model-picker.js +49 -0
  23. package/dist/tui/native-scrollback.d.ts +2 -1
  24. package/dist/tui/native-scrollback.js +26 -6
  25. package/dist/tui/prompt-queue-store.d.ts +1 -0
  26. package/dist/tui/prompt-queue-store.js +4 -0
  27. package/dist/tui/queued-questions.js +2 -4
  28. package/dist/tui/quick-help.d.ts +1 -1
  29. package/dist/tui/quick-help.js +3 -0
  30. package/dist/tui/repl.d.ts +3 -0
  31. package/dist/tui/repl.js +110 -24
  32. package/dist/tui/review-store.d.ts +7 -1
  33. package/dist/tui/review-store.js +36 -4
  34. package/dist/tui/scrollback-archive.d.ts +16 -0
  35. package/dist/tui/scrollback-archive.js +90 -0
  36. package/dist/tui/selector.d.ts +4 -0
  37. package/dist/tui/selector.js +9 -2
  38. package/dist/tui/session-naming-notice.d.ts +14 -0
  39. package/dist/tui/session-naming-notice.js +57 -0
  40. package/dist/tui/session-picker.d.ts +2 -0
  41. package/dist/tui/session-picker.js +32 -0
  42. package/dist/tui/storage-warning.d.ts +2 -0
  43. package/dist/tui/storage-warning.js +35 -0
  44. package/dist/vascend/context-feedback-loop.js +2 -0
  45. package/dist/vascend/context-snapshot.d.ts +1 -0
  46. package/dist/vascend/context-snapshot.js +4 -3
  47. package/dist/vascend/context-window-compiler.d.ts +1 -0
  48. package/dist/vascend/context-window-compiler.js +1 -0
  49. package/dist/vascend/granular-kingdom.js +32 -4
  50. package/dist/vascend/kingdom.js +4 -0
  51. package/dist/vascend/memory-store.d.ts +1 -0
  52. package/dist/vascend/memory-store.js +12 -4
  53. package/dist/vascend/room-memory.d.ts +3 -1
  54. package/dist/vascend/room-memory.js +6 -4
  55. 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;
@@ -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 content = this.screen.render(W, H);
794
- const rows = Array.from({ length: H }, (_, i) => fitLine(content[i] ?? "", Math.max(1, W - 1)));
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 = this.statusTool ? pulseGlyph(this.statusTool, this.frame) : SPINNER[this.frame % SPINNER.length];
1005
- const name = this.statusTool ? pulseToolName(this.statusTool, label, this.frame) : style(label);
1006
- const meta = [elapsed >= 1 ? fmtElapsed(elapsed) : "", this.h.runningHint?.() ?? "", this.roomSegment()].filter(Boolean);
1007
- const right = style(this.cols() >= 64 && (!this.queue.length || this.modal || this.queueEditor)
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();