@vascend/talos 0.1.5 → 0.1.6

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 (224) hide show
  1. package/dist/core/agent.d.ts +16 -3
  2. package/dist/core/agent.js +114 -34
  3. package/dist/core/background.d.ts +10 -3
  4. package/dist/core/background.js +55 -9
  5. package/dist/core/bootstrap.js +1 -0
  6. package/dist/core/compaction.js +1 -1
  7. package/dist/core/config-storage.d.ts +5 -0
  8. package/dist/core/config-storage.js +83 -0
  9. package/dist/core/config.d.ts +11 -2
  10. package/dist/core/config.js +24 -8
  11. package/dist/core/context-ledger.js +2 -1
  12. package/dist/core/context-policy.d.ts +1 -1
  13. package/dist/core/context-policy.js +10 -7
  14. package/dist/core/credentials.js +29 -30
  15. package/dist/core/effort.d.ts +1 -1
  16. package/dist/core/effort.js +5 -1
  17. package/dist/core/execution-guidance.d.ts +1 -1
  18. package/dist/core/execution-guidance.js +2 -0
  19. package/dist/core/file-coordination.d.ts +3 -0
  20. package/dist/core/file-coordination.js +28 -0
  21. package/dist/core/file-lease-store.d.ts +3 -0
  22. package/dist/core/file-lease-store.js +28 -12
  23. package/dist/core/fusion.d.ts +24 -0
  24. package/dist/core/fusion.js +80 -0
  25. package/dist/core/model-limits.d.ts +1 -0
  26. package/dist/core/model-limits.js +5 -1
  27. package/dist/core/permission-queue.js +15 -15
  28. package/dist/core/request-plan.js +4 -3
  29. package/dist/core/session-naming.js +4 -0
  30. package/dist/core/session.d.ts +4 -2
  31. package/dist/core/session.js +13 -4
  32. package/dist/core/settings.d.ts +2 -0
  33. package/dist/core/settings.js +5 -16
  34. package/dist/core/step-policy.d.ts +18 -0
  35. package/dist/core/step-policy.js +6 -0
  36. package/dist/core/tool-artifacts.d.ts +2 -0
  37. package/dist/core/tool-artifacts.js +21 -3
  38. package/dist/core/tool-result-index.js +29 -33
  39. package/dist/core/tool-result-store.js +6 -5
  40. package/dist/core/types.d.ts +10 -3
  41. package/dist/core/user-inbox.d.ts +9 -0
  42. package/dist/core/user-inbox.js +22 -3
  43. package/dist/core/user-input.js +1 -1
  44. package/dist/fusion/autopilot-fast.d.ts +59 -0
  45. package/dist/fusion/autopilot-fast.js +213 -0
  46. package/dist/fusion/autopilot.d.ts +75 -0
  47. package/dist/fusion/autopilot.js +308 -0
  48. package/dist/fusion/focus.d.ts +30 -0
  49. package/dist/fusion/focus.js +122 -0
  50. package/dist/fusion/gate.d.ts +30 -0
  51. package/dist/fusion/gate.js +95 -0
  52. package/dist/fusion/jev.d.ts +71 -0
  53. package/dist/fusion/jev.js +164 -0
  54. package/dist/fusion/receipts.d.ts +25 -0
  55. package/dist/fusion/receipts.js +34 -0
  56. package/dist/fusion/route.d.ts +25 -0
  57. package/dist/fusion/route.js +84 -0
  58. package/dist/fusion/run.d.ts +4 -0
  59. package/dist/fusion/run.js +186 -0
  60. package/dist/horizon/engine.d.ts +20 -1
  61. package/dist/horizon/engine.js +57 -0
  62. package/dist/horizon/index.d.ts +2 -2
  63. package/dist/horizon/index.js +1 -1
  64. package/dist/horizon/types.d.ts +29 -0
  65. package/dist/index.js +12 -0
  66. package/dist/providers/anthropic-oauth.d.ts +8 -3
  67. package/dist/providers/anthropic-oauth.js +13 -10
  68. package/dist/providers/anthropic.d.ts +9 -2
  69. package/dist/providers/anthropic.js +64 -21
  70. package/dist/providers/cli-models.d.ts +30 -0
  71. package/dist/providers/cli-models.js +194 -0
  72. package/dist/providers/model-effort.js +1 -1
  73. package/dist/providers/openai-oauth.d.ts +8 -3
  74. package/dist/providers/openai-oauth.js +9 -3
  75. package/dist/providers/openrouter-manual-fallback.d.ts +3 -1
  76. package/dist/providers/openrouter-manual-fallback.js +20 -20
  77. package/dist/providers/registry.js +3 -2
  78. package/dist/providers/retry.d.ts +3 -0
  79. package/dist/providers/retry.js +44 -1
  80. package/dist/remote/cifratura.d.ts +12 -0
  81. package/dist/remote/cifratura.js +66 -0
  82. package/dist/remote/index.d.ts +15 -0
  83. package/dist/remote/index.js +65 -0
  84. package/dist/remote/relay-client.d.ts +58 -0
  85. package/dist/remote/relay-client.js +315 -0
  86. package/dist/remote/sessione-remota.d.ts +60 -0
  87. package/dist/remote/sessione-remota.js +266 -0
  88. package/dist/sdk.d.ts +7 -2
  89. package/dist/sdk.js +3 -1
  90. package/dist/tools/builtin.js +41 -20
  91. package/dist/tools/capabilities.d.ts +3 -1
  92. package/dist/tools/capabilities.js +5 -2
  93. package/dist/tools/computer-use.js +2 -1
  94. package/dist/tools/pagination.js +1 -1
  95. package/dist/tools/program.js +34 -21
  96. package/dist/tools/team-tools.js +11 -6
  97. package/dist/tui/account-entry.js +14 -9
  98. package/dist/tui/agent-label.d.ts +2 -0
  99. package/dist/tui/agent-label.js +5 -0
  100. package/dist/tui/agent-sessions-screen.d.ts +12 -2
  101. package/dist/tui/agent-sessions-screen.js +62 -16
  102. package/dist/tui/agent-sessions.d.ts +14 -0
  103. package/dist/tui/agent-sessions.js +67 -6
  104. package/dist/tui/bottom-input.d.ts +46 -2
  105. package/dist/tui/bottom-input.js +358 -59
  106. package/dist/tui/branding.js +8 -4
  107. package/dist/tui/capability-lists.d.ts +10 -0
  108. package/dist/tui/capability-lists.js +41 -0
  109. package/dist/tui/command-output-registry.js +4 -0
  110. package/dist/tui/command-transcript.d.ts +1 -1
  111. package/dist/tui/command-transcript.js +17 -21
  112. package/dist/tui/commands.js +4 -1
  113. package/dist/tui/compaction-status.d.ts +7 -0
  114. package/dist/tui/compaction-status.js +39 -0
  115. package/dist/tui/config-recovery.d.ts +12 -0
  116. package/dist/tui/config-recovery.js +54 -0
  117. package/dist/tui/context-meter.d.ts +3 -0
  118. package/dist/tui/context-meter.js +35 -8
  119. package/dist/tui/diff.d.ts +9 -0
  120. package/dist/tui/diff.js +43 -19
  121. package/dist/tui/fusion-menu.d.ts +5 -0
  122. package/dist/tui/fusion-menu.js +89 -0
  123. package/dist/tui/i18n-catalog.js +312 -1
  124. package/dist/tui/live-command.d.ts +1 -0
  125. package/dist/tui/live-command.js +9 -0
  126. package/dist/tui/live-output.d.ts +1 -0
  127. package/dist/tui/live-output.js +11 -0
  128. package/dist/tui/model-details.js +13 -12
  129. package/dist/tui/model-picker.js +17 -7
  130. package/dist/tui/model-selection.js +19 -10
  131. package/dist/tui/native-scrollback.js +5 -0
  132. package/dist/tui/prompt-queue.d.ts +3 -0
  133. package/dist/tui/prompt-queue.js +19 -1
  134. package/dist/tui/provider-label.d.ts +2 -0
  135. package/dist/tui/provider-label.js +9 -0
  136. package/dist/tui/provider-menu.js +25 -24
  137. package/dist/tui/queue-screen.js +2 -1
  138. package/dist/tui/remote-command.d.ts +7 -0
  139. package/dist/tui/remote-command.js +79 -0
  140. package/dist/tui/repl.d.ts +8 -2
  141. package/dist/tui/repl.js +355 -311
  142. package/dist/tui/review-store.d.ts +1 -0
  143. package/dist/tui/review-store.js +28 -7
  144. package/dist/tui/review-team.js +4 -3
  145. package/dist/tui/runtime-error.d.ts +2 -0
  146. package/dist/tui/runtime-error.js +26 -0
  147. package/dist/tui/selector-mouse.d.ts +1 -0
  148. package/dist/tui/selector-mouse.js +1 -1
  149. package/dist/tui/selector.d.ts +7 -0
  150. package/dist/tui/selector.js +19 -2
  151. package/dist/tui/session-controls.d.ts +26 -0
  152. package/dist/tui/session-controls.js +65 -0
  153. package/dist/tui/session-naming-notice.js +5 -3
  154. package/dist/tui/session-replay.d.ts +1 -0
  155. package/dist/tui/session-replay.js +101 -32
  156. package/dist/tui/session-transcript-layout.d.ts +9 -0
  157. package/dist/tui/session-transcript-layout.js +41 -0
  158. package/dist/tui/startup.js +63 -33
  159. package/dist/tui/status-line.d.ts +1 -1
  160. package/dist/tui/status-line.js +20 -12
  161. package/dist/tui/talos-art-data.d.ts +1 -0
  162. package/dist/tui/talos-art-data.js +2 -0
  163. package/dist/tui/talos-art.d.ts +1 -0
  164. package/dist/tui/talos-art.js +9 -0
  165. package/dist/tui/task-labels.d.ts +1 -0
  166. package/dist/tui/task-labels.js +8 -0
  167. package/dist/tui/terminal-text.d.ts +2 -1
  168. package/dist/tui/terminal-text.js +28 -2
  169. package/dist/tui/theme.js +2 -2
  170. package/dist/tui/tool-presentation.d.ts +3 -0
  171. package/dist/tui/tool-presentation.js +98 -0
  172. package/dist/tui/tool-style.js +4 -0
  173. package/dist/tui/transcript-store.d.ts +25 -0
  174. package/dist/tui/transcript-store.js +151 -0
  175. package/dist/tui/usage-footer.d.ts +1 -1
  176. package/dist/tui/usage-footer.js +3 -1
  177. package/dist/tui/vascend-id.d.ts +49 -0
  178. package/dist/tui/vascend-id.js +321 -0
  179. package/dist/tui/web-login-screen.js +4 -3
  180. package/dist/tui/web-login.d.ts +7 -15
  181. package/dist/tui/web-login.js +38 -78
  182. package/dist/tui/worker-transcripts.d.ts +31 -0
  183. package/dist/tui/worker-transcripts.js +91 -0
  184. package/dist/vascend/candidate-collector.d.ts +6 -5
  185. package/dist/vascend/candidate-collector.js +44 -43
  186. package/dist/vascend/context-lint.js +32 -2
  187. package/dist/vascend/context-market-selection.js +12 -5
  188. package/dist/vascend/context-pack-render.js +10 -4
  189. package/dist/vascend/context-packet-mirrors.d.ts +8 -0
  190. package/dist/vascend/context-packet-mirrors.js +24 -0
  191. package/dist/vascend/context-plan-events.d.ts +4 -0
  192. package/dist/vascend/context-plan-events.js +44 -0
  193. package/dist/vascend/context-snapshot.js +4 -2
  194. package/dist/vascend/context-store-reader.d.ts +20 -0
  195. package/dist/vascend/context-store-reader.js +46 -0
  196. package/dist/vascend/file-lock.d.ts +1 -0
  197. package/dist/vascend/file-lock.js +26 -0
  198. package/dist/vascend/granular-kingdom.d.ts +1 -0
  199. package/dist/vascend/granular-kingdom.js +22 -15
  200. package/dist/vascend/kingdom-bus.d.ts +1 -0
  201. package/dist/vascend/kingdom-bus.js +3 -0
  202. package/dist/vascend/kingdom-channel.d.ts +2 -0
  203. package/dist/vascend/kingdom-channel.js +3 -0
  204. package/dist/vascend/kingdom-live.d.ts +32 -4
  205. package/dist/vascend/kingdom-live.js +149 -13
  206. package/dist/vascend/kingdom-run.d.ts +5 -1
  207. package/dist/vascend/kingdom-run.js +6 -2
  208. package/dist/vascend/kingdom.d.ts +3 -0
  209. package/dist/vascend/kingdom.js +20 -13
  210. package/dist/vascend/memory-packet.d.ts +2 -1
  211. package/dist/vascend/memory-packet.js +31 -44
  212. package/dist/vascend/plan-store.d.ts +3 -1
  213. package/dist/vascend/plan-store.js +6 -4
  214. package/dist/vascend/room-memory.d.ts +10 -1
  215. package/dist/vascend/room-memory.js +42 -2
  216. package/dist/vascend/runtime.d.ts +3 -11
  217. package/dist/vascend/runtime.js +11 -123
  218. package/dist/vascend/session-search.js +2 -0
  219. package/dist/vascend/team-session.js +3 -2
  220. package/dist/vascend/vision.d.ts +3 -1
  221. package/dist/vascend/vision.js +6 -4
  222. package/dist/vascend/worker-session.d.ts +20 -0
  223. package/dist/vascend/worker-session.js +83 -0
  224. package/package.json +1 -1
@@ -3,11 +3,12 @@ import { renderMarkdown } from "./markdown.js";
3
3
  import { tokenAnsi } from "./theme.js";
4
4
  import { fitLine, padLine, plainText, wrapText as wrapTerminalText } from "./terminal-text.js";
5
5
  import { createAttachmentHighlighter } from "./attachment-highlight.js";
6
+ import { tr } from "./i18n.js";
6
7
  const RESET = "\x1b[0m";
7
8
  const SILVER = "\x1b[38;2;210;214;220m";
8
9
  const DARK_SILVER = "\x1b[38;2;104;111;122m";
9
10
  const CHAT_TEXT = "\x1b[38;2;232;235;239m";
10
- const CHAT_BG = "\x1b[48;2;29;41;56m";
11
+ const CHAT_BG = "\x1b[48;2;43;43;43m";
11
12
  const ANSI = {
12
13
  blue: "\x1b[34m",
13
14
  cyan: "\x1b[36m",
@@ -94,7 +95,10 @@ export function makeTheme(ui, spec) {
94
95
  const code = `${composerBg}${fg("userMessageText")}`;
95
96
  return wrap(code, s.replace(/\x1b\[0m/g, RESET + code));
96
97
  },
97
- selected: (s) => wrap(`${bg("selectedBg")}${fg("text")}\x1b[1m`, s),
98
+ selected: (s) => {
99
+ const code = `${bg("selectedBg")}${fg("text")}\x1b[1m`;
100
+ return wrap(code, s.replace(/\x1b\[0m/g, RESET + code));
101
+ },
98
102
  dark: (s) => wrap(fg("muted"), s),
99
103
  diffAdded: (s) => wrap(`${bg("toolDiffAddedBg")}${fg("toolDiffAdded")}`, s),
100
104
  diffBorder: (s) => wrap(fg("toolDiffBorder"), s),
@@ -201,7 +205,7 @@ export function renderAssistantSegment(text, theme, columns, imageCtx, _kind = "
201
205
  ].join("\n") + "\n";
202
206
  }
203
207
  export function renderAssistantTurnEnd(theme, sequence, status = "completed") {
204
- return status === "error" ? theme.red(" × Risposta interrotta\n") : "";
208
+ return status === "error" ? theme.red(` × ${tr("Risposta interrotta", "Response interrupted")}\n`) : "";
205
209
  }
206
210
  // Compatibilita' con i renderer esterni: una chat box isolata e' una risposta
207
211
  // completa, mentre il REPL usa renderAssistantSegment per la gerarchia live.
@@ -332,7 +336,7 @@ export function renderUserMessage(text, theme, columns, label) {
332
336
  const start = source.indexOf(line, offset);
333
337
  const highlighted = highlight(line, start);
334
338
  offset = start + line.length;
335
- return " " + theme.chatText(padLine((index === 0 ? "› " : " ") + highlighted, width));
339
+ return " " + theme.chatText((index === 0 ? "› " : " ") + highlighted + " ");
336
340
  }).join("\n") + "\n";
337
341
  }
338
342
  // Badge di un'immagine allegata, stile artifact Claude Code: un riquadro con
@@ -0,0 +1,10 @@
1
+ import type { Skill } from "../skills/skills.js";
2
+ import type { ToolRegistry } from "../tools/registry.js";
3
+ export declare function skillList(skills: readonly Skill[], commandsEnabled: boolean): {
4
+ title: string;
5
+ lines: string[];
6
+ };
7
+ export declare function mcpList(tools: ToolRegistry): {
8
+ title: string;
9
+ lines: string[];
10
+ };
@@ -0,0 +1,41 @@
1
+ import { plainText } from "./terminal-text.js";
2
+ import { tr } from "./i18n.js";
3
+ const line = (value) => plainText(value).replace(/\s+/g, " ").trim();
4
+ export function skillList(skills, commandsEnabled) {
5
+ const lines = [];
6
+ if (!skills.length)
7
+ lines.push(tr("Nessuna skill caricata in questa sessione.", "No skills loaded in this session."));
8
+ else if (!commandsEnabled)
9
+ lines.push(tr("I comandi delle skill sono disabilitati.", "Skill commands are disabled."));
10
+ for (const skill of [...skills].sort((a, b) => a.name.localeCompare(b.name))) {
11
+ lines.push("", line(skill.name), ` ${line(skill.description)}`, ` ${line(skill.path)}`);
12
+ if (commandsEnabled)
13
+ lines.push(` /skill:${skill.name}`);
14
+ if (skill.disableModelInvocation)
15
+ lines.push(" " + tr("Solo invocazione manuale", "Manual invocation only"));
16
+ }
17
+ return { title: tr("Skill disponibili ({count})", "Available skills ({count})", { count: skills.length }), lines };
18
+ }
19
+ export function mcpList(tools) {
20
+ const servers = new Map();
21
+ // Read the full runtime registry, including deferred tools. Registration
22
+ // does not imply that a remote server is reachable or authenticated.
23
+ for (const name of tools.names()) {
24
+ const match = /^mcp__(.+?)__(.+)$/.exec(name) ?? /^([\w-]+)\.([\w.-]+)$/.exec(name);
25
+ if (!match)
26
+ continue;
27
+ const entries = servers.get(match[1]) ?? [];
28
+ entries.push({ name: match[2], description: tools.get(name)?.description ?? "" });
29
+ servers.set(match[1], entries);
30
+ }
31
+ const lines = [servers.size
32
+ ? tr("Strumenti registrati nella sessione; lo stato della connessione non viene verificato.", "Tools registered in this session; connection status is not checked.")
33
+ : tr("Nessun server MCP con strumenti registrati in questa sessione.", "No MCP servers with registered tools in this session.")];
34
+ for (const [server, entries] of [...servers].sort(([a], [b]) => a.localeCompare(b))) {
35
+ lines.push("", tr("{name} ({count} strumenti)", "{name} ({count} tools)", { name: line(server), count: entries.length }));
36
+ for (const tool of entries.sort((a, b) => a.name.localeCompare(b.name))) {
37
+ lines.push(` ${line(tool.name)}`, ...(tool.description ? [` ${line(tool.description)}`] : []));
38
+ }
39
+ }
40
+ return { title: tr("Server MCP ({count})", "MCP servers ({count})", { count: servers.size }), lines };
41
+ }
@@ -11,9 +11,12 @@ import { BUILTIN_COMMANDS } from "./commands.js";
11
11
  // Inventario completo: una voce per ciascun comando builtin canonico.
12
12
  // Classificazione derivata dall'analisi dei case di dispatch in repl.ts.
13
13
  const ENTRIES = [
14
+ { name: "skills", category: "sistema", output: "navigable", mechanism: "panel", argDependent: false, longOutput: true, note: "skill caricate, descrizioni, percorsi e comandi in un pannello temporaneo" },
15
+ { name: "mcp", category: "sistema", output: "navigable", mechanism: "panel", argDependent: false, longOutput: true, note: "server e strumenti MCP registrati, inclusi quelli on demand" },
14
16
  { name: "fast", category: "modello", output: "transient", mechanism: "notice", argDependent: true, longOutput: false, note: "imposta o mostra Fast; indicatore accanto al modello" },
15
17
  { name: "queue", category: "interfaccia", output: "navigable", mechanism: "live-viewer", argDependent: false, longOutput: true, note: "gestione immediata dei messaggi in attesa, modifica e momento di consegna" },
16
18
  { name: "review", category: "interfaccia", output: "navigable", mechanism: "live-viewer", argDependent: true, longOutput: true, note: "schermata dedicata con sezioni, ricerca e dettaglio per operazione; Ctrl+R anche durante il run" },
19
+ { name: "agents", category: "interfaccia", output: "navigable", mechanism: "live-viewer", argDependent: false, longOutput: true, note: "selettore delle sessioni degli agenti; stessa vista accessibile con Alt+A" },
17
20
  { name: "help", category: "sistema", output: "navigable", mechanism: "picker", argDependent: false, longOutput: false, note: "palette comandi temporanea, Esc chiude senza lasciare testo" },
18
21
  { name: "copy", category: "interfaccia", output: "transient", mechanism: "notice", argDependent: true, longOutput: false, note: "copia testo negli appunti locali; conferma solo se il helper riesce" },
19
22
  { name: "rename", category: "sessione", output: "transient", mechanism: "notice", argDependent: true, longOutput: false, note: "salva un titolo della chat; errore di sintassi se assente" },
@@ -44,6 +47,7 @@ const ENTRIES = [
44
47
  { name: "resume", category: "sessione", output: "navigable", mechanism: "picker", argDependent: true, longOutput: false, note: "picker sessioni se senza arg; azione diretta se passato id" },
45
48
  { name: "new", category: "sessione", output: "none", mechanism: "action", argDependent: false, longOutput: false, note: "crea una sessione nuova e vuota (alias /clear); azzera il contesto" },
46
49
  { name: "language", category: "interfaccia", output: "navigable", mechanism: "picker", argDependent: true, longOutput: false, note: "sceglie la lingua o segue il sistema; preferenza persistita" },
50
+ { name: "remote", category: "sistema", output: "static", mechanism: "inline-text", argDependent: true, longOutput: false, note: "accende il controllo remoto e scrive in chat il link di abbinamento; status e off rispondono con una riga" },
47
51
  { name: "logout", category: "sistema", output: "transient", mechanism: "notice", argDependent: false, longOutput: false, note: "revoca best-effort sul backend + rimozione del token locale; una riga di conferma" },
48
52
  { name: "quit", category: "sistema", output: "none", mechanism: "action", argDependent: false, longOutput: false, note: "esce, nessun output" },
49
53
  ];
@@ -3,7 +3,7 @@ import type { TuiTheme } from "./branding.js";
3
3
  export type CommandState = "running" | "ok" | "error" | "background" | "stopped";
4
4
  export declare function commandSource(call: Pick<ToolCallPart, "name" | "input">): string | undefined;
5
5
  export declare function highlightCommand(source: string, t: TuiTheme): string;
6
- export declare function commandHeader(firstRow: string, state: CommandState, t: TuiTheme, columns: number): string;
6
+ export declare function commandHeader(_firstRow: string, state: CommandState, t: TuiTheme, columns: number): string;
7
7
  export declare function commandPreview(source: string, t: TuiTheme, columns: number, state?: CommandState): {
8
8
  firstRow: string;
9
9
  lines: string[];
@@ -41,13 +41,13 @@ export function highlightCommand(source, t) {
41
41
  function label(state, columns) {
42
42
  return columns < 40
43
43
  ? ({ running: "…", ok: "OK", error: "×", background: "BG", stopped: "–" })[state].padEnd(2)
44
- : ({ running: tr("Esegue", "Running"), ok: tr("Eseguito", "Executed"), error: tr("Errore", "Error"),
45
- background: tr("Avviato", "Started"), stopped: tr("Fermato", "Stopped") })[state].padEnd(8);
44
+ : ({ running: tr("Esegue comando", "Running command"), ok: tr("Comando eseguito", "Ran command"), error: tr("Errore comando", "Command failed"),
45
+ background: tr("Comando avviato", "Command started"), stopped: tr("Comando fermato", "Command stopped") })[state];
46
46
  }
47
- export function commandHeader(firstRow, state, t, columns) {
48
- const color = state === "ok" ? t.green : state === "error" ? t.red : state === "stopped" ? t.yellow : t.accent;
49
- return fitLine(" " + color(state === "error" ? "× " : "● ") + t.bold(label(state, columns)) + " "
50
- + highlightCommand(firstRow, t), Math.max(1, columns - 1));
47
+ export function commandHeader(_firstRow, state, t, columns) {
48
+ const color = state === "error" ? t.red : state === "stopped" ? t.yellow : t.muted;
49
+ return fitLine(" " + color(state === "error" ? "× " : state === "running" ? "◇ " : "◆ ")
50
+ + t.bold(color(label(state, columns))), Math.max(1, columns - 1));
51
51
  }
52
52
  function moreRows(count, t, width) {
53
53
  const full = tr("… +{count} righe · Ctrl+R dettagli", "… +{count} lines · Ctrl+R details", { count });
@@ -56,30 +56,26 @@ function moreRows(count, t, width) {
56
56
  }
57
57
  export function commandPreview(source, t, columns, state = "running") {
58
58
  const width = Math.max(2, columns - 1);
59
- const prefix = columns < 40 ? " " : " │ ";
59
+ const prefix = " │ ";
60
60
  const bodyWidth = Math.max(2, width - terminalWidth(prefix));
61
- const firstWidth = Math.max(2, width - terminalWidth(" ● " + label(state, columns) + " "));
62
- const sourceRows = plainText(source).trimEnd().split("\n");
63
- const firstLine = sourceRows.shift() || tr("(comando vuoto)", "(empty command)");
64
- const firstRow = fitLine(firstLine, firstWidth, false);
65
- const firstPlain = plainText(firstRow);
66
- const rest = firstLine.slice(firstPlain.length);
67
- const rows = [...(rest ? wrapText(rest, bodyWidth) : []), ...sourceRows.flatMap(row => wrapText(row, bodyWidth))];
68
- const lines = [commandHeader(firstPlain, state, t, columns),
69
- ...rows.slice(0, PREVIEW_ROWS - 1).map(row => t.dim(prefix) + highlightCommand(row, t))];
70
- if (rows.length >= PREVIEW_ROWS)
71
- lines.push(t.dim(prefix) + moreRows(rows.length - PREVIEW_ROWS + 1, t, bodyWidth));
72
- return { firstRow: firstPlain, lines };
61
+ const sourceRows = plainText(source).trimEnd() || tr("(comando vuoto)", "(empty command)");
62
+ const rows = wrapText(sourceRows, bodyWidth);
63
+ const firstRow = rows[0] ?? "";
64
+ const lines = [commandHeader(firstRow, state, t, columns),
65
+ ...rows.slice(0, PREVIEW_ROWS).map(row => t.dim(prefix) + t.muted(row))];
66
+ if (rows.length > PREVIEW_ROWS)
67
+ lines.push(t.dim(prefix) + moreRows(rows.length - PREVIEW_ROWS, t, bodyWidth));
68
+ return { firstRow, lines: lines.map(row => fitLine(row, width)) };
73
69
  }
74
70
  export function commandOutput(output, t, columns, isError = false) {
75
71
  const width = Math.max(2, columns - 1);
76
- const prefix = columns < 40 ? " " : " │ ";
72
+ const prefix = " │ ";
77
73
  const bodyWidth = Math.max(2, width - terminalWidth(prefix));
78
74
  const rows = wrapText(plainText(output).trim() || tr("(nessun output)", "(no output)"), bodyWidth);
79
75
  const shown = rows.slice(0, PREVIEW_ROWS);
80
76
  const more = rows.length - shown.length;
81
77
  return [...shown.map((row, index) => {
82
- const gutter = !more && index === shown.length - 1 && columns >= 40 ? " └ " : prefix;
78
+ const gutter = !more && index === shown.length - 1 ? " └ " : prefix;
83
79
  return t.dim(gutter) + (isError ? t.red(row) : t.dim(row));
84
80
  }), ...(more ? [t.dim(prefix) + moreRows(more, t, bodyWidth)] : [])].map(row => fitLine(row, width));
85
81
  }
@@ -21,6 +21,8 @@ export const CATEGORY_ORDER = [
21
21
  ];
22
22
  // Comandi slash builtin (per autocomplete, help navigabile, suggerimenti).
23
23
  export const BUILTIN_COMMANDS = [
24
+ { name: "skills", hint: "", get desc() { return tr("Elenca le skill disponibili e come richiamarle", "List available skills and how to invoke them"); }, category: "sistema" },
25
+ { name: "mcp", hint: "", get desc() { return tr("Elenca i server MCP e gli strumenti della sessione", "List MCP servers and session tools"); }, category: "sistema" },
24
26
  { name: "help", hint: "", get desc() { return tr("Sfoglia i comandi con frecce o mouse", "Browse commands with arrow keys or the mouse"); }, category: "sistema" },
25
27
  { name: "copy", hint: "[testo]", get desc() { return tr("Copia l’ultima risposta o il testo indicato", "Copy the last response or the specified text"); }, category: "interfaccia" },
26
28
  { name: "queue", hint: "", get desc() { return tr("Gestisci la coda · Alt+Q; da 3 richieste, Alt+T invia al team", "Manage queued messages; Alt+Q; Alt+T sends 3 or more requests to a team"); }, category: "interfaccia", aliases: ["coda"] },
@@ -40,7 +42,7 @@ export const BUILTIN_COMMANDS = [
40
42
  { name: "language", hint: "[system|it|en|es|de]", get desc() { return tr("Scegli la lingua dell'interfaccia o segui il sistema", "Choose the interface language or follow the system"); }, category: "interfaccia", aliases: ["lang", "lingua", "idioma", "sprache"] },
41
43
  { name: "review", hint: "[attivita|modifiche|team|problemi]", get desc() { return tr("Esamina attività e modifiche · Ctrl+R", "Review tasks and changes; Ctrl+R"); }, category: "interfaccia", aliases: ["revisione"] },
42
44
  { name: "agents", hint: "", get desc() { return tr("Sessioni degli agenti · Alt+A", "Agent sessions; Alt+A"); }, category: "interfaccia", aliases: ["agenti"] },
43
- { name: "compact", hint: "[istruzioni]", get desc() { return tr("Compatta conservando obiettivo, attivita aperte e lavoro recente; salva un checkpoint", "Compact while preserving the goal, open tasks and recent work; save a checkpoint"); }, category: "vascend", aliases: ["vascend-compact"] },
45
+ { name: "compact", hint: "[istruzioni]", get desc() { return tr("Compatta la cronologia JSON conservando obiettivo, attivita aperte e lavoro recente", "Compact the JSON session history while preserving the goal, open tasks and recent work"); }, category: "vascend" },
44
46
  { name: "vascend", hint: "[on|off|goal]", get desc() { return tr("Configura il metodo di lavoro Vascend", "Configure the Vascend workflow"); }, category: "vascend" },
45
47
  { name: "todo", hint: "", get desc() { return tr("Lista delle attività: da fare, in corso, completate e fallite · Ctrl+G", "Task list: pending, running, completed and failed; Ctrl+G"); }, category: "attivita", aliases: ["piano", "tasks", "attivita", "regno", "castello", "castelli"] },
46
48
  { name: "collaboratori", hint: "<attività1; attività2; ...>", get desc() { return tr("Assegna attività a collaboratori in parallelo", "Assign parallel tasks to collaborators"); }, category: "attivita", aliases: ["kingdoms", "warm"] },
@@ -55,6 +57,7 @@ export const BUILTIN_COMMANDS = [
55
57
  { name: "sessions", hint: "[close]", get desc() { return tr("elenco sessioni salvate (collassabile, /sessions close lo nasconde)", "List saved sessions; /sessions close hides the list"); }, category: "sessione" },
56
58
  { name: "resume", hint: "[id]", get desc() { return tr("Cerca e riapri le chat salvate", "Find and reopen saved chats"); }, category: "sessione", aliases: ["chats"] },
57
59
  { name: "new", hint: "", get desc() { return tr("Apri una chat vuota; la precedente resta salvata", "Start an empty chat; the previous chat stays saved"); }, category: "sessione", aliases: ["clear"] },
60
+ { name: "remote", hint: "[status|off]", get desc() { return tr("controlla questa sessione dal telefono: /remote, /remote status, /remote off", "Control this session from your phone: /remote, /remote status, /remote off"); }, category: "sistema", aliases: ["remoto"] },
58
61
  { name: "logout", hint: "", get desc() { return tr("esci dall'account Vascend (al prossimo avvio serve di nuovo l'accesso)", "Sign out of Vascend; sign-in is required on the next start"); }, category: "sistema" },
59
62
  { name: "quit", hint: "", get desc() { return tr("esci", "Exit Talos"); }, category: "sistema", aliases: ["exit"] },
60
63
  ];
@@ -0,0 +1,7 @@
1
+ export declare function shimmerText(text: string, frame: number): string;
2
+ export declare function renderCompactionStatus(input: {
3
+ readonly frame: number;
4
+ readonly elapsedSeconds: number;
5
+ readonly columns: number;
6
+ readonly style?: (text: string) => string;
7
+ }): readonly string[];
@@ -0,0 +1,39 @@
1
+ import { frostPaint } from "./branding.js";
2
+ import { tr } from "./i18n.js";
3
+ import { fitLine, terminalWidth } from "./terminal-text.js";
4
+ const identity = (text) => text;
5
+ function elapsedLabel(seconds) {
6
+ const safe = Math.max(0, Math.floor(seconds));
7
+ if (safe < 60)
8
+ return `${safe}s`;
9
+ return `${Math.floor(safe / 60)}m ${safe % 60}s`;
10
+ }
11
+ // A narrow light band moves over the title. Only ANSI colors change between
12
+ // frames, so the terminal painter rewrites this row without touching chat or
13
+ // composer content.
14
+ export function shimmerText(text, frame) {
15
+ const chars = Array.from(text);
16
+ const travel = chars.length + 10;
17
+ const head = ((frame % travel) + travel) % travel - 5;
18
+ return chars.map((char, index) => {
19
+ const distance = Math.abs(index - head);
20
+ const light = distance < 0.75 ? 1 : distance < 1.75 ? 0.82 : distance < 3.25 ? 0.55 : 0.24;
21
+ return frostPaint(light, char);
22
+ }).join("");
23
+ }
24
+ export function renderCompactionStatus(input) {
25
+ const width = Math.max(1, input.columns - 1);
26
+ const style = input.style ?? identity;
27
+ const title = shimmerText(tr("Compattazione del contesto", "Compacting context"), input.frame);
28
+ const left = ` ${style("╭─")} ${frostPaint(0.72, "✦")} ${title}`;
29
+ const elapsed = style(elapsedLabel(input.elapsedSeconds));
30
+ const gap = Math.max(2, width - terminalWidth(left) - terminalWidth(elapsed));
31
+ const headline = terminalWidth(left) + 2 + terminalWidth(elapsed) <= width
32
+ ? left + " ".repeat(gap) + elapsed
33
+ : `${left} ${elapsed}`;
34
+ return [
35
+ fitLine(headline, width),
36
+ fitLine(` ${style("│")} ${frostPaint(0.58, tr("Creo spazio per continuare…", "Making room to continue…"))}`, width),
37
+ fitLine(` ${style("╰─")} ${style(tr("Esc interrompe", "Esc to interrupt"))}`, width),
38
+ ];
39
+ }
@@ -0,0 +1,12 @@
1
+ import { buildConfig } from "../core/config.js";
2
+ export type RecoveryChoice = "reset" | "manual";
3
+ export declare const recoveryOptions: ({
4
+ value: "reset";
5
+ label: string;
6
+ detail: string;
7
+ } | {
8
+ value: "manual";
9
+ label: string;
10
+ detail: string;
11
+ })[];
12
+ export declare function prepareStartupConfig(opts: Parameters<typeof buildConfig>[0], interactive: boolean, choose?: () => Promise<RecoveryChoice | null>, write?: (text: string) => void): Promise<boolean>;
@@ -0,0 +1,54 @@
1
+ import { buildConfig, DEFAULT_UI, InvalidConfigError } from "../core/config.js";
2
+ import { resetInvalidConfig } from "../core/config-storage.js";
3
+ import { makeTheme } from "./branding.js";
4
+ import { selectOption } from "./selector.js";
5
+ export const recoveryOptions = [
6
+ { value: "reset", label: "Reimposta le impostazioni predefinite", detail: "Conserva un backup del file corrotto. Potrebbe essere necessario accedere di nuovo." },
7
+ { value: "manual", label: "No, aggiusto manualmente", detail: "Esci senza modificare il file di configurazione." },
8
+ ];
9
+ async function chooseRecovery() {
10
+ let rows = 0;
11
+ const selected = await selectOption({
12
+ title: "Configurazione non valida: come vuoi procedere?",
13
+ options: recoveryOptions, current: "manual", theme: makeTheme(DEFAULT_UI),
14
+ filterable: false, terminalColumns: () => process.stdout.columns || 80,
15
+ ctl: {
16
+ pause() { }, resume() { }, write: (text) => { process.stdout.write(text); },
17
+ setOverlay(lines) {
18
+ if (rows)
19
+ process.stdout.write(`\x1b[${rows}A\r\x1b[J`);
20
+ rows = lines?.length ?? 0;
21
+ if (lines)
22
+ process.stdout.write(lines.join("\n") + "\n");
23
+ },
24
+ },
25
+ });
26
+ return selected?.value ?? null;
27
+ }
28
+ // Preflight prima di bootstrap/login. Print, RPC e pipe non devono mai
29
+ // attendere input ne' reimpostare configurazioni implicitamente.
30
+ export async function prepareStartupConfig(opts, interactive, choose = chooseRecovery, write = (text) => { process.stderr.write(text); }) {
31
+ for (;;) {
32
+ try {
33
+ buildConfig(opts);
34
+ return true;
35
+ }
36
+ catch (error) {
37
+ if (!(error instanceof InvalidConfigError) || !interactive)
38
+ throw error;
39
+ write(`\nConfigurazione JSON non valida: ${error.path}\n`);
40
+ if (await choose() !== "reset") {
41
+ write(`Correggi o ripristina ${error.path}, quindi riavvia Talos. Nessun file modificato per questa scelta.\n`);
42
+ return false;
43
+ }
44
+ try {
45
+ const backup = resetInvalidConfig(error.path);
46
+ write(backup ? `Impostazioni reimpostate. Backup: ${backup}\n` : "Il file è già stato corretto; riprovo il caricamento.\n");
47
+ }
48
+ catch {
49
+ write(`Ripristino non riuscito: ${error.path}. Controlla i permessi e correggi il file manualmente.\n`);
50
+ return false;
51
+ }
52
+ }
53
+ }
54
+ }
@@ -16,7 +16,10 @@ export interface ContextReading {
16
16
  export declare class ContextMeter {
17
17
  private cache?;
18
18
  private observed?;
19
+ private archive?;
20
+ private archiveTokens;
19
21
  reset(): void;
22
+ private archivedPrefix;
20
23
  observe(source: ContextSource, ledger: ContextLedger): void;
21
24
  read(source: ContextSource): ContextReading;
22
25
  private baseline;
@@ -3,6 +3,7 @@ import { contextWindowFor, estimateMessagesTokens } from "../core/compaction.js"
3
3
  import { applyToolResultDemotion, planToolResultDemotion, toolIndexPolicy, applyMediaEviction, mediaBudgetPolicy, planMediaEviction } from "../core/tool-result-index.js";
4
4
  import { ToolOutputStore } from "../core/tool-result-store.js";
5
5
  import { contextPolicy, workingContextLimits } from "../core/context-policy.js";
6
+ import { maxInputFor, resolveMaxTokens } from "../core/model-limits.js";
6
7
  // La cronologia su disco contiene ancora gli output integrali. Il contatore
7
8
  // usa la stessa proiezione del loop; il registro della richiesta aggiunge il
8
9
  // system effettivo e i blocchi di contesto compilati. Fra due richieste si
@@ -10,7 +11,32 @@ import { contextPolicy, workingContextLimits } from "../core/context-policy.js";
10
11
  export class ContextMeter {
11
12
  cache;
12
13
  observed;
13
- reset() { this.cache = undefined; this.observed = undefined; }
14
+ archive;
15
+ archiveTokens = new WeakMap();
16
+ reset() {
17
+ this.cache = undefined;
18
+ this.observed = undefined;
19
+ this.archive = undefined;
20
+ this.archiveTokens = new WeakMap();
21
+ }
22
+ archivedPrefix(session, boundary) {
23
+ if (this.archive?.session === session && this.archive.boundary === boundary)
24
+ return this.archive.blocks;
25
+ // Nodes are immutable. Estimate each archived message once; subsequent
26
+ // live messages reuse the prefix, even when the live ledger is refreshed.
27
+ const nodes = session.nodeChain();
28
+ const cutoff = nodes.findIndex(node => node.id === boundary);
29
+ const archived = nodes.slice(0, Math.max(0, cutoff)).map(node => {
30
+ let tokens = this.archiveTokens.get(node.message);
31
+ if (tokens === undefined) {
32
+ tokens = estimateMessagesTokens([node.message]);
33
+ this.archiveTokens.set(node.message, tokens);
34
+ }
35
+ return { tokens, at: node.ts };
36
+ }).filter(block => block.tokens > 0);
37
+ this.archive = { session, boundary, blocks: archived };
38
+ return this.archive.blocks;
39
+ }
14
40
  observe(source, ledger) {
15
41
  if (ledger.model !== source.config.model)
16
42
  return;
@@ -32,7 +58,7 @@ export class ContextMeter {
32
58
  const history = session.history(), defs = tools.defs();
33
59
  const scaledPolicy = toolIndexPolicy(env, contextWindowFor(config.model, config.contextWindow));
34
60
  const mediaPolicy = mediaBudgetPolicy(env);
35
- const limits = workingContextLimits(contextPolicy(env), contextWindowFor(config.model, config.contextWindow), config.compaction.reserveTokens);
61
+ const limits = workingContextLimits(contextPolicy(env), contextWindowFor(config.model, config.contextWindow), config.compaction.reserveTokens, resolveMaxTokens(config.maxTokens, config.model), maxInputFor(config.model));
36
62
  const key = JSON.stringify([config.model, config.contextWindow, config.sessionDir, config.roomStoreId, scaledPolicy, mediaPolicy,
37
63
  limits, config.compaction.enabled, config.compaction.perRoom, contextPolicy(env).roomCompaction]);
38
64
  const cached = this.cache;
@@ -46,17 +72,18 @@ export class ContextMeter {
46
72
  const ledger = buildContextLedger({ model: config.model, contextWindow: config.contextWindow,
47
73
  system: config.system, tools: defs, messages });
48
74
  const demoted = new Map(demotion.refs.map(ref => [ref.callId, ref.tokens]));
49
- const chain = session.nodeChain();
50
- const kept = session.lastCompaction ? chain.findIndex(node => node.id === session.lastCompaction.firstKeptId) : -1;
51
- const archived = [];
52
- for (const [index, node] of chain.entries()) {
75
+ const boundary = session.lastCompaction?.firstKeptId;
76
+ const chain = session.nodeChain(boundary);
77
+ const archived = boundary && chain[0]?.id === boundary
78
+ ? [...this.archivedPrefix(session, boundary)] : [];
79
+ for (const node of chain) {
53
80
  // Le due porzioni sono disgiunte: un risultato gia' dentro il prefisso
54
81
  // riassunto non si riconta come risultato retrocesso nella parte viva.
55
- const tokens = index < kept ? estimateMessagesTokens([node.message]) : node.message.content.reduce((sum, part) => sum + (part.type === "tool_result" ? demoted.get(part.callId) ?? 0 : 0), 0);
82
+ const tokens = node.message.content.reduce((sum, part) => sum + (part.type === "tool_result" ? demoted.get(part.callId) ?? 0 : 0), 0);
56
83
  if (tokens > 0)
57
84
  archived.push({ tokens, at: node.ts });
58
85
  }
59
- const reading = { tokens: ledger.tokens, window: ledger.window, archived, nodes: session.tree().length,
86
+ const reading = { tokens: ledger.tokens, window: ledger.window, archived, nodes: session.nodeCount,
60
87
  ...(config.compaction.enabled ? { workingTarget: limits.active,
61
88
  ...(config.compaction.perRoom && contextPolicy(env).roomCompaction !== "never" ? { checkpointAt: limits.checkpoint } : {}),
62
89
  } : {}),
@@ -5,5 +5,14 @@ export interface DiffOptions {
5
5
  readonly context?: number;
6
6
  readonly columns?: number;
7
7
  readonly wrapLines?: boolean;
8
+ readonly maxRows?: number;
8
9
  }
10
+ export interface RenderedDiff {
11
+ readonly header: string;
12
+ readonly body: string;
13
+ readonly added: number;
14
+ readonly deleted: number;
15
+ readonly hiddenRows?: number;
16
+ }
17
+ export declare function renderDiffParts(oldText: string, newText: string, theme: TuiTheme, opts?: DiffOptions): RenderedDiff | null;
9
18
  export declare function renderDiff(oldText: string, newText: string, theme: TuiTheme, opts?: DiffOptions): string;
package/dist/tui/diff.js CHANGED
@@ -1,4 +1,4 @@
1
- import { fitLine, padLine, plainText, wrapText } from "./terminal-text.js";
1
+ import { fitLine, padLine, plainText, terminalWidth, wrapText, wrappedLineCount } from "./terminal-text.js";
2
2
  // Riduce prima il problema alle righe cambiate; limita la matrice quadratica.
3
3
  function diffLines(oldLines, newLines) {
4
4
  let prefix = 0;
@@ -60,7 +60,7 @@ function diffMiddle(oldLines, newLines) {
60
60
  }
61
61
  return ops;
62
62
  }
63
- export function renderDiff(oldText, newText, theme, opts = {}) {
63
+ export function renderDiffParts(oldText, newText, theme, opts = {}) {
64
64
  const context = opts.context ?? 3;
65
65
  const cols = Math.max(4, opts.columns ?? 80);
66
66
  const oldLines = oldText === "" ? [] : plainText(oldText).replace(/\r?\n$/, "").split(/\r?\n/);
@@ -69,10 +69,16 @@ export function renderDiff(oldText, newText, theme, opts = {}) {
69
69
  const added = ops.filter((o) => o.kind === "add").length;
70
70
  const deleted = ops.filter((o) => o.kind === "del").length;
71
71
  if (added === 0 && deleted === 0)
72
- return "";
73
- // Numero di riga lato NUOVO file (per add/contesto); le rimozioni non ne hanno.
74
- let n = 0;
75
- const rows = ops.map((op) => ({ ...op, no: op.kind === "del" ? null : (n += 1) }));
72
+ return null;
73
+ // Deletions use the old file's numbers; additions use the new file's.
74
+ let oldNo = 0, newNo = 0;
75
+ const rows = ops.map(op => {
76
+ if (op.kind !== "add")
77
+ oldNo++;
78
+ if (op.kind !== "del")
79
+ newNo++;
80
+ return { ...op, no: op.kind === "del" ? oldNo : newNo };
81
+ });
76
82
  // Tieni solo le righe entro `context` da una modifica; il resto e' un "gap".
77
83
  const keep = new Array(rows.length).fill(false);
78
84
  rows.forEach((r, i) => {
@@ -81,17 +87,24 @@ export function renderDiff(oldText, newText, theme, opts = {}) {
81
87
  for (let k = Math.max(0, i - context); k <= Math.min(rows.length - 1, i + context); k += 1)
82
88
  keep[k] = true;
83
89
  });
84
- const numW = Math.max(2, String(newLines.length || 1).length);
90
+ const numW = Math.max(2, String(Math.max(oldLines.length, newLines.length, 1)).length);
85
91
  const verb = opts.verb ?? "modificato";
86
92
  const path = plainText(opts.path ?? "file").replace(/[\r\n]/g, " ");
87
- const bodyWidth = Math.max(1, cols - numW - 5);
93
+ const bodyWidth = Math.max(1, cols - numW - 6);
88
94
  // Estende il testo (privo di ANSI) fino a `cols` colonne, cosi' lo sfondo del
89
95
  // pittore copre l'intera larghezza della riga (look "a blocco" del diff).
90
96
  const pad = (s) => padLine(fitLine(s, cols), cols);
91
- const out = [
92
- "",
93
- theme.diffHeader(pad(` ${verb} ${path} +${added} −${deleted}`)),
94
- ];
97
+ const counts = `+${added} −${deleted}`;
98
+ const displayVerb = terminalWidth(` ${verb} ${counts}`) <= cols ? verb : `${verb.slice(0, 3)}.`;
99
+ const fixedHeader = ` ● ${displayVerb} ${counts}`;
100
+ const pathWidth = Math.max(0, cols - terminalWidth(fixedHeader));
101
+ const displayPath = pathWidth ? plainText(fitLine(path, pathWidth, false)) : "";
102
+ // Keep the heading unboxed; reserve background fills for changed code.
103
+ const header = fitLine(" " + theme.green("● " + displayVerb)
104
+ + (displayPath ? " " + theme.bold(theme.silver(displayPath)) : "") + theme.dim(` ${counts}`), cols);
105
+ const out = [];
106
+ const maxRows = opts.maxRows === undefined ? Infinity : Math.max(0, Math.floor(opts.maxRows));
107
+ let hiddenRows = 0;
95
108
  let gap = 0;
96
109
  rows.forEach((r, i) => {
97
110
  if (!keep[i]) {
@@ -99,21 +112,32 @@ export function renderDiff(oldText, newText, theme, opts = {}) {
99
112
  return;
100
113
  }
101
114
  if (gap > 0) {
102
- out.push(theme.diffBorder(fitLine(` ${" ".repeat(numW)} ⋯ ${gap} righe invariate`, cols)));
115
+ if (out.length < maxRows)
116
+ out.push(theme.diffBorder(fitLine(` ${" ".repeat(numW)} ⋯ ${gap} righe invariate`, cols)));
117
+ else
118
+ hiddenRows++;
103
119
  gap = 0;
104
120
  }
105
- const numStr = r.no === null ? " ".repeat(numW) : String(r.no).padStart(numW);
121
+ const numStr = String(r.no).padStart(numW);
106
122
  const text = r.line.replace(/\t/g, " ");
107
- const lines = opts.wrapLines ? wrapText(text, bodyWidth) : [fitLine(text, bodyWidth)];
123
+ const remaining = Math.max(0, maxRows - out.length);
124
+ const lines = remaining === 0 ? [] : opts.wrapLines ? wrapText(text, bodyWidth, false, remaining) : [fitLine(text, bodyWidth)];
125
+ if (Number.isFinite(maxRows) && (remaining === 0 || lines.length === remaining)) {
126
+ hiddenRows += (opts.wrapLines ? wrappedLineCount(text, bodyWidth) : 1) - lines.length;
127
+ }
108
128
  lines.forEach((line, part) => {
109
129
  const number = part ? " ".repeat(numW) : numStr;
110
130
  if (r.kind === "add")
111
- out.push(theme.diffAdded(pad(` ${number} + ${line}`)));
131
+ out.push(theme.diffAdded(pad(` │ ${number} + ${line}`)));
112
132
  else if (r.kind === "del")
113
- out.push(theme.diffRemoved(pad(` ${number} - ${line}`)));
133
+ out.push(theme.diffRemoved(pad(` │ ${number} - ${line}`)));
114
134
  else
115
- out.push(theme.diffContext(pad(` ${number} ${line}`)));
135
+ out.push(theme.diffContext(pad(` │ ${number} ${line}`)));
116
136
  });
117
137
  });
118
- return out.join("\n") + "\n";
138
+ return { header, body: out.join("\n") + "\n", added, deleted, ...(opts.maxRows === undefined ? {} : { hiddenRows }) };
139
+ }
140
+ export function renderDiff(oldText, newText, theme, opts = {}) {
141
+ const rendered = renderDiffParts(oldText, newText, theme, opts);
142
+ return rendered ? `\n${rendered.header}\n${rendered.body}` : "";
119
143
  }
@@ -0,0 +1,5 @@
1
+ import type { Harness } from "../core/bootstrap.js";
2
+ import type { InputControl } from "./repl.js";
3
+ import type { TuiTheme } from "./branding.js";
4
+ import { selectOption } from "./selector.js";
5
+ export declare function pickFusion(harness: Harness, ctl: InputControl, theme: TuiTheme, terminalColumns: () => number, select?: typeof selectOption): Promise<boolean>;