@vascend/talos 0.1.4 → 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 (251) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +37 -12
  3. package/dist/core/agent.d.ts +16 -3
  4. package/dist/core/agent.js +170 -70
  5. package/dist/core/background.d.ts +10 -3
  6. package/dist/core/background.js +55 -9
  7. package/dist/core/bootstrap.js +1 -0
  8. package/dist/core/compaction.js +1 -1
  9. package/dist/core/config-storage.d.ts +5 -0
  10. package/dist/core/config-storage.js +83 -0
  11. package/dist/core/config.d.ts +11 -2
  12. package/dist/core/config.js +24 -8
  13. package/dist/core/context-ledger.js +2 -1
  14. package/dist/core/context-policy.d.ts +1 -1
  15. package/dist/core/context-policy.js +10 -7
  16. package/dist/core/credentials.js +29 -30
  17. package/dist/core/effort.d.ts +1 -1
  18. package/dist/core/effort.js +5 -1
  19. package/dist/core/execution-guidance.d.ts +1 -1
  20. package/dist/core/execution-guidance.js +2 -0
  21. package/dist/core/file-coordination.d.ts +3 -0
  22. package/dist/core/file-coordination.js +28 -0
  23. package/dist/core/file-lease-store.d.ts +3 -0
  24. package/dist/core/file-lease-store.js +28 -12
  25. package/dist/core/fusion.d.ts +24 -0
  26. package/dist/core/fusion.js +80 -0
  27. package/dist/core/model-limits.d.ts +1 -0
  28. package/dist/core/model-limits.js +5 -1
  29. package/dist/core/permission-queue.js +15 -15
  30. package/dist/core/request-plan.js +4 -3
  31. package/dist/core/session-naming.d.ts +3 -0
  32. package/dist/core/session-naming.js +47 -0
  33. package/dist/core/session-usage.js +4 -1
  34. package/dist/core/session.d.ts +5 -2
  35. package/dist/core/session.js +22 -7
  36. package/dist/core/settings.d.ts +2 -0
  37. package/dist/core/settings.js +5 -16
  38. package/dist/core/step-policy.d.ts +18 -0
  39. package/dist/core/step-policy.js +6 -0
  40. package/dist/core/storage-health.d.ts +24 -0
  41. package/dist/core/storage-health.js +72 -0
  42. package/dist/core/tool-artifacts.d.ts +2 -0
  43. package/dist/core/tool-artifacts.js +21 -3
  44. package/dist/core/tool-result-index.js +29 -33
  45. package/dist/core/tool-result-store.js +6 -5
  46. package/dist/core/types.d.ts +26 -2
  47. package/dist/core/user-inbox.d.ts +9 -0
  48. package/dist/core/user-inbox.js +22 -3
  49. package/dist/core/user-input.js +1 -1
  50. package/dist/fusion/autopilot-fast.d.ts +59 -0
  51. package/dist/fusion/autopilot-fast.js +213 -0
  52. package/dist/fusion/autopilot.d.ts +75 -0
  53. package/dist/fusion/autopilot.js +308 -0
  54. package/dist/fusion/focus.d.ts +30 -0
  55. package/dist/fusion/focus.js +122 -0
  56. package/dist/fusion/gate.d.ts +30 -0
  57. package/dist/fusion/gate.js +95 -0
  58. package/dist/fusion/jev.d.ts +71 -0
  59. package/dist/fusion/jev.js +164 -0
  60. package/dist/fusion/receipts.d.ts +25 -0
  61. package/dist/fusion/receipts.js +34 -0
  62. package/dist/fusion/route.d.ts +25 -0
  63. package/dist/fusion/route.js +84 -0
  64. package/dist/fusion/run.d.ts +4 -0
  65. package/dist/fusion/run.js +186 -0
  66. package/dist/horizon/engine.d.ts +20 -1
  67. package/dist/horizon/engine.js +57 -0
  68. package/dist/horizon/index.d.ts +2 -2
  69. package/dist/horizon/index.js +1 -1
  70. package/dist/horizon/types.d.ts +29 -0
  71. package/dist/index.js +12 -0
  72. package/dist/providers/anthropic-oauth.d.ts +8 -3
  73. package/dist/providers/anthropic-oauth.js +13 -10
  74. package/dist/providers/anthropic.d.ts +9 -2
  75. package/dist/providers/anthropic.js +64 -21
  76. package/dist/providers/cli-models.d.ts +30 -0
  77. package/dist/providers/cli-models.js +194 -0
  78. package/dist/providers/model-effort.js +1 -1
  79. package/dist/providers/openai-oauth.d.ts +8 -3
  80. package/dist/providers/openai-oauth.js +9 -3
  81. package/dist/providers/openrouter-manual-fallback.d.ts +3 -1
  82. package/dist/providers/openrouter-manual-fallback.js +20 -20
  83. package/dist/providers/registry.js +3 -2
  84. package/dist/providers/retry.d.ts +3 -0
  85. package/dist/providers/retry.js +44 -1
  86. package/dist/remote/cifratura.d.ts +12 -0
  87. package/dist/remote/cifratura.js +66 -0
  88. package/dist/remote/index.d.ts +15 -0
  89. package/dist/remote/index.js +65 -0
  90. package/dist/remote/relay-client.d.ts +58 -0
  91. package/dist/remote/relay-client.js +315 -0
  92. package/dist/remote/sessione-remota.d.ts +60 -0
  93. package/dist/remote/sessione-remota.js +266 -0
  94. package/dist/sdk.d.ts +7 -2
  95. package/dist/sdk.js +3 -1
  96. package/dist/tools/builtin.js +41 -20
  97. package/dist/tools/capabilities.d.ts +3 -1
  98. package/dist/tools/capabilities.js +5 -2
  99. package/dist/tools/computer-use.js +2 -1
  100. package/dist/tools/pagination.js +1 -1
  101. package/dist/tools/program.js +34 -21
  102. package/dist/tools/team-tools.js +11 -6
  103. package/dist/tui/account-entry.js +14 -9
  104. package/dist/tui/agent-label.d.ts +2 -0
  105. package/dist/tui/agent-label.js +5 -0
  106. package/dist/tui/agent-sessions-screen.d.ts +35 -0
  107. package/dist/tui/agent-sessions-screen.js +178 -0
  108. package/dist/tui/agent-sessions.d.ts +53 -0
  109. package/dist/tui/agent-sessions.js +236 -0
  110. package/dist/tui/bottom-input.d.ts +57 -2
  111. package/dist/tui/bottom-input.js +449 -54
  112. package/dist/tui/branding.js +8 -4
  113. package/dist/tui/capability-lists.d.ts +10 -0
  114. package/dist/tui/capability-lists.js +41 -0
  115. package/dist/tui/command-output-registry.js +4 -0
  116. package/dist/tui/command-transcript.d.ts +1 -1
  117. package/dist/tui/command-transcript.js +17 -21
  118. package/dist/tui/commands.js +6 -2
  119. package/dist/tui/compaction-status.d.ts +7 -0
  120. package/dist/tui/compaction-status.js +39 -0
  121. package/dist/tui/config-recovery.d.ts +12 -0
  122. package/dist/tui/config-recovery.js +54 -0
  123. package/dist/tui/context-meter.d.ts +3 -0
  124. package/dist/tui/context-meter.js +35 -8
  125. package/dist/tui/diff.d.ts +9 -0
  126. package/dist/tui/diff.js +43 -19
  127. package/dist/tui/fusion-menu.d.ts +5 -0
  128. package/dist/tui/fusion-menu.js +89 -0
  129. package/dist/tui/i18n-catalog.js +351 -2
  130. package/dist/tui/live-command.d.ts +1 -0
  131. package/dist/tui/live-command.js +9 -0
  132. package/dist/tui/live-output.d.ts +1 -0
  133. package/dist/tui/live-output.js +11 -0
  134. package/dist/tui/logo.js +3 -0
  135. package/dist/tui/model-details.js +13 -12
  136. package/dist/tui/model-picker.d.ts +2 -0
  137. package/dist/tui/model-picker.js +59 -0
  138. package/dist/tui/model-selection.js +19 -10
  139. package/dist/tui/native-scrollback.d.ts +2 -1
  140. package/dist/tui/native-scrollback.js +31 -6
  141. package/dist/tui/prompt-queue-store.d.ts +1 -0
  142. package/dist/tui/prompt-queue-store.js +4 -0
  143. package/dist/tui/prompt-queue.d.ts +3 -0
  144. package/dist/tui/prompt-queue.js +19 -1
  145. package/dist/tui/provider-label.d.ts +2 -0
  146. package/dist/tui/provider-label.js +9 -0
  147. package/dist/tui/provider-menu.js +25 -24
  148. package/dist/tui/queue-screen.js +2 -1
  149. package/dist/tui/queued-questions.js +2 -4
  150. package/dist/tui/quick-help.d.ts +1 -1
  151. package/dist/tui/quick-help.js +3 -0
  152. package/dist/tui/remote-command.d.ts +7 -0
  153. package/dist/tui/remote-command.js +79 -0
  154. package/dist/tui/repl.d.ts +11 -2
  155. package/dist/tui/repl.js +457 -327
  156. package/dist/tui/review-store.d.ts +8 -1
  157. package/dist/tui/review-store.js +64 -11
  158. package/dist/tui/review-team.js +4 -3
  159. package/dist/tui/runtime-error.d.ts +2 -0
  160. package/dist/tui/runtime-error.js +26 -0
  161. package/dist/tui/scrollback-archive.d.ts +16 -0
  162. package/dist/tui/scrollback-archive.js +90 -0
  163. package/dist/tui/selector-mouse.d.ts +1 -0
  164. package/dist/tui/selector-mouse.js +1 -1
  165. package/dist/tui/selector.d.ts +11 -0
  166. package/dist/tui/selector.js +28 -4
  167. package/dist/tui/session-controls.d.ts +26 -0
  168. package/dist/tui/session-controls.js +65 -0
  169. package/dist/tui/session-naming-notice.d.ts +14 -0
  170. package/dist/tui/session-naming-notice.js +59 -0
  171. package/dist/tui/session-picker.d.ts +2 -0
  172. package/dist/tui/session-picker.js +32 -0
  173. package/dist/tui/session-replay.d.ts +1 -0
  174. package/dist/tui/session-replay.js +101 -32
  175. package/dist/tui/session-transcript-layout.d.ts +9 -0
  176. package/dist/tui/session-transcript-layout.js +41 -0
  177. package/dist/tui/startup.js +63 -33
  178. package/dist/tui/status-line.d.ts +1 -1
  179. package/dist/tui/status-line.js +20 -12
  180. package/dist/tui/storage-warning.d.ts +2 -0
  181. package/dist/tui/storage-warning.js +35 -0
  182. package/dist/tui/talos-art-data.d.ts +1 -0
  183. package/dist/tui/talos-art-data.js +2 -0
  184. package/dist/tui/talos-art.d.ts +1 -0
  185. package/dist/tui/talos-art.js +9 -0
  186. package/dist/tui/task-labels.d.ts +1 -0
  187. package/dist/tui/task-labels.js +8 -0
  188. package/dist/tui/terminal-text.d.ts +2 -1
  189. package/dist/tui/terminal-text.js +28 -2
  190. package/dist/tui/theme.js +2 -2
  191. package/dist/tui/tool-presentation.d.ts +3 -0
  192. package/dist/tui/tool-presentation.js +98 -0
  193. package/dist/tui/tool-style.js +4 -0
  194. package/dist/tui/transcript-store.d.ts +25 -0
  195. package/dist/tui/transcript-store.js +151 -0
  196. package/dist/tui/usage-footer.d.ts +1 -1
  197. package/dist/tui/usage-footer.js +3 -1
  198. package/dist/tui/vascend-id.d.ts +49 -0
  199. package/dist/tui/vascend-id.js +321 -0
  200. package/dist/tui/web-login-screen.js +4 -3
  201. package/dist/tui/web-login.d.ts +7 -15
  202. package/dist/tui/web-login.js +38 -78
  203. package/dist/tui/worker-transcripts.d.ts +31 -0
  204. package/dist/tui/worker-transcripts.js +91 -0
  205. package/dist/vascend/candidate-collector.d.ts +6 -5
  206. package/dist/vascend/candidate-collector.js +44 -43
  207. package/dist/vascend/context-feedback-loop.js +2 -0
  208. package/dist/vascend/context-lint.js +32 -2
  209. package/dist/vascend/context-market-selection.js +12 -5
  210. package/dist/vascend/context-pack-render.js +10 -4
  211. package/dist/vascend/context-packet-mirrors.d.ts +8 -0
  212. package/dist/vascend/context-packet-mirrors.js +24 -0
  213. package/dist/vascend/context-plan-events.d.ts +4 -0
  214. package/dist/vascend/context-plan-events.js +44 -0
  215. package/dist/vascend/context-snapshot.d.ts +1 -0
  216. package/dist/vascend/context-snapshot.js +8 -5
  217. package/dist/vascend/context-store-reader.d.ts +20 -0
  218. package/dist/vascend/context-store-reader.js +46 -0
  219. package/dist/vascend/context-window-compiler.d.ts +1 -0
  220. package/dist/vascend/context-window-compiler.js +1 -0
  221. package/dist/vascend/file-lock.d.ts +1 -0
  222. package/dist/vascend/file-lock.js +26 -0
  223. package/dist/vascend/granular-kingdom.d.ts +1 -0
  224. package/dist/vascend/granular-kingdom.js +52 -17
  225. package/dist/vascend/kingdom-bus.d.ts +1 -0
  226. package/dist/vascend/kingdom-bus.js +3 -0
  227. package/dist/vascend/kingdom-channel.d.ts +2 -0
  228. package/dist/vascend/kingdom-channel.js +3 -0
  229. package/dist/vascend/kingdom-live.d.ts +32 -4
  230. package/dist/vascend/kingdom-live.js +149 -13
  231. package/dist/vascend/kingdom-run.d.ts +5 -1
  232. package/dist/vascend/kingdom-run.js +6 -2
  233. package/dist/vascend/kingdom.d.ts +3 -0
  234. package/dist/vascend/kingdom.js +22 -11
  235. package/dist/vascend/memory-packet.d.ts +2 -1
  236. package/dist/vascend/memory-packet.js +31 -44
  237. package/dist/vascend/memory-store.d.ts +1 -0
  238. package/dist/vascend/memory-store.js +12 -4
  239. package/dist/vascend/plan-store.d.ts +3 -1
  240. package/dist/vascend/plan-store.js +6 -4
  241. package/dist/vascend/room-memory.d.ts +13 -2
  242. package/dist/vascend/room-memory.js +48 -6
  243. package/dist/vascend/runtime.d.ts +3 -11
  244. package/dist/vascend/runtime.js +11 -123
  245. package/dist/vascend/session-search.js +2 -0
  246. package/dist/vascend/team-session.js +3 -2
  247. package/dist/vascend/vision.d.ts +3 -1
  248. package/dist/vascend/vision.js +6 -4
  249. package/dist/vascend/worker-session.d.ts +20 -0
  250. package/dist/vascend/worker-session.js +83 -0
  251. package/package.json +2 -2
package/dist/tui/repl.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { reviewContinuation } from "../core/continuation-review.js";
2
+ import { skillList, mcpList } from "./capability-lists.js";
2
3
  import * as readline from "node:readline";
3
4
  import { spawn } from "node:child_process";
4
5
  import { watch, existsSync, readFileSync } from "node:fs";
@@ -6,17 +7,23 @@ import { stdin, stdout } from "node:process";
6
7
  import { repoRoot, currentBranch } from "../vascend/worktree.js";
7
8
  import { runTurn, compactNow } from "../core/agent.js";
8
9
  import { createUserInbox, pendingText } from "../core/user-inbox.js";
9
- import { renderDiff } from "./diff.js";
10
+ import { renderDiffParts } from "./diff.js";
10
11
  import { renderMarkdown, wrapPlain } from "./markdown.js";
11
12
  import { SessionUsageState } from "./session-usage.js";
13
+ import { readSessionUsage } from "../core/session-usage.js";
14
+ import { storageHealth } from "../core/storage-health.js";
15
+ import { storageWarning } from "./storage-warning.js";
12
16
  import { ContextMeter, contextDetails } from "./context-meter.js";
13
- import { buildUsageFooter, subscriptionDetails, SESSION_USAGE_EXPLANATION } from "./usage-footer.js";
17
+ import { buildUsageFooter, subscriptionDetails, sessionUsageExplanation } from "./usage-footer.js";
14
18
  import { copyText, lastAssistantText } from "./clipboard-text.js";
19
+ import { sendWorker, stopWorker, deleteWorker, workerFooter, bindWorkerInbox, wakeWorker } from "./session-controls.js";
15
20
  import { withCommandFeedback, notifyStatus } from "./command-feedback.js";
16
21
  import { loadModelsDev, cachedModelCatalog } from "../providers/models-dev.js";
17
22
  import { modelEfforts } from "../providers/model-effort.js";
18
23
  import { modelDetail } from "./model-details.js";
24
+ import { providerLabel } from "./provider-label.js";
19
25
  import { applyModelChoice, pickEffort } from "./model-selection.js";
26
+ import { pickFusion } from "./fusion-menu.js";
20
27
  import { PROVIDER_CATALOG } from "../providers/catalog.js";
21
28
  import { fetchOpenRouterModels, openRouterDetail } from "../providers/openrouter.js";
22
29
  import { providerRows, actionsFor, rowStatusLabel, providerKeyHint, saveProviderKey, saveProviderBaseUrl, removeProvider, } from "./provider-menu.js";
@@ -29,6 +36,7 @@ import { frostPaint, makeTheme, renderAssistantSegment, renderAssistantTurnEnd,
29
36
  import { renderVascendTerminalLogo } from "./logo.js";
30
37
  import { enterAccount } from "./account-entry.js";
31
38
  import { tr } from "./i18n.js";
39
+ import { runtimeError, isUserInterruption } from "./runtime-error.js";
32
40
  import { LANGUAGES, normalizePreference } from "./locales.js";
33
41
  import { languagePreference, saveLanguagePreference } from "./language-preference.js";
34
42
  import { leggiOnboarding } from "./onboarding.js";
@@ -45,7 +53,7 @@ import { restoreQueuedInput } from "./restore-queued-input.js";
45
53
  import { ReviewStore } from "./review-store.js";
46
54
  import { ReviewScreen, reviewText } from "./review-screen.js";
47
55
  import { fitLine, oneLine, terminalWidth, wrapText } from "./terminal-text.js";
48
- import { livePreview, liveResultSummary } from "./live-output.js";
56
+ import { livePreview, liveResultSummary, backgroundResultSummary } from "./live-output.js";
49
57
  import { commandSource, commandPreview, commandHeader, commandOutput, commandResultState } from "./command-transcript.js";
50
58
  import { mcpName, mcpHeader, mcpPreview, mcpOutput } from "./mcp-transcript.js";
51
59
  import { loadedSkill, skillHeader, skillPreview } from "./skill-transcript.js";
@@ -58,6 +66,7 @@ import { readClipboardImage } from "./clipboard-image.js";
58
66
  import { queryBackgroundMode } from "./term-bg.js";
59
67
  import { runKingdomWarm, warmConcurrency, decomposeKingdom, looksBigGoal, collectConflicts, formatConflictList } from "../vascend/kingdom.js";
60
68
  import { worktreeMode } from "../vascend/worktree.js";
69
+ import { kingdomLive } from "../vascend/kingdom-live.js";
61
70
  import { runKingdomForHost } from "../vascend/kingdom-run.js";
62
71
  import { kingdomBus } from "../vascend/kingdom-bus.js";
63
72
  import { resolveKingdomBus } from "../vascend/kingdom-channel.js";
@@ -76,16 +85,22 @@ import { VascendPlanStore } from "../vascend/plan-store.js";
76
85
  import { reviewOptions, shouldReview, buildRoomMetricsDigest, buildReviewerPrompt } from "../vascend/reviewer.js";
77
86
  import { Session, listSessions, sessionFilePath } from "../core/session.js";
78
87
  import { sessionTitle } from "../core/session-title.js";
88
+ import { nameSession } from "../core/session-naming.js";
79
89
  import { ToolRegistry } from "../tools/registry.js";
80
90
  import { saveGlobalSettings } from "../core/settings.js";
81
91
  import { autoApproves, nextPermMode, permModeLabel } from "./perm-mode.js";
82
92
  import { backgroundManager } from "../core/background.js";
83
93
  import { BUILTIN_COMMANDS, visibleBuiltinNames, commandsByCategory, suggestCommands } from "./commands.js";
84
- import { TASK_STATUS_LABELS } from "./task-labels.js";
85
- import { rimuoviLicenzaLocale, rimuoviTokenVascend, revocaSessione, tokenVascend, } from "./vascend-login.js";
86
- import { paintToolName, paintCategoryName, pulseDot } from "./tool-style.js";
94
+ import { TASK_STATUS_LABELS, taskActivityLabel } from "./task-labels.js";
95
+ import { liveCommandAllowed } from "./live-command.js";
96
+ import { rimuoviLicenzaLocale, rimuoviTokenVascend, revocaSessione, } from "./vascend-login.js";
97
+ import { leggiSessione, revocaSessioneId, rimuoviSessione, tokenAgente } from "./vascend-id.js";
98
+ import { paintToolName, pulseDot } from "./tool-style.js";
99
+ import { toolArg, toolLabel } from "./tool-presentation.js";
87
100
  import { listVibeModes, parseVibeMode } from "../core/vibe-modes.js";
88
101
  import { contextProjectKey, loadContextFrame, renderContextFrame, resetContextFrame, updateContextPins, } from "../vascend/context-fabric.js";
102
+ import { collegaMani, fermaRemoto, messaggioUtente, osservaEvento, permessoConGara, sessioneRemota } from "../remote/index.js";
103
+ import { comandoRemoto } from "./remote-command.js";
89
104
  // (BUILTIN_COMMANDS spostato in ./commands.ts con categorie: vedi import in testa.)
90
105
  // Sorgente dinamica dei nomi comando (builtin + estensioni + template + skill).
91
106
  function allCommandNames(harness, templates) {
@@ -323,7 +338,10 @@ export async function startRepl(deps) {
323
338
  const stats = new SessionUsageState(costTracker);
324
339
  stats.restore(harness.config.sessionDir, harness.session.id, harness.session.tree().find(n => n.message.role === "assistant")?.ts);
325
340
  const review = new ReviewStore();
341
+ review.sessions.conversations.configure((buffer, width) => makeSink(theme, () => width, harness.config.cwd, env, { ...buffer, setStatus: () => { }, setTokens: () => { },
342
+ refresh: () => { } }, undefined, true), () => theme);
326
343
  review.reset(sessionConversation(harness.session));
344
+ review.sessions.restoreMain(readSessionUsage(harness.config.sessionDir, harness.session.id));
327
345
  const reviewContext = () => {
328
346
  const cost = costTracker.snapshot(harness.config.model);
329
347
  const tasks = vascend.state().kingdom.castles.flatMap(c => c.tasks);
@@ -348,7 +366,7 @@ export async function startRepl(deps) {
348
366
  ? last.cacheReadTokens / Math.max(1, last.inputTokens + last.cacheReadTokens)
349
367
  : undefined;
350
368
  return {
351
- provider: current ? (current.provider.label ?? current.provider.id) : "non configurato",
369
+ provider: current ? providerLabel(current.provider) : tr("non configurato", "not configured"),
352
370
  sessionId: harness.session.id,
353
371
  sessionNodes: stima.nodi,
354
372
  headId: harness.session.head,
@@ -411,7 +429,7 @@ export async function startRepl(deps) {
411
429
  const startupMeta = () => {
412
430
  const resolved = harness.providers.resolve(harness.config.model);
413
431
  return {
414
- provider: resolved ? (resolved.provider.label ?? resolved.provider.id) : "non configurato",
432
+ provider: resolved ? providerLabel(resolved.provider) : tr("non configurato", "not configured"),
415
433
  sessionId: harness.session.id,
416
434
  sessionNodes: harness.session.tree().length,
417
435
  headId: harness.session.head,
@@ -445,8 +463,10 @@ export async function startRepl(deps) {
445
463
  // questa sessione soltanto: dopo /new, `txt #1` sarebbe un altro testo.
446
464
  const pasteRegistry = new PasteRegistry();
447
465
  const resetSessionUi = (ctl) => {
466
+ kingdomLive.clearAll();
448
467
  ctl.clearConversation?.();
449
468
  review.reset(sessionConversation(harness.session));
469
+ review.sessions.restoreMain(readSessionUsage(harness.config.sessionDir, harness.session.id));
450
470
  imageRegistry.clear();
451
471
  pasteRegistry.reset();
452
472
  ctl.restoreQueue?.();
@@ -501,6 +521,7 @@ export async function startRepl(deps) {
501
521
  try {
502
522
  if (!line.startsWith("/")) {
503
523
  ctl.write(renderUserMessage(line, theme, terminalColumns(), fromQueue ? tr("Dalla coda · consegnato", "From queue · delivered") : undefined));
524
+ messaggioUtente(line);
504
525
  const imageParts = queued ? queued.parts.filter(part => part.type === "image") : imageRegistry.imagePartsFor(line);
505
526
  // In chat resta il riferimento compatto (`txt #1`); al modello va il
506
527
  // testo per intero. L'espansione avviene qui, dopo l'echo e prima del
@@ -518,6 +539,10 @@ export async function startRepl(deps) {
518
539
  ctl.write(reviewText(review, reviewContext(), theme, terminalColumns(), section));
519
540
  return;
520
541
  }
542
+ if (/^\/(?:agents|agenti)\s*$/i.test(line)) {
543
+ ctl.showAgents?.();
544
+ return;
545
+ }
521
546
  // /img <path>: allega esplicitamente un'immagine. La registra come "img #N"
522
547
  // e ne mostra l'anteprima inline; non avvia un turno (e' solo l'allegato).
523
548
  const imgCmd = /^\/img(?:\s+(.+))?$/i.exec(line);
@@ -628,7 +653,7 @@ export async function startRepl(deps) {
628
653
  ? { done: all.filter((t) => t.status === "completed").length, total: all.length, castles: castles.length }
629
654
  : null;
630
655
  return {
631
- model: harness.config.model,
656
+ model: harness.config.fusion ? `Fusion: Jev + ${harness.config.model}` : harness.config.model,
632
657
  provider: m.provider,
633
658
  cwd: harness.config.cwd,
634
659
  effort: harness.config.effort || "default",
@@ -650,15 +675,25 @@ export async function startRepl(deps) {
650
675
  };
651
676
  const input = new BottomInput({
652
677
  review,
678
+ onSessionSubmit: sendWorker,
679
+ bindSessionInbox: bindWorkerInbox,
680
+ wakeSession: wakeWorker,
681
+ onSessionStop: stopWorker,
682
+ onSessionDelete: deleteWorker,
683
+ onSessionBackground: session => ({ ok: !!backgroundManager.requestBackground(session.id) }),
684
+ sessionFooter: (session, width) => workerFooter(session, width, theme, stats, model => harness.providers.resolve(model)?.provider.id ?? "").slice(0, 1),
685
+ sessionBelowInput: (session, width) => workerFooter(session, width, theme, stats, model => harness.providers.resolve(model)?.provider.id ?? "").slice(1),
653
686
  createReviewScreen: (section) => new ReviewScreen(review, () => theme, reviewContext, section),
654
687
  nativeScroll: env["TALOS_NATIVE_SCROLL"] !== "0",
655
688
  headerLines: () => buildTerminalHeader(statusData(), theme),
656
689
  startupLines: () => buildTerminalStartup(statusData(), theme),
657
690
  promptText: () => theme.accent(harness.config.ui.prompt),
658
- placeholder: () => theme.muted(tr("Chiedi a Talos di fare qualcosa", "Ask Talos to do anything")),
691
+ placeholder: () => theme.muted(tr("Descrivi un'attività o digita / per i comandi", "Describe a task or type / for commands")),
659
692
  composerPadding: () => !harness.session.head,
660
693
  composerStyle: (s) => theme.composerText?.(s) ?? theme.chatText(s),
661
694
  attachmentStyle: (s) => theme.attachmentRef?.(s) ?? s,
695
+ currentModel: () => harness.config.model,
696
+ limitWarningStyle: s => theme.yellow(s),
662
697
  belowInputLines: () => {
663
698
  if (!harness.session.head)
664
699
  return [buildStartupRuntime(statusData(), theme)];
@@ -671,6 +706,7 @@ export async function startRepl(deps) {
671
706
  const providerId = harness.providers.resolve(harness.config.model)?.provider.id ?? "";
672
707
  return buildUsageFooter(statusData(), theme, stats, providerId, statusOpts()).slice(0, 1);
673
708
  },
709
+ warningLines: () => storageWarning(storageHealth.snapshot(harness.config.sessionDir), terminalColumns(), theme.yellow),
674
710
  completion,
675
711
  hint: slashHint,
676
712
  statusStyle: (s) => theme.dark(s),
@@ -692,14 +728,14 @@ export async function startRepl(deps) {
692
728
  // Ctrl+B: manda in background il comando shell in corso; se non ce n'e'
693
729
  // uno, mostra la lista dei task in background.
694
730
  onBackground: () => {
695
- const task = backgroundManager.requestBackground();
731
+ const task = backgroundManager.requestBackground(harness.session.id);
696
732
  if (task)
697
733
  input.write(theme.dim(`\n ⤓ in background [${task.id}]: ${truncate(task.command, 60)}\n`));
698
734
  else
699
735
  void showBackgroundPanel(input, theme, terminalColumns);
700
736
  },
701
737
  // Suffisso spinner: suggerisce Ctrl+B solo quando c'e' un comando in corso.
702
- runningHint: () => backgroundManager.hasForeground() ? "Ctrl+B sfondo" : null,
738
+ runningHint: () => backgroundManager.hasForeground(harness.session.id) ? "Ctrl+B sfondo" : null,
703
739
  prepareQueuedInput: text => [{ type: "text", text: pasteRegistry.expand(text) }, ...imageRegistry.imagePartsFor(text)],
704
740
  queuePersistence: () => new PromptQueueFile(harness.config.sessionDir, harness.session.id),
705
741
  restoreQueuedInput: entry => restoreQueuedInput(entry, imageRegistry),
@@ -709,6 +745,18 @@ export async function startRepl(deps) {
709
745
  if (result === "quit")
710
746
  input.requestQuit();
711
747
  },
748
+ onCommandDuringRun: async (line, signal) => {
749
+ const ctl = new Proxy(input, {
750
+ get(target, prop, receiver) {
751
+ if (prop === "busyCommands")
752
+ return true;
753
+ const value = Reflect.get(target, prop, receiver);
754
+ return typeof value === "function" ? value.bind(target) : value;
755
+ },
756
+ });
757
+ // Bypass processLine: its finally closes the active turn's inbox.
758
+ await handleSlash(line === "/" ? "/commands" : line, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, () => { });
759
+ },
712
760
  // Incollo: un path/data-URI immagine diventa "img #N", un testo lungo
713
761
  // diventa "txt #N", il resto si inserisce come testo normale.
714
762
  onPaste: onPasteInput,
@@ -721,23 +769,28 @@ export async function startRepl(deps) {
721
769
  return span ? { start: span.start, end: span.end } : null;
722
770
  },
723
771
  onReady: () => {
724
- input.write(buildTerminalWelcome(terminalColumns(), theme));
725
- if (accessoNota)
726
- input.write(wrapText(accessoNota, Math.max(2, terminalColumns() - 3), true).map((line) => theme.muted(" " + line.trim())).join("\n") + "\n");
727
- input.write("\n");
772
+ input.write(buildTerminalWelcome(terminalColumns(), theme, accessoNota));
728
773
  if (harness.session.head)
729
774
  replaySession(harness.session, input, theme, terminalColumns());
730
775
  if (deps.resumePicker)
731
776
  void processLine("/resume", input, new AbortController().signal);
732
777
  },
733
778
  onQuit: () => {
779
+ kingdomLive.clearAll();
780
+ review.sessions.conversations.reset();
734
781
  void harness.host.emit("session_shutdown", {});
735
- stdout.write("\n");
736
- process.exit(0);
782
+ const esci = () => { stdout.write("\n"); process.exit(0); };
783
+ // Il telefono deve sapere che la sessione e' finita, senza trattenere l'uscita.
784
+ if (sessioneRemota())
785
+ void Promise.race([fermaRemoto(), new Promise(r => setTimeout(r, 2_000))]).finally(esci);
786
+ else
787
+ esci();
737
788
  },
738
789
  });
739
790
  // Le notify delle estensioni vanno nella regione viva (no corruzione).
740
791
  commit = (t) => input.write(t);
792
+ // Controllo remoto: il telefono scrive e ferma attraverso la barra di input.
793
+ collegaMani({ invia: (testo, subito) => input.submitExternal(testo, subito), ferma: () => input.requestStop() });
741
794
  // UI reale per le estensioni: confirm/select/input passano dalla TUI (cosi'
742
795
  // un'estensione puo' fare domande in chat come il tool builtin ask_user).
743
796
  harness.host.bind({
@@ -778,11 +831,21 @@ export async function startRepl(deps) {
778
831
  backgroundManager.onFinish((t) => {
779
832
  if (!t.detached)
780
833
  return; // i comandi in primo piano mostrano gia' il risultato inline
781
- const mark = t.status === "done" ? theme.green("✓") : theme.red("✗");
834
+ if (t.status === "done") {
835
+ notifyStatus(input, tr("Background {id} completato · /bg {id} per l'output", "Background {id} completed · /bg {id} for output", { id: t.id }));
836
+ return;
837
+ }
838
+ const mark = theme.red("✗");
782
839
  input.write(`\n ${mark} background ${t.id} ` +
783
840
  theme.dim(`[${t.status}${t.exitCode != null ? ` · uscita ${t.exitCode}` : ""}] ${truncate(t.command, 50)} · /bg ${t.id} per l'output\n`));
784
841
  });
785
- await input.start();
842
+ const unsubscribeStorage = storageHealth.subscribe(() => input.refresh());
843
+ try {
844
+ await input.start();
845
+ }
846
+ finally {
847
+ unsubscribeStorage();
848
+ }
786
849
  return;
787
850
  }
788
851
  // Fallback (non-TTY o TALOS_CLASSIC_INPUT=1): loop readline classico inline.
@@ -790,6 +853,13 @@ export async function startRepl(deps) {
790
853
  if (harness.session.head)
791
854
  replaySession(harness.session, { write: text => stdout.write(text) }, theme, terminalColumns());
792
855
  const rl = readline.createInterface({ input: stdin, output: stdout });
856
+ const reportStorage = () => {
857
+ const rows = storageWarning(storageHealth.snapshot(harness.config.sessionDir), terminalColumns(), theme.yellow);
858
+ if (rows.length)
859
+ stdout.write("\n" + rows.join("\n") + "\n");
860
+ };
861
+ const unsubscribeStorage = storageHealth.subscribe(reportStorage);
862
+ reportStorage();
793
863
  const ctl = {
794
864
  review,
795
865
  pause: () => rl.pause(),
@@ -826,7 +896,7 @@ export async function startRepl(deps) {
826
896
  processLine: (raw, signal) => processLine(raw, ctl, signal),
827
897
  prompt,
828
898
  requestClose: () => rl.close(),
829
- onError: (error) => notifyStatus(ctl, theme.red(`\n errore: ${error.message}\n`)),
899
+ onError: (error) => notifyStatus(ctl, theme.red(`\n ${tr("Errore", "Error")}: ${runtimeError(error.message)}\n`)),
830
900
  installAbort: (abort) => {
831
901
  const onSigint = () => abort();
832
902
  process.once("SIGINT", onSigint);
@@ -848,6 +918,7 @@ export async function startRepl(deps) {
848
918
  void lineQueue.enqueue(raw);
849
919
  });
850
920
  rl.on("close", () => {
921
+ unsubscribeStorage();
851
922
  lineQueue.close();
852
923
  harness.dispose?.();
853
924
  void harness.host.emit("session_shutdown", {});
@@ -868,15 +939,26 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
868
939
  // Gate permessi: selezione Sì/No dalla UI con le frecce (stile Claude Code),
869
940
  // niente piu' digitazione di y/n. Le modalita' "accetta modifiche"/"auto"
870
941
  // (Shift+Tab) saltano il prompt per le azioni che approvano.
871
- const confirm = async (action, detail) => {
942
+ const confirm = async (action, detail, requestSignal = signal) => {
872
943
  if (autoApproves(getPermMode(), action))
873
944
  return true;
945
+ // Con il controllo remoto acceso risponde chi arriva prima, TUI o telefono.
946
+ return permessoConGara(action, detail, requestSignal, (gara) => confirmInTui(action, detail, gara));
947
+ };
948
+ const confirmInTui = async (action, detail, requestSignal) => {
874
949
  const choice = await selectOption({
875
- title: `consenti: ${action}`,
876
- hint: detail,
950
+ signal: requestSignal,
951
+ title: tr("consenti: {p0}", "Allow: {p0}", { p0: action }),
952
+ hint: tr("{p0}\nShift+Tab: cambia permessi anche durante la conferma", "{p0}\nShift+Tab: change permissions during confirmation", { p0: detail }),
953
+ onCycleMode: ctl.cyclePermissionMode ? () => {
954
+ ctl.cyclePermissionMode();
955
+ return autoApproves(getPermMode(), action)
956
+ ? { label: tr("Consentito dalla modalità corrente", "Allowed by the current mode"), detail: "", value: true }
957
+ : undefined;
958
+ } : undefined,
877
959
  options: [
878
- { key: "yes", label: "Sì, esegui", detail: "consenti questa azione", value: true },
879
- { key: "no", label: "No, annulla", detail: "blocca l'azione", value: false },
960
+ { key: "yes", label: tr("Sì, esegui", "Yes, run it"), detail: tr("consenti questa azione", "allow this action"), value: true },
961
+ { key: "no", label: tr("No, annulla", "No, cancel"), detail: tr("blocca l'azione", "block the action"), value: false },
880
962
  ],
881
963
  currentKey: "yes",
882
964
  ctl,
@@ -886,11 +968,12 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
886
968
  return choice?.value === true;
887
969
  };
888
970
  // Domanda all'utente in chat (tool ask_user): opzioni -> picker; libera -> input.
889
- const ask = async (question, options) => {
971
+ const ask = async (question, options, requestSignal = signal) => {
890
972
  if (ctl.queueQuestion)
891
- return ctl.queueQuestion(question, options, signal);
973
+ return ctl.queueQuestion(question, options, requestSignal);
892
974
  if (options && options.length) {
893
975
  const choice = await selectOption({
976
+ signal: requestSignal,
894
977
  title: question,
895
978
  question: true,
896
979
  options: options.map((o) => ({ label: o, detail: "", value: o })),
@@ -901,15 +984,20 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
901
984
  return choice?.value ?? null;
902
985
  }
903
986
  ctl.write(renderQuestion(question, theme, terminalColumns()));
904
- return ctl.promptInput("rispondi qui (Invio conferma · Esc annulla)");
987
+ return ctl.promptInput(tr("rispondi qui (Invio conferma · Esc annulla)", "Reply here (Enter confirm · Esc cancel)"));
905
988
  };
906
989
  // Kingdoms Mode invocabile dall'agente (tool kingdoms_warm): esegue i castelli
907
990
  // in parallelo (vassalli) e ritorna i report. Il consenso e' gia' stato dato
908
991
  // al gate del tool (ctx.confirm); qui i vassalli eseguono i tool in blocco.
992
+ const onWorkerEvent = (actor, event) => {
993
+ if (event.type !== "kingdom" && event.type !== "usage")
994
+ ctl.review?.recordActor(actor, event);
995
+ };
909
996
  const spawnKingdom = async (castles, kopts) => {
910
997
  if (kopts?.detach)
911
998
  return runKingdomForHost({
912
- harness: deps.harness, castles, signal, sink: wrappedSink, confirm, ask, env, detach: true,
999
+ harness: deps.harness, castles, signal, sink: wrappedSink, confirm, ask, env, detach: true, retainSessions: "queued",
1000
+ onWorkerEvent,
913
1001
  ...(kopts.baseBranch ? { baseBranch: kopts.baseBranch } : {}),
914
1002
  });
915
1003
  // Obiettivo complessivo del regno: esplicito > goal grezzo del turno.
@@ -941,6 +1029,7 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
941
1029
  concurrency: conc,
942
1030
  ask,
943
1031
  onEvent: live.onEvent,
1032
+ retainSessions: "queued",
944
1033
  worktree: wt,
945
1034
  ...(kopts?.baseBranch ? { baseBranch: kopts.baseBranch } : {}),
946
1035
  onNotice: (text) => notifyStatus(ctl, theme.accent(" ⎇ ") + theme.dim(text) + "\n"),
@@ -1007,6 +1096,7 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
1007
1096
  ctl.setStatus(tr("avvio", "starting"));
1008
1097
  let turnFailed = false;
1009
1098
  const wrappedSink = (event) => {
1099
+ osservaEvento(event);
1010
1100
  if (event.type === "error" || (event.type === "turn_end" && event.stopReason === "error"))
1011
1101
  turnFailed = true;
1012
1102
  ctl.review?.record(event);
@@ -1057,7 +1147,7 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
1057
1147
  const turnInput = imageParts && imageParts.length
1058
1148
  ? [{ type: "text", text: input }, ...imageParts]
1059
1149
  : input;
1060
- await runTurn({ ...deps.harness.deps(confirm), ask, spawnKingdom, inbox }, turnInput, wrappedSink, signal);
1150
+ await runTurn({ ...deps.harness.deps(confirm), env: { ...env, TALOS_AUTO_TITLE: "1" }, ask, spawnKingdom, onWorkerEvent, inbox }, turnInput, wrappedSink, signal);
1061
1151
  if (turnFailed || signal.aborted)
1062
1152
  return;
1063
1153
  // REVISORE di team: per i task complessi (molte stanze) con stanze fatte e
@@ -1122,8 +1212,14 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
1122
1212
  }
1123
1213
  }
1124
1214
  catch (err) {
1125
- ctl.review?.record({ type: "error", message: err.message });
1126
- notifyStatus(ctl, theme.red(`\n errore: ${err.message}\n`));
1215
+ if (signal.aborted) {
1216
+ ctl.setRoomStatus?.(null);
1217
+ ctl.write(theme.yellow(`\n ${tr("Interrotto · puoi inviare un nuovo messaggio per continuare.", "Interrupted · send a new message to continue.")}\n`));
1218
+ }
1219
+ else {
1220
+ ctl.review?.record({ type: "error", message: err.message });
1221
+ notifyStatus(ctl, theme.red(`\n ${tr("Errore", "Error")}: ${runtimeError(err.message)}\n`));
1222
+ }
1127
1223
  }
1128
1224
  finally {
1129
1225
  ctl.review?.finish(signal.aborted);
@@ -1209,7 +1305,7 @@ async function runTeamReviewer(deps, vascend, sdir, sid, ctl, theme, terminalCol
1209
1305
  }
1210
1306
  vascend.loadPlan(sdir, sid);
1211
1307
  }
1212
- export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1308
+ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan, replay = false) {
1213
1309
  let textBuffer = "";
1214
1310
  let assistantSegments = 0;
1215
1311
  let assistantStarted = false;
@@ -1217,6 +1313,11 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1217
1313
  const writeNotice = (text, failed = false) => {
1218
1314
  notifyStatus(ctl, text, failed);
1219
1315
  };
1316
+ const writeWarning = (text) => {
1317
+ ctl.setRoomStatus?.(null);
1318
+ const rows = wrapText(text, Math.max(8, terminalColumns() - 4));
1319
+ ctl.write("\n" + rows.map(row => theme.yellow(` ${row}`)).join("\n") + "\n");
1320
+ };
1220
1321
  // Correlazione call->risultato e contenuto PRE-scrittura, per gli avvisi del
1221
1322
  // regno e per il diff dei tool di scrittura.
1222
1323
  const calls = new Map();
@@ -1226,6 +1327,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1226
1327
  const mcpCards = new Map();
1227
1328
  const toolCards = new Map();
1228
1329
  const skillCards = new Map();
1330
+ const backgroundPolls = new Map();
1229
1331
  let newInputTok = 0; // input non in cache del run (cumulativo)
1230
1332
  let cacheTok = 0; // cache letta/scritta nel run (cumulativo)
1231
1333
  let outTok = 0; // token generati nel run (cumulativi)
@@ -1281,21 +1383,21 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1281
1383
  }
1282
1384
  // Riposa il bullet allo stato statico (accent) come il resto dello scrollback.
1283
1385
  if (settle && liveDot && ctl.setLogLine) {
1284
- ctl.setLogLine(liveDot.index, fitLine(theme.dark(" │ ") + theme.accent("● ") + liveDot.tail, terminalColumns() - 1));
1386
+ ctl.setLogLine(liveDot.index, fitLine(" " + theme.muted("◇ ") + liveDot.tail, terminalColumns() - 1));
1285
1387
  }
1286
1388
  liveDot = null;
1287
1389
  };
1288
1390
  const startDot = (index, name, tail) => {
1289
1391
  stopDot(false); // chiudi l'eventuale precedente senza ridisegnare (verra' sovrascritto)
1290
- if (!ctl.setLogLine)
1291
- return; // fallback readline: niente animazione scrollback
1392
+ if (!ctl.setLogLine || replay)
1393
+ return; // Replay has no timers or terminal side effects.
1292
1394
  liveDot = { index, name, tail };
1293
1395
  liveFrame = 0;
1294
1396
  dotTimer = setInterval(() => {
1295
1397
  if (!liveDot || !ctl.setLogLine)
1296
1398
  return;
1297
1399
  liveFrame += 1;
1298
- ctl.setLogLine(liveDot.index, fitLine(theme.dark(" │ ") + pulseDot(liveDot.name, liveFrame) + " " + liveDot.tail, terminalColumns() - 1));
1400
+ ctl.setLogLine(liveDot.index, fitLine(" " + pulseDot(liveDot.name, liveFrame) + " " + liveDot.tail, terminalColumns() - 1));
1299
1401
  }, 140);
1300
1402
  };
1301
1403
  const beginCommand = () => {
@@ -1304,7 +1406,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1304
1406
  stopDot();
1305
1407
  ctl.write("\n");
1306
1408
  };
1307
- return (event) => {
1409
+ const sink = (event) => {
1308
1410
  switch (event.type) {
1309
1411
  case "context_window":
1310
1412
  ctl.refresh?.();
@@ -1335,7 +1437,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1335
1437
  // come evento status separato.
1336
1438
  lastTruncated = event.stopReason === "max_tokens";
1337
1439
  if (event.stopReason === "max_tokens") {
1338
- notifyStatus(ctl, theme.dim("\n ⟳ output al limite del modello: continuo automaticamente…\n"));
1440
+ notifyStatus(ctl, theme.dim(`\n ${tr("Output al limite del modello: continuo automaticamente…", "Model output limit reached: continuing automatically…")}\n`));
1339
1441
  }
1340
1442
  break;
1341
1443
  case "status":
@@ -1344,6 +1446,15 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1344
1446
  break;
1345
1447
  // Attivita' del modello senza testo (es. streaming di un input grande).
1346
1448
  ctl.setStatus(event.label);
1449
+ {
1450
+ const limit = event.label.match(/^Limite sessione di 5 ore raggiunto · reset alle (.+?) · riprendo automaticamente al reset · Esc per annullare$/);
1451
+ if (limit) {
1452
+ flushText("final");
1453
+ const time = limit[1];
1454
+ writeWarning(tr("Limite della sessione raggiunto · reset alle {time}", "You've hit your session limit · resets {time}", { time }));
1455
+ writeWarning(tr("Ripresa automatica alle {time} · Esc per annullare", "Continuing automatically at {time} · esc to cancel", { time: time.replace(/\s*\([^)]*\)$/, "") }));
1456
+ }
1457
+ }
1347
1458
  break;
1348
1459
  case "text":
1349
1460
  ctl.setStatus(tr("risposta in corso", "responding"));
@@ -1358,6 +1469,10 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1358
1469
  parent.child = true;
1359
1470
  }
1360
1471
  calls.set(event.call.id, event.call);
1472
+ if (event.call.name === "bash_output") {
1473
+ ctl.setStatus(tr("leggo output background", "reading background output"));
1474
+ break;
1475
+ }
1361
1476
  // Compact protocol wrappers emit real child calls. Show those commands
1362
1477
  // once, without a duplicate execute(args) envelope in the transcript.
1363
1478
  if (event.call.name === "execute" || event.call.name === "tool_invoke" || event.call.name === "run_program") {
@@ -1404,7 +1519,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1404
1519
  ctl.write(preview.lines.join("\n") + "\n");
1405
1520
  commandCards.set(event.call.id, { index: ctl.logLength() - preview.lines.length,
1406
1521
  firstRow: preview.firstRow, columns, end: ctl.logLength(), header: preview.lines[0] });
1407
- ctl.setStatus(tr("esegue comando terminale", "running terminal command"));
1522
+ ctl.setStatus(tr("Esecuzione comando", "Running command"));
1408
1523
  ctl.setStatusTool?.(event.call.name);
1409
1524
  break;
1410
1525
  }
@@ -1421,11 +1536,11 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1421
1536
  const st = String(event.call.input["status"] ?? "completed");
1422
1537
  const where = slug === "master" ? "" : `${slug}/`;
1423
1538
  if (st === "running")
1424
- ctl.setRoomStatus?.(`attività ${where}${task} avviata`, "running");
1539
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "running"), "running");
1425
1540
  else if (st === "failed")
1426
- ctl.setRoomStatus?.(`attività ${where}${task} non riuscita`, "failed");
1541
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "failed"), "failed");
1427
1542
  else
1428
- ctl.setRoomStatus?.(`attività ${where}${task} completata`, "done");
1543
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "completed"), "done");
1429
1544
  break;
1430
1545
  }
1431
1546
  // Each operation owns a small separate block. Full data stays in review.
@@ -1438,7 +1553,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1438
1553
  calls.set(event.call.id, event.call);
1439
1554
  // Cattura il contenuto PRIMA della scrittura, per il diff (il file non
1440
1555
  // e' ancora stato toccato a questo punto del loop).
1441
- if (event.call.name === "write_file") {
1556
+ if (!replay && event.call.name === "write_file") {
1442
1557
  const p = event.call.input["path"];
1443
1558
  if (typeof p === "string" && p) {
1444
1559
  const abs = isAbsolute(p) ? p : resolve(cwd, p);
@@ -1454,18 +1569,15 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1454
1569
  const argRendered = typeof pathArg === "string" && pathArg && argText === pathArg
1455
1570
  ? fileLink(isAbsolute(pathArg) ? pathArg : resolve(cwd, pathArg), theme.dim(pathArg))
1456
1571
  : theme.dim(argText);
1457
- // Nome del tool COLORATO per categoria (read/write/shell/search/...): a
1458
- // colpo d'occhio si distingue il tipo di azione. Git eredita la categoria
1459
- // shell (e' un comando bash).
1460
- const nameRendered = gitSum
1461
- ? paintCategoryName("shell", "Git")
1462
- : paintToolName(event.call.name, toolLabel(event.call.name));
1572
+ // Quiet action headers leave the emphasis to results and changed code.
1573
+ const nameRendered = theme.muted(label);
1463
1574
  // Coda statica della riga (tutto cio' che segue il bullet): la riusiamo
1464
1575
  // sia per la scrittura iniziale sia per l'animazione del bullet.
1465
- const tail = nameRendered + theme.dim("(") + argRendered + theme.dim(")");
1466
- const header = fitLine(theme.dark(" │ ") + theme.accent("● ") + tail, terminalColumns() - 1);
1576
+ const tail = nameRendered + " " + argRendered;
1577
+ const header = fitLine(" " + theme.muted("◇ ") + tail, terminalColumns() - 1);
1578
+ const index = ctl.logLength();
1467
1579
  ctl.write(header + "\n");
1468
- toolCards.set(event.call.id, { end: ctl.logLength(), header });
1580
+ toolCards.set(event.call.id, { index, end: ctl.logLength(), header });
1469
1581
  if (event.call.name === "ask_user")
1470
1582
  startDot(ctl.logLength() - 1, event.call.name, tail);
1471
1583
  break;
@@ -1474,6 +1586,9 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1474
1586
  ctl.setStatus(tr("elaboro", "working"));
1475
1587
  stopDot(); // ferma l'animazione del bullet: il tool e' finito
1476
1588
  const call = calls.get(event.result.callId);
1589
+ const before = preContent.get(event.result.callId);
1590
+ calls.delete(event.result.callId);
1591
+ preContent.delete(event.result.callId);
1477
1592
  const toolCard = toolCards.get(event.result.callId);
1478
1593
  toolCards.delete(event.result.callId);
1479
1594
  if (event.parentId) {
@@ -1483,6 +1598,20 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1483
1598
  parent.isError = event.result.isError;
1484
1599
  }
1485
1600
  }
1601
+ if (call?.name === "bash_output") {
1602
+ const id = String(call.input["id"] ?? call.id);
1603
+ const snapshot = JSON.stringify([event.result.isError ?? false, event.result.output, event.result.media]);
1604
+ if (backgroundPolls.get(id) === snapshot)
1605
+ break;
1606
+ backgroundPolls.set(id, snapshot);
1607
+ flushText();
1608
+ ensureAssistantStarted();
1609
+ const summary = backgroundResultSummary(event.result.output, terminalColumns());
1610
+ ctl.write("\n" + fitLine(" " + (event.result.isError ? theme.red("× ") : theme.muted("◇ "))
1611
+ + theme.muted(toolLabel("bash_output")), terminalColumns() - 1) + "\n"
1612
+ + theme.dark(" └ ") + (event.result.isError ? theme.red(summary) : theme.dim(summary)) + "\n");
1613
+ break;
1614
+ }
1486
1615
  const skillCard = skillCards.get(event.result.callId);
1487
1616
  if (skillCard) {
1488
1617
  skillCards.delete(event.result.callId);
@@ -1534,7 +1663,8 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1534
1663
  const card = commandCards.get(event.result.callId);
1535
1664
  if (card && call) {
1536
1665
  const state = commandResultState(event.result.output, event.result.isError);
1537
- const retained = !ctl.getLogLine || ctl.getLogLine(card.index) === card.header;
1666
+ const retained = !ctl.getLogLine || (ctl.getLogLine(card.index) === card.header
1667
+ && ctl.getLogLine(card.index + 1) === commandPreview(commandSource(call), theme, card.columns).lines[1]);
1538
1668
  if (retained)
1539
1669
  ctl.setLogLine?.(card.index, commandHeader(card.firstRow, state, theme, card.columns));
1540
1670
  // Parallel calls can complete in a different order. Repeat the
@@ -1548,7 +1678,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1548
1678
  break;
1549
1679
  }
1550
1680
  if (call && !event.result.isError && call.name === "write_file") {
1551
- ctl.review?.change(call.id, { before: call.input["append"] === true ? "" : preContent.get(call.id),
1681
+ ctl.review?.change(call.id, { before: call.input["append"] === true ? "" : before,
1552
1682
  after: String(call.input["content"] ?? ""), scope: call.input["append"] === true ? "append" : "file" });
1553
1683
  }
1554
1684
  // Token per stanza/castello (solo con vascend attivo). Snapshot a
@@ -1564,11 +1694,11 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1564
1694
  const st = String(call.input["status"] ?? "completed");
1565
1695
  const where = slug === "master" ? "" : `${slug}/`;
1566
1696
  if (st === "running")
1567
- ctl.setRoomStatus?.(`attività ${where}${task} avviata`, "running");
1697
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "running"), "running");
1568
1698
  else if (st === "failed")
1569
- ctl.setRoomStatus?.(`attività ${where}${task} non riuscita`, "failed");
1699
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "failed"), "failed");
1570
1700
  else
1571
- ctl.setRoomStatus?.(`attività ${where}${task} completata`, "done");
1701
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "completed"), "done");
1572
1702
  break;
1573
1703
  }
1574
1704
  // Detailed plans are available in the task panel.
@@ -1579,6 +1709,23 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1579
1709
  notifyStatus(ctl, notice);
1580
1710
  break;
1581
1711
  }
1712
+ const diff = call && !event.result.isError
1713
+ ? diffForCall(call, before, event.result.output, theme, terminalColumns())
1714
+ : undefined;
1715
+ if (diff) {
1716
+ const retained = Boolean(toolCard && (!ctl.getLogLine || ctl.getLogLine(toolCard.index) === toolCard.header));
1717
+ // Con un risultato sequenziale trasformiamo Update(path) direttamente
1718
+ // nell'intestazione finale. Nessun secondo riepilogo "modificato" e
1719
+ // nessuna intestazione duplicata sopra la diff.
1720
+ if (toolCard && retained && ctl.setLogLine && toolCard.end === ctl.logLength()) {
1721
+ ctl.setLogLine(toolCard.index, fitLine(diff.header, terminalColumns() - 1));
1722
+ }
1723
+ else {
1724
+ ctl.write("\n" + fitLine(diff.header, terminalColumns() - 1) + "\n");
1725
+ }
1726
+ ctl.write(previewDiff(diff.body, theme, terminalColumns() - 1, diff.hiddenRows));
1727
+ break;
1728
+ }
1582
1729
  // Associate out-of-order results with their own operation rather than
1583
1730
  // appending several anonymous result lines below the newest call.
1584
1731
  if (toolCard && toolCard.end !== ctl.logLength())
@@ -1588,18 +1735,22 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1588
1735
  // muted (piu' leggibile del dim slavato, ma chiaramente secondario).
1589
1736
  {
1590
1737
  const summary = liveResultSummary(event.result.output, event.result.isError ? null : resultSummary(call, event.result.output), terminalColumns());
1591
- ctl.write(theme.dark(" │ ") + (event.result.isError ? theme.red("× ") : theme.dark("⎿ ")) +
1738
+ ctl.write(theme.dark(" └ ") + (event.result.isError ? theme.red("× ") : "") +
1592
1739
  (event.result.isError ? theme.red(summary) : theme.dim(summary)) + "\n");
1593
1740
  }
1594
- // Keep the change preview alongside its operation.
1595
- if (call && !event.result.isError) {
1596
- const diff = diffForCall(call, preContent.get(event.result.callId), event.result.output, theme, terminalColumns());
1597
- if (diff) {
1598
- ctl.write(previewDiff(diff, theme, terminalColumns() - 1));
1599
- }
1600
- }
1601
1741
  break;
1602
1742
  }
1743
+ case "session_naming":
1744
+ ctl.setSessionNaming?.(event.phase, event.title);
1745
+ break;
1746
+ case "compacting":
1747
+ stopDot();
1748
+ flushText();
1749
+ ctl.setCompacting?.(true);
1750
+ break;
1751
+ case "compaction_end":
1752
+ ctl.setCompacting?.(false);
1753
+ break;
1603
1754
  case "compacted":
1604
1755
  stopDot();
1605
1756
  flushText();
@@ -1635,7 +1786,12 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1635
1786
  ctl.setLogLine?.(card.index, commandHeader(card.firstRow, "stopped", theme, card.columns));
1636
1787
  }
1637
1788
  commandCards.clear();
1638
- writeNotice(`errore: ${event.message}`, true);
1789
+ if (isUserInterruption(event.message)) {
1790
+ writeWarning(tr("Interrotto · puoi inviare un nuovo messaggio per continuare.", "Interrupted · send a new message to continue."));
1791
+ }
1792
+ else {
1793
+ writeNotice(`${tr("Errore", "Error")}: ${runtimeError(event.message)}`, true);
1794
+ }
1639
1795
  break;
1640
1796
  case "done":
1641
1797
  for (const card of mcpCards.values()) {
@@ -1654,11 +1810,15 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1654
1810
  // Auto-continuazione esaurita: il run finisce ancora troncato. Avvisa di
1655
1811
  // spezzare il compito (o alzare TALOS_MAX_AUTOCONTINUE).
1656
1812
  if (lastTruncated) {
1657
- notifyStatus(ctl, theme.yellow("\n ⚠ output ancora al limite del modello dopo le continuazioni automatiche: spezza il compito o alza TALOS_MAX_AUTOCONTINUE.\n"));
1813
+ notifyStatus(ctl, theme.yellow(`\n ${tr("Output ancora al limite dopo le continuazioni automatiche: suddividi il compito o aumenta TALOS_MAX_AUTOCONTINUE.", "Output still at the limit after automatic continuations: split the task or increase TALOS_MAX_AUTOCONTINUE.")}\n`));
1658
1814
  }
1659
1815
  break;
1660
1816
  }
1661
1817
  };
1818
+ return Object.assign(sink, { preview: () => {
1819
+ const text = stripToolMarkup(textBuffer).trimEnd();
1820
+ return text ? renderAssistantSegment(text, theme, terminalColumns(), { cwd, env }, assistantStarted ? "progress" : "start", assistantSegments + 1) : "";
1821
+ } });
1662
1822
  }
1663
1823
  // Attribuzione token alle stanze + riepilogo per castello (metodo Danilov).
1664
1824
  // All'avvio della stanza (running) salva lo snapshot di outTok; a stanza
@@ -1847,10 +2007,10 @@ function renderVascendToolNotice(call, output, theme, columns) {
1847
2007
  const st = String(call.input["status"] ?? "completed");
1848
2008
  const where = slug === "master" ? "" : `${slug}/`;
1849
2009
  if (st === "running")
1850
- return renderVascendNotice("start", `attività ${where}${task} avviata`, theme);
2010
+ return renderVascendNotice("start", taskActivityLabel(`${where}${task}`, "running"), theme);
1851
2011
  if (st === "failed")
1852
- return renderVascendNotice("fail", `attività ${where}${task} non riuscita`, theme);
1853
- return renderVascendNotice("done", `attività ${where}${task} completata`, theme);
2012
+ return renderVascendNotice("fail", taskActivityLabel(`${where}${task}`, "failed"), theme);
2013
+ return renderVascendNotice("done", taskActivityLabel(`${where}${task}`, "completed"), theme);
1854
2014
  }
1855
2015
  return null;
1856
2016
  }
@@ -1862,26 +2022,26 @@ function diffForCall(call, pre, output, theme, columns) {
1862
2022
  const append = call.input["append"] === true;
1863
2023
  const next = String(call.input["content"] ?? "");
1864
2024
  const existed = (pre ?? "") !== "";
1865
- const verb = append ? "aggiunto" : existed ? "modificato" : "creato";
2025
+ const verb = append ? tr("aggiunto", "appended") : existed ? tr("modificato", "modified") : tr("creato", "created");
1866
2026
  // In append mostriamo solo l'aggiunta (base vuota); altrimenti il delta vs file.
1867
- return renderDiff(append ? "" : (pre ?? ""), next, theme, { path, verb, columns: columns - 1, wrapLines: true });
2027
+ return renderDiffParts(append ? "" : (pre ?? ""), next, theme, { path, verb, columns: columns - 1, wrapLines: true, maxRows: 100 }) ?? undefined;
1868
2028
  }
1869
2029
  if (call.name === "edit_file" && output.startsWith("modificato:")) {
1870
2030
  const path = typeof call.input["path"] === "string" ? call.input["path"] : undefined;
1871
2031
  const oldS = String(call.input["old_string"] ?? "");
1872
2032
  const newS = String(call.input["new_string"] ?? "");
1873
- return renderDiff(oldS, newS, theme, { path, verb: "modificato", context: 2, columns: columns - 1, wrapLines: true });
2033
+ return renderDiffParts(oldS, newS, theme, { path, verb: tr("modificato", "modified"), context: 2, columns: columns - 1, wrapLines: true, maxRows: 100 }) ?? undefined;
1874
2034
  }
1875
- return "";
2035
+ return undefined;
1876
2036
  }
1877
2037
  // Soglia riferita alle righe del confronto, escluse intestazione e separatore.
1878
- function previewDiff(full, theme, width) {
2038
+ function previewDiff(full, theme, width, hiddenRows = 0) {
1879
2039
  const lines = full.replace(/\n$/, "").split("\n");
1880
- const limit = 100 + 2;
1881
- if (lines.length <= limit)
2040
+ const limit = 100;
2041
+ if (lines.length <= limit && hiddenRows === 0)
1882
2042
  return full;
1883
2043
  const head = lines.slice(0, limit);
1884
- const hidden = lines.length - limit;
2044
+ const hidden = Math.max(0, lines.length - limit) + hiddenRows;
1885
2045
  const hint = ` … altre ${hidden} righe · Ctrl+O scegli file`;
1886
2046
  head.push(theme.dim(fitLine(terminalWidth(hint) <= width ? hint : ` Ctrl+O · +${hidden} righe`, width)));
1887
2047
  return head.join("\n") + "\n";
@@ -2061,6 +2221,7 @@ function sessionOptions(list) {
2061
2221
  label: sessionTitle(s.name ?? "") || s.preview || s.id,
2062
2222
  detail: `${fmtAge(s.updatedAt)} · ${s.messages} msg · ${s.id}`,
2063
2223
  value: s.id,
2224
+ age: fmtAge(s.updatedAt),
2064
2225
  }));
2065
2226
  }
2066
2227
  // Riprende una sessione: la carica nell'harness (le closure vedono il nuovo
@@ -2083,115 +2244,6 @@ function resumeInto(harness, vascend, id, ctl, theme, tcols, resetSessionUi) {
2083
2244
  replaySession(loaded, ctl, theme, tcols());
2084
2245
  return true;
2085
2246
  }
2086
- // Etichetta leggibile del tool (stile Claude Code: Read/Write/Update/Bash...).
2087
- function toolLabel(name) {
2088
- const map = {
2089
- read_file: "Read",
2090
- write_file: "Write",
2091
- edit_file: "Update",
2092
- list_dir: "List",
2093
- bash: "Bash",
2094
- bash_output: "BashOutput",
2095
- web_fetch: "Fetch",
2096
- ask_user: "Ask",
2097
- wait: "Wait",
2098
- tool_search: "ToolSearch",
2099
- tool_invoke: "Invoke",
2100
- skill_search: "SkillSearch",
2101
- skill_load: "SkillLoad",
2102
- output_page: "Page",
2103
- kingdoms_warm: tr("Collaboratori", "Collaborators"),
2104
- kingdom_send: "Send",
2105
- kingdom_inbox: "Inbox",
2106
- kingdom_wait: "WaitMsg",
2107
- glob: "Glob",
2108
- grep: "Grep",
2109
- recall: "Recall",
2110
- image_generate: "Image",
2111
- vascend_plan: tr("Lista attività", "Task list"),
2112
- vascend_mark: tr("Stato attività", "Task status"),
2113
- vascend_note: "Note",
2114
- vascend_skill: "Skill",
2115
- vascend_addtasks: tr("Aggiungi attività", "Add tasks"),
2116
- vascend_addrooms: tr("Aggiungi attività", "Add tasks"),
2117
- vascend_status: tr("Lista attività", "Task list"),
2118
- };
2119
- return map[name] ?? name;
2120
- }
2121
- // Argomento mostrato accanto al tool: il path se c'e', il comando per bash,
2122
- // altrimenti gli argomenti compatti.
2123
- function toolArg(call) {
2124
- const p = call.input["path"];
2125
- if (call.name === "run_program") {
2126
- const code = String(call.input["code"] ?? "");
2127
- return tr("JavaScript · {count} righe · Ctrl+R dettagli", "JavaScript · {count} lines · Ctrl+R details", { count: code.split("\n").length });
2128
- }
2129
- if (call.name === "tool_invoke") {
2130
- const inner = String(call.input["name"] ?? "");
2131
- const a = call.input["args"] && typeof call.input["args"] === "object" ? compactArgs(call.input["args"]) : "";
2132
- return truncate(`${inner}${a && a !== "{}" ? ` ${a}` : ""}`, 56);
2133
- }
2134
- if (call.name === "tool_search" || call.name === "skill_search")
2135
- return truncate(String(call.input["query"] ?? ""), 50);
2136
- if (call.name === "skill_load")
2137
- return String(call.input["name"] ?? "");
2138
- if (call.name === "grep" || call.name === "glob")
2139
- return truncate(String(call.input["pattern"] ?? ""), 50);
2140
- if (call.name === "wait") {
2141
- const until = call.input["until"];
2142
- return typeof until === "string" && until ? `until ${truncate(until, 44)}` : `${call.input["seconds"] ?? 5}s`;
2143
- }
2144
- // Tool del metodo Danilov: argomento LEGGIBILE invece del JSON grezzo
2145
- // (es. Mark(T01 → running) al posto di Mark({"task":"T01","status":"running"})).
2146
- const danilov = vascendArg(call);
2147
- if (danilov !== null)
2148
- return danilov;
2149
- if (typeof p === "string" && p)
2150
- return p;
2151
- if (call.name === "bash")
2152
- return truncate(oneLine(String(call.input["command"] ?? "")), 60);
2153
- return compactArgs(call.input);
2154
- }
2155
- // Argomento LEGGIBILE per i tool del metodo Danilov: trasforma l'input grezzo
2156
- // in una sintesi human-friendly. Ritorna null se non e' un tool Danilov noto
2157
- // (cosi' toolArg prosegue con la sua logica standard).
2158
- function vascendArg(call) {
2159
- const i = call.input;
2160
- const castle = typeof i["castle"] === "string" && i["castle"] && i["castle"] !== "master" ? ` @${i["castle"]}` : "";
2161
- switch (call.name) {
2162
- case "vascend_mark": {
2163
- const task = String(i["task"] ?? "");
2164
- const status = String(i["status"] ?? "completed");
2165
- return truncate(`${task} → ${status}${castle}`, 56);
2166
- }
2167
- case "vascend_plan": {
2168
- const tasks = Array.isArray(i["tasks"]) ? i["tasks"].length : 0;
2169
- const title = typeof i["title"] === "string" ? i["title"] : "";
2170
- const n = tr("{count} attività", "{count} tasks", { count: tasks });
2171
- return truncate(title ? `${title} · ${n}${castle}` : `${n}${castle}`, 60);
2172
- }
2173
- case "vascend_addtasks": {
2174
- const tasks = Array.isArray(i["tasks"]) ? i["tasks"].length : 0;
2175
- return truncate("+" + tr("{count} attività", "{count} tasks", { count: tasks }) + castle, 56);
2176
- }
2177
- case "vascend_note": {
2178
- const task = typeof i["task"] === "string" && i["task"] ? `${i["task"]}: ` : "";
2179
- const text = String(i["text"] ?? i["note"] ?? "");
2180
- return truncate(`${task}${text}`, 56);
2181
- }
2182
- case "vascend_skill": {
2183
- const task = typeof i["task"] === "string" && i["task"] ? `${i["task"]}: ` : "";
2184
- const skills = Array.isArray(i["skills"]) ? i["skills"].join(", ") : String(i["skills"] ?? i["skill"] ?? "");
2185
- return truncate(`${task}${skills}`, 56);
2186
- }
2187
- default:
2188
- return null;
2189
- }
2190
- }
2191
- function compactArgs(input) {
2192
- const s = JSON.stringify(input);
2193
- return s.length > 80 ? s.slice(0, 77) + "..." : s;
2194
- }
2195
2247
  function firstLine(s) {
2196
2248
  const line = s.split("\n")[0] ?? "";
2197
2249
  return line.length > 100 ? line.slice(0, 97) + "..." : line;
@@ -2287,6 +2339,10 @@ function controlCollapsible(ctl, channel, action) {
2287
2339
  // Ritorna: "quit" per uscire, input da inviare al modello, o null se il
2288
2340
  // comando e' stato gestito internamente.
2289
2341
  async function handleSlash(line, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi) {
2342
+ if (ctl.busyCommands && !liveCommandAllowed(line)) {
2343
+ notifyStatus(ctl, tr("{command}: disponibile a fine turno. Non inviato al modello; attendi o premi Esc per interrompere.", "{command}: available after the turn. Not sent to the model; wait or press Esc to interrupt.", { command: line.split(/\s+/)[0] }));
2344
+ return null;
2345
+ }
2290
2346
  return withCommandFeedback(ctl, commandCtl => handleSlashCommand(line, deps, commandCtl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi), lines => showReadonlyOverlay({ title: line.split(/\s+/)[0], lines, ctl, theme, terminalColumns }));
2291
2347
  }
2292
2348
  async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi) {
@@ -2294,6 +2350,12 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2294
2350
  const args = rest.join(" ");
2295
2351
  const { harness } = deps;
2296
2352
  switch (cmd) {
2353
+ case "skills":
2354
+ case "mcp": {
2355
+ const inventory = cmd === "skills" ? skillList(harness.skills, harness.skillCommandsEnabled) : mcpList(harness.tools);
2356
+ await showReadonlyOverlay({ ...inventory, ctl, theme, terminalColumns });
2357
+ return null;
2358
+ }
2297
2359
  case "review":
2298
2360
  case "revisione": {
2299
2361
  if (ctl.showReview)
@@ -2318,8 +2380,45 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2318
2380
  return null;
2319
2381
  }
2320
2382
  case "rename": {
2321
- const title = args.trim();
2322
- if (!title || title.length > 120) {
2383
+ let title = args.trim();
2384
+ if (!title) {
2385
+ const choice = await selectOption({
2386
+ title: tr("Rinomina chat", "Rename chat"),
2387
+ question: true,
2388
+ options: [
2389
+ { key: "generate", label: tr("Rigenera dall'ultimo prompt", "Regenerate from the latest prompt"),
2390
+ detail: tr("Usa il modello selezionato per scegliere il titolo.", "Use the selected model to choose the title."), value: "generate" },
2391
+ { key: "manual", label: tr("Imposta manualmente", "Set manually"),
2392
+ detail: tr("Scrivi il nuovo nome della chat.", "Type the new chat name."), value: "manual" },
2393
+ ],
2394
+ ctl, theme, terminalColumns,
2395
+ });
2396
+ if (!choice || signal.aborted)
2397
+ return null;
2398
+ if (choice.value === "manual") {
2399
+ const entered = await ctl.promptInput(tr("Nome della chat (Invio conferma · Esc annulla)", "Chat name (Enter confirms · Esc cancels)"));
2400
+ if (!entered?.trim() || signal.aborted)
2401
+ return null;
2402
+ title = entered.trim();
2403
+ }
2404
+ }
2405
+ if (!title) {
2406
+ const last = [...harness.session.history()].reverse().find(message => message.role === "user");
2407
+ const prompt = last?.content.filter(part => part.type === "text").map(part => part.text).join(" ") || "";
2408
+ if (!prompt) {
2409
+ notifyStatus(ctl, "Invia prima un messaggio per generare il titolo.");
2410
+ return null;
2411
+ }
2412
+ await nameSession(harness.deps(async () => false), prompt, event => {
2413
+ if (event.type === "usage")
2414
+ stats.observe?.(event);
2415
+ if (event.type === "session_naming")
2416
+ ctl.setSessionNaming?.(event.phase, event.title);
2417
+ }, signal, true);
2418
+ ctl.refresh?.();
2419
+ return null;
2420
+ }
2421
+ if (title.length > 120) {
2323
2422
  ctl.write(theme.dim("\n Usa /rename Titolo della chat (da 1 a 120 caratteri).\n"));
2324
2423
  return null;
2325
2424
  }
@@ -2403,15 +2502,29 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2403
2502
  // Esegue/riapre il comando scelto come se digitato.
2404
2503
  return handleSlash(`/${selected.value}`, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi);
2405
2504
  }
2505
+ case "remote":
2506
+ case "remoto": {
2507
+ await comandoRemoto(args, ctl, theme, harness.env, harness.config.cwd, (testo) => {
2508
+ if (ctl.setRoomStatus)
2509
+ ctl.setRoomStatus(testo, "done");
2510
+ });
2511
+ return null;
2512
+ }
2406
2513
  case "logout": {
2407
2514
  // Esce dall'account Vascend: revoca best-effort sul backend, poi rimuove
2408
2515
  // il token locale. Al prossimo avvio il cancello chiedera' l'accesso.
2409
- const token = tokenVascend(harness.env);
2410
- if (!token) {
2516
+ // Il controllo remoto si chiude prima: usa la stessa sessione.
2517
+ await fermaRemoto();
2518
+ if (!leggiSessione(harness.env)) {
2519
+ rimuoviTokenVascend(harness.env);
2411
2520
  ctl.write(theme.dim("\n nessun accesso Vascend attivo\n"));
2412
2521
  return null;
2413
2522
  }
2414
- await revocaSessione(harness.env, token);
2523
+ const { token } = await tokenAgente(harness.env);
2524
+ if (token)
2525
+ await revocaSessione(harness.env, token);
2526
+ await revocaSessioneId(harness.env);
2527
+ rimuoviSessione(harness.env);
2415
2528
  rimuoviTokenVascend(harness.env);
2416
2529
  // Via anche la prova offline: un logout che la lasciasse in giro
2417
2530
  // regalerebbe una finestra offline a chi e' appena uscito.
@@ -2455,7 +2568,10 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2455
2568
  await pickModel(harness, ctl, theme, terminalColumns);
2456
2569
  return null;
2457
2570
  }
2458
- await applyModelChoice(args, harness, ctl, theme, terminalColumns);
2571
+ if (args.trim().toLowerCase() === "fusion")
2572
+ await pickFusion(harness, ctl, theme, terminalColumns);
2573
+ else
2574
+ await applyModelChoice(args, harness, ctl, theme, terminalColumns);
2459
2575
  return null;
2460
2576
  }
2461
2577
  case "models": {
@@ -2466,22 +2582,22 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2466
2582
  case "themes": {
2467
2583
  if (args) {
2468
2584
  if (themeCtl.apply(args)) {
2469
- ctl.write(makeTheme(deps.harness.config.ui, themeCtl.spec()).green(`\n tema impostato: ${args}\n`));
2585
+ ctl.write(makeTheme(deps.harness.config.ui, themeCtl.spec()).green(("\n " + tr("tema impostato: {p0}", "Theme set: {p0}", { p0: args }) + "\n")));
2470
2586
  }
2471
2587
  else {
2472
- ctl.write(theme.yellow(`\n tema sconosciuto: ${args}`) +
2473
- theme.dim(` disponibili: ${themeCtl.available.map((t) => t.name).join(", ")}\n`));
2588
+ ctl.write(theme.yellow(("\n " + tr("tema sconosciuto: {p0}", "Unknown theme: {p0}", { p0: args }))) +
2589
+ theme.dim((" " + tr("disponibili: {p0}", "available: {p0}", { p0: themeCtl.available.map((t) => t.name).join(", ") }) + "\n")));
2474
2590
  }
2475
2591
  return null;
2476
2592
  }
2477
2593
  const options = themeCtl.available.map((t) => ({
2478
2594
  key: t.name,
2479
2595
  label: t.name,
2480
- detail: t.mode === "light" ? "chiaro" : "scuro",
2596
+ detail: t.mode === "light" ? tr("chiaro", "light") : tr("scuro", "dark"),
2481
2597
  value: t.name,
2482
2598
  }));
2483
2599
  const selected = await selectOption({
2484
- title: "Temi",
2600
+ title: tr("Temi", "Themes"),
2485
2601
  options,
2486
2602
  currentKey: themeCtl.activeName(),
2487
2603
  ctl,
@@ -2489,7 +2605,7 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2489
2605
  terminalColumns,
2490
2606
  });
2491
2607
  if (selected && themeCtl.apply(selected.value)) {
2492
- ctl.write(makeTheme(deps.harness.config.ui, themeCtl.spec()).green(`\n tema impostato: ${selected.value}\n`));
2608
+ ctl.write(makeTheme(deps.harness.config.ui, themeCtl.spec()).green(("\n " + tr("tema impostato: {p0}", "Theme set: {p0}", { p0: selected.value }) + "\n")));
2493
2609
  }
2494
2610
  return null;
2495
2611
  }
@@ -2503,39 +2619,39 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2503
2619
  const snapshot = stats.cost.snapshot(harness.config.model);
2504
2620
  const last = stats.last;
2505
2621
  const context = (stats.context ?? new ContextMeter()).read(harness);
2506
- const lines = [theme.bold("\n Consumi della sessione") + theme.dim(` · ${harness.session.name || harness.session.id}`)];
2507
- lines.push(theme.dim(` ${SESSION_USAGE_EXPLANATION}`));
2622
+ const lines = [theme.bold(("\n " + tr("Consumi della sessione", "Session usage"))) + theme.dim(` · ${harness.session.name || harness.session.id}`)];
2623
+ lines.push(theme.dim(` ${sessionUsageExplanation()}`));
2508
2624
  if (stats.incomplete)
2509
- lines.push(theme.yellow(" Storico parziale: le chiamate precedenti alla registrazione non sono ricostruibili."));
2625
+ lines.push(theme.yellow((" " + tr("Storico parziale: le chiamate precedenti alla registrazione non sono ricostruibili.", "Partial history: calls made before recording cannot be reconstructed."))));
2510
2626
  if (stats.missing)
2511
- lines.push(theme.yellow(` ${stats.missing} chiamate senza conteggio token restituito dal provider.`));
2627
+ lines.push(theme.yellow((" " + tr("{p0} chiamate senza conteggio token restituito dal provider.", "{p0} calls without token counts reported by the provider.", { p0: stats.missing }))));
2512
2628
  if (stats.unsaved)
2513
- lines.push(theme.yellow(" Alcuni consumi non sono stati salvati su disco."));
2629
+ lines.push(theme.yellow((" " + tr("Alcuni consumi non sono stati salvati su disco.", "Some usage records were not saved to disk."))));
2514
2630
  if (snapshot.totalTurns === 0) {
2515
- lines.push(theme.dim(" Nessun conteggio token ricevuto dal provider per questa sessione."));
2631
+ lines.push(theme.dim((" " + tr("Nessun conteggio token ricevuto dal provider per questa sessione.", "No token counts received from the provider for this session."))));
2516
2632
  }
2517
2633
  else {
2518
- lines.push(theme.silver(` chiamate modello: ${formatTokenCount(snapshot.totalTurns)}`));
2519
- lines.push(theme.silver(` input totale: ${formatTokenCount(snapshot.totalPromptTokens)}`));
2520
- lines.push(theme.dim(` nuovo: ${formatTokenCount(snapshot.totalInputTokens)}`));
2521
- lines.push(theme.dim(` cache letta: ${formatTokenCount(snapshot.totalCacheReadTokens)}`));
2522
- lines.push(theme.dim(` cache scritta: ${formatTokenCount(snapshot.totalCacheWriteTokens)}`));
2634
+ lines.push(theme.silver((" " + tr("chiamate modello: {p0}", "model calls: {p0}", { p0: formatTokenCount(snapshot.totalTurns) }))));
2635
+ lines.push(theme.silver((" " + tr("input totale: {p0}", "total input: {p0}", { p0: formatTokenCount(snapshot.totalPromptTokens) }))));
2636
+ lines.push(theme.dim((" " + tr("nuovo: {p0}", "new: {p0}", { p0: formatTokenCount(snapshot.totalInputTokens) }))));
2637
+ lines.push(theme.dim((" " + tr("cache letta: {p0}", "cache read: {p0}", { p0: formatTokenCount(snapshot.totalCacheReadTokens) }))));
2638
+ lines.push(theme.dim((" " + tr("cache scritta: {p0}", "cache written: {p0}", { p0: formatTokenCount(snapshot.totalCacheWriteTokens) }))));
2523
2639
  lines.push(theme.silver(` output: ${formatTokenCount(snapshot.totalOutputTokens)}`));
2524
- lines.push(theme.silver(` totale provider: ${formatTokenCount(snapshot.totalProviderTokens)}`));
2640
+ lines.push(theme.silver((" " + tr("totale provider: {p0}", "provider total: {p0}", { p0: formatTokenCount(snapshot.totalProviderTokens) }))));
2525
2641
  }
2526
2642
  if (last) {
2527
- lines.push(theme.bold("\n ultima chiamata"));
2528
- lines.push(theme.dim(` input ${formatTokenCount(promptTokens(last))} = nuovo ${formatTokenCount(last.inputTokens)} + cache letta ${formatTokenCount(last.cacheReadTokens ?? 0)} + cache scritta ${formatTokenCount(last.cacheWriteTokens ?? 0)}`));
2643
+ lines.push(theme.bold(("\n " + tr("ultima chiamata", "last call"))));
2644
+ lines.push(theme.dim((" " + tr("input {p0} = nuovo {p1} + cache letta {p2} + cache scritta {p3}", "input {p0} = new {p1} + cache read {p2} + cache written {p3}", { p0: formatTokenCount(promptTokens(last)), p1: formatTokenCount(last.inputTokens), p2: formatTokenCount(last.cacheReadTokens ?? 0), p3: formatTokenCount(last.cacheWriteTokens ?? 0) }))));
2529
2645
  lines.push(theme.dim(` output ${formatTokenCount(last.outputTokens)}`));
2530
2646
  }
2531
2647
  if (snapshot.perModel.length) {
2532
- lines.push(theme.bold("\n Per modello (collaboratori inclusi quando collegati alla chat)"));
2648
+ lines.push(theme.bold(("\n " + tr("Per modello (collaboratori inclusi quando collegati alla chat)", "Per model (including collaborators linked to the chat)"))));
2533
2649
  for (const row of snapshot.perModel)
2534
- lines.push(theme.dim(` ${row.modelId}: ${formatTokenCount(promptTokens(row) + row.outputTokens)} token · ${row.turns} chiamate`));
2650
+ lines.push(theme.dim((" " + tr("{p0}: {p1} token · {p2} chiamate", "{p0}: {p1} tokens · {p2} calls", { p0: row.modelId, p1: formatTokenCount(promptTokens(row) + row.outputTokens), p2: row.turns }))));
2535
2651
  }
2536
2652
  const providerId = harness.providers.resolve(harness.config.model)?.provider.id ?? "";
2537
2653
  lines.push(...subscriptionDetails(providerId, stats.limits?.get(providerId), theme));
2538
- lines.push(theme.bold("\n contesto vivo stimato") + theme.dim(" · non e' consumo API"));
2654
+ lines.push(theme.bold(("\n " + tr("contesto vivo stimato", "estimated active context"))) + theme.dim((" " + tr("· non e' consumo API", "· not API usage"))));
2539
2655
  lines.push(...contextDetails(context).map(line => theme.dim(` ${line}`)));
2540
2656
  ctl.write(lines.join("\n") + "\n");
2541
2657
  return null;
@@ -2585,10 +2701,10 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2585
2701
  const csub = args.trim();
2586
2702
  if (csub === "close" || csub === "open" || csub === "toggle") {
2587
2703
  if (!controlCollapsible(ctl, "config", csub) && csub === "close") {
2588
- ctl.write(theme.dim("\n nessuna config da chiudere: usa /config\n\n"));
2704
+ ctl.write(theme.dim(("\n " + tr("nessuna config da chiudere: usa /config", "No configuration view to close: use /config") + "\n\n")));
2589
2705
  }
2590
2706
  else if (csub === "close") {
2591
- ctl.write(theme.dim("\n config chiusa (output rimosso dalla chat)\n\n"));
2707
+ ctl.write(theme.dim(("\n " + tr("config chiusa (output rimosso dalla chat)", "Configuration closed (output removed from chat)") + "\n\n")));
2592
2708
  }
2593
2709
  return null;
2594
2710
  }
@@ -2596,21 +2712,21 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2596
2712
  const cfg = harness.config;
2597
2713
  const resolved = harness.providers.resolve(cfg.model);
2598
2714
  const provider = resolved
2599
- ? (resolved.provider.label ?? resolved.provider.id)
2600
- : "non configurato";
2715
+ ? providerLabel(resolved.provider)
2716
+ : tr("non configurato", "not configured");
2601
2717
  const settings = [
2602
- { key: "model", label: "Modello", detail: cfg.model, value: "model" },
2603
- { key: "provider", label: "Provider e accessi", detail: provider, value: "provider" },
2604
- { key: "vibe", label: "Profilo operativo", detail: cfg.vibeMode, value: "vibe" },
2605
- { key: "theme", label: "Tema", detail: themeCtl.activeName(), value: "theme" },
2718
+ { key: "model", label: tr("Modello", "Model"), detail: cfg.model, value: "model" },
2719
+ { key: "provider", label: tr("Provider e accessi", "Providers and sign-ins"), detail: provider, value: "provider" },
2720
+ { key: "vibe", label: tr("Profilo operativo", "Work profile"), detail: cfg.vibeMode, value: "vibe" },
2721
+ { key: "theme", label: tr("Tema", "Theme"), detail: themeCtl.activeName(), value: "theme" },
2606
2722
  { key: "cwd", label: "Workspace", detail: cfg.cwd, value: "cwd" },
2607
- { key: "sessions", label: "Sessioni", detail: harness.session.id, value: "sessions" },
2608
- { key: "extensions", label: "Estensioni", detail: `${harness.host.inventory().filter((item) => item.loaded).length} caricate`, value: "extensions" },
2609
- { key: "usage", label: "Utilizzo e contesto", detail: "token provider, cache e finestra viva", value: "usage" },
2723
+ { key: "sessions", label: tr("Sessioni", "Sessions"), detail: harness.session.id, value: "sessions" },
2724
+ { key: "extensions", label: tr("Estensioni", "Extensions"), detail: tr("{p0} caricate", "{p0} loaded", { p0: harness.host.inventory().filter((item) => item.loaded).length }), value: "extensions" },
2725
+ { key: "usage", label: tr("Utilizzo e contesto", "Usage and context"), detail: tr("token provider, cache e finestra viva", "provider tokens, cache and active context"), value: "usage" },
2610
2726
  ];
2611
2727
  const selected = await selectOption({
2612
- title: "Impostazioni",
2613
- hint: "/config details mostra i file di configurazione",
2728
+ title: tr("Impostazioni", "Settings"),
2729
+ hint: tr("/config details mostra i file di configurazione", "/config details shows configuration files"),
2614
2730
  options: settings,
2615
2731
  fullHeight: true,
2616
2732
  wide: true,
@@ -2625,22 +2741,22 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2625
2741
  const cfg = harness.config;
2626
2742
  const files = cfg.loadedConfigFiles.length
2627
2743
  ? cfg.loadedConfigFiles.map((file) => ` ${file}`).join("\n")
2628
- : " nessun file caricato";
2629
- const body = theme.dim(` modello: ${cfg.model}\n`) +
2630
- theme.dim(` maxTokens: ${cfg.maxTokens > 0 ? cfg.maxTokens : "auto (max del modello)"}\n`) +
2744
+ : (" " + tr("nessun file caricato", "no files loaded"));
2745
+ const body = theme.dim((" " + tr("modello: {p0}", "model: {p0}", { p0: cfg.model }) + "\n")) +
2746
+ theme.dim(` maxTokens: ${cfg.maxTokens > 0 ? cfg.maxTokens : tr("auto (max del modello)", "auto (model maximum)")}\n`) +
2631
2747
  theme.dim(` vibeMode: ${cfg.vibeMode}\n`) +
2632
2748
  theme.dim(` sessionDir: ${cfg.sessionDir}\n`) +
2633
2749
  theme.dim(` contextFiles: ${cfg.contextFiles.join(", ")}\n`) +
2634
- theme.dim(` skill: ${harness.skills.length}${harness.skillCommandsEnabled ? "" : " (comandi off)"}${harness.skills.length ? ` (${harness.skills.map((s) => s.name).join(", ")})` : ""}\n`) +
2635
- theme.dim(` progetto fidato: ${harness.projectTrusted ? "si" : "no (risorse locali non caricate)"}\n`) +
2636
- theme.bold("\n tema\n") +
2637
- theme.dim(` attivo: ${themeCtl.activeName()}${themeCtl.spec() ? ` (${themeCtl.spec()?.mode === "light" ? "chiaro" : "scuro"})` : ""}\n`) +
2638
- theme.dim(` disponibili: ${themeCtl.available.map((t) => t.name).join(", ")}\n`) +
2750
+ theme.dim(` skill: ${harness.skills.length}${harness.skillCommandsEnabled ? "" : (" " + tr("(comandi off)", "(commands off)"))}${harness.skills.length ? ` (${harness.skills.map((s) => s.name).join(", ")})` : ""}\n`) +
2751
+ theme.dim((" " + tr("progetto fidato: {p0}", "trusted project: {p0}", { p0: harness.projectTrusted ? tr("si", "yes") : tr("no (risorse locali non caricate)", "no (local resources not loaded)") }) + "\n")) +
2752
+ theme.bold(("\n " + tr("tema", "theme") + "\n")) +
2753
+ theme.dim((" " + tr("attivo: {p0}{p1}", "active: {p0}{p1}", { p0: themeCtl.activeName(), p1: themeCtl.spec() ? ` (${themeCtl.spec()?.mode === "light" ? tr("chiaro", "light") : tr("scuro", "dark")})` : "" }) + "\n")) +
2754
+ theme.dim((" " + tr("disponibili: {p0}", "available: {p0}", { p0: themeCtl.available.map((t) => t.name).join(", ") }) + "\n")) +
2639
2755
  theme.dim(` banner: ${cfg.ui.banner}\n`) +
2640
2756
  theme.dim(` prompt: ${JSON.stringify(cfg.ui.prompt)}\n`) +
2641
2757
  theme.bold("\n file\n") +
2642
2758
  theme.dim(`${files}`);
2643
- showCollapsible(ctl, "config", theme.bold("\n configurazione") + theme.dim(" · /config close per nascondere"), body);
2759
+ showCollapsible(ctl, "config", theme.bold(("\n " + tr("configurazione", "configuration"))) + theme.dim((" " + tr("· /config close per nascondere", "· /config close to hide"))), body);
2644
2760
  return null;
2645
2761
  }
2646
2762
  case "vibe":
@@ -3001,50 +3117,53 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
3001
3117
  ctl.write(theme.green(`\n ${result.message}\n`));
3002
3118
  return null;
3003
3119
  }
3004
- // Compact UNIFICATO: una sola operazione che (1) compatta il contesto vivo
3005
- // con riassunto LLM (stile Pi) e (2) salva il checkpoint Vascend/Danilov in
3006
- // .vascend-compact.md per la ripresa a freddo. /vascend-compact e' un alias.
3007
- case "vascend-compact":
3120
+ // Compatta direttamente la cronologia JSON della sessione. Piano e stato
3121
+ // operativo hanno gia i rispettivi store JSON: nessun checkpoint parallelo.
3008
3122
  case "compact": {
3009
3123
  const hasModel = Boolean(harness.providers.resolve(harness.config.model));
3124
+ let feedback = "Nessun modello configurato; contesto conservato.";
3125
+ let failed = false;
3010
3126
  // 1) Compattazione LLM del contesto della sessione.
3011
3127
  if (hasModel) {
3012
3128
  const meter = stats.context ?? new ContextMeter();
3013
3129
  const before = meter.read(harness).tokens;
3014
- ctl.write(theme.dim(`\n compatto ~${formatTokenCount(before)} token attivi; preservo richiesta, attivita aperte e lavoro recente...\n`));
3015
3130
  const controller = new AbortController();
3016
3131
  const onSigint = () => controller.abort();
3132
+ if (signal.aborted)
3133
+ controller.abort();
3134
+ signal.addEventListener("abort", onSigint, { once: true });
3017
3135
  process.once("SIGINT", onSigint);
3018
3136
  try {
3019
3137
  const done = await compactNow(harness.deps(async () => false), controller.signal, (e) => {
3020
3138
  if (e.type === "usage")
3021
3139
  stats.observe?.(e);
3140
+ if (e.type === "compacting")
3141
+ ctl.setCompacting?.(true);
3142
+ if (e.type === "compaction_end")
3143
+ ctl.setCompacting?.(false);
3022
3144
  }, args || undefined);
3023
3145
  if (done) {
3024
3146
  const after = meter.read(harness);
3025
- const archived = after.archived.reduce((sum, block) => sum + block.tokens, 0);
3026
- ctl.write(theme.green(` attivo ~${formatTokenCount(before)} → ~${formatTokenCount(after.tokens)} token · archivio originale ~${formatTokenCount(archived)}\n`));
3147
+ feedback = tr("Contesto compattato: ~{before} -> ~{after} token", "Context compacted: ~{before} -> ~{after} tokens", { before: formatTokenCount(before), after: formatTokenCount(after.tokens) });
3148
+ }
3149
+ else {
3150
+ feedback = controller.signal.aborted
3151
+ ? tr("Compattazione interrotta; contesto conservato.", "Compaction interrupted; context preserved.")
3152
+ : tr("Nessuna compattazione necessaria; contesto conservato.", "No compaction needed; context preserved.");
3027
3153
  }
3028
- if (!done)
3029
- ctl.write(theme.dim(controller.signal.aborted
3030
- ? " compattazione interrotta; contesto conservato.\n"
3031
- : " nessuna compattazione applicata; contesto conservato.\n"));
3032
3154
  }
3033
3155
  catch (err) {
3034
- ctl.write(theme.red(` errore compattazione: ${err.message}\n`));
3156
+ feedback = tr("Compattazione non riuscita: {detail}", "Compaction failed: {detail}", { detail: runtimeError(err.message) });
3157
+ failed = true;
3035
3158
  }
3036
3159
  finally {
3037
3160
  process.removeListener("SIGINT", onSigint);
3161
+ signal.removeEventListener("abort", onSigint);
3162
+ ctl.setCompacting?.(false);
3163
+ ctl.setStatus(null);
3038
3164
  }
3039
3165
  }
3040
- else {
3041
- ctl.write(theme.dim("\n nessun modello configurato: salvo solo il checkpoint Vascend.\n"));
3042
- }
3043
- // 2) Checkpoint Vascend/Danilov sullo stato (gia' compattato) della sessione.
3044
- vascend.loadPlan(harness.config.sessionDir, harness.config.planStoreId ?? harness.config.roomStoreId ?? harness.session.id);
3045
- const checkpoint = vascend.compact(harness.session.history());
3046
- ctl.write(theme.green(" ✓ checkpoint Vascend salvato in .vascend-compact.md\n"));
3047
- ctl.write(theme.dim(` piano e contesto pronti per la ripresa (${checkpoint.text.split("\n").length} righe)\n`));
3166
+ notifyStatus(ctl, feedback, failed);
3048
3167
  return null;
3049
3168
  }
3050
3169
  case "tree": {
@@ -3200,9 +3319,11 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
3200
3319
  return null;
3201
3320
  }
3202
3321
  const selected = await selectOption({
3203
- title: tr("Chat salvate", "Saved chats"),
3322
+ title: "Resume a previous session",
3204
3323
  hint: tr("Scrivi per cercare · Invio apre · Esc torna alla chat corrente", "Type to search · Enter open · Esc return to the current chat"),
3205
3324
  filterable: true,
3325
+ sessionList: true,
3326
+ fullHeight: true,
3206
3327
  options: sessionOptions(list).map((option) => ({ ...option,
3207
3328
  detail: option.detail + (option.value === harness.session.id ? " · " + tr("corrente", "current") : ""),
3208
3329
  })),
@@ -3400,7 +3521,7 @@ async function loginMenu(harness, ctl, theme, terminalColumns) {
3400
3521
  const rows = loginRows(harness.env, harness.providers);
3401
3522
  const picked = await selectOption({
3402
3523
  title: tr("accesso ai modelli", "model sign-in"),
3403
- hint: "Enter gestisci · digita per cercare · Esc chiudi",
3524
+ hint: tr("Enter gestisci · digita per cercare · Esc chiudi", "Enter manage · type to search · Esc close"),
3404
3525
  options: rows.map((row) => ({
3405
3526
  key: row.id,
3406
3527
  label: row.label,
@@ -3428,7 +3549,7 @@ async function loginMenu(harness, ctl, theme, terminalColumns) {
3428
3549
  }
3429
3550
  const action = await selectOption({
3430
3551
  title: `${row.label} · ${loginStatusLabel(row)}`,
3431
- hint: row.command ? `comando: ${row.command.join(" ")}` : "",
3552
+ hint: row.command ? tr("comando: {p0}", "command: {p0}", { p0: row.command.join(" ") }) : "",
3432
3553
  options: [
3433
3554
  {
3434
3555
  key: "login",
@@ -3462,10 +3583,10 @@ async function loginMenu(harness, ctl, theme, terminalColumns) {
3462
3583
  harness.reloadProviders();
3463
3584
  const refreshed = loginRows(harness.env, harness.providers).find((item) => item.id === row.id);
3464
3585
  if (completed && refreshed?.configured && !refreshed.needsRefresh) {
3465
- ctl.write(theme.green(`\n ${row.label}: accesso rilevato e modelli aggiornati.\n`));
3586
+ ctl.write(theme.green(("\n " + tr("{p0}: accesso rilevato e modelli aggiornati.", "{p0}: sign-in detected and models updated.", { p0: row.label }) + "\n")));
3466
3587
  }
3467
3588
  else if (completed) {
3468
- ctl.write(theme.yellow(`\n ${row.label}: il comando e' terminato, ma non trovo ancora una sessione valida.\n`));
3589
+ ctl.write(theme.yellow(("\n " + tr("{p0}: il comando e' terminato, ma non trovo ancora una sessione valida.", "{p0}: the command finished, but no valid session was found.", { p0: row.label }) + "\n")));
3469
3590
  }
3470
3591
  }
3471
3592
  }
@@ -3474,11 +3595,11 @@ async function runInteractiveLogin(command, harness, ctl, theme) {
3474
3595
  if (!executable)
3475
3596
  return false;
3476
3597
  if (!stdin.isTTY || !stdout.isTTY) {
3477
- ctl.write(theme.dim(`\n esegui in un terminale: ${command.join(" ")}\n`));
3598
+ ctl.write(theme.dim(("\n " + tr("esegui in un terminale: {p0}", "Run in a terminal: {p0}", { p0: command.join(" ") }) + "\n")));
3478
3599
  return false;
3479
3600
  }
3480
- ctl.write(theme.bold(`\n avvio ${command.join(" ")}\n`));
3481
- ctl.write(theme.dim(" completa l'accesso; al termine tornerai automaticamente a Talos.\n\n"));
3601
+ ctl.write(theme.bold(("\n " + tr("avvio {p0}", "Starting {p0}", { p0: command.join(" ") }) + "\n")));
3602
+ ctl.write(theme.dim((" " + tr("completa l'accesso; al termine tornerai automaticamente a Talos.", "Complete sign-in; you will return to Talos automatically.") + "\n\n")));
3482
3603
  ctl.pause();
3483
3604
  ctl.setOverlay(null);
3484
3605
  ctl.setStatus(null);
@@ -3497,7 +3618,7 @@ async function runInteractiveLogin(command, harness, ctl, theme) {
3497
3618
  child.once("exit", (value) => resolve(value ?? -1));
3498
3619
  });
3499
3620
  if (code !== 0) {
3500
- ctl.write(theme.yellow(`\n login non completato (uscita ${code}). Verifica che \`${executable}\` sia installato.\n`));
3621
+ ctl.write(theme.yellow(("\n " + tr("login non completato (uscita {p0}). Verifica che `{p1}` sia installato.", "Sign-in incomplete (exit {p0}). Check that `{p1}` is installed.", { p0: code, p1: executable }) + "\n")));
3501
3622
  return false;
3502
3623
  }
3503
3624
  return true;
@@ -3521,13 +3642,13 @@ async function providerMenu(harness, ctl, theme, terminalColumns) {
3521
3642
  const options = rows.map((row) => ({
3522
3643
  key: row.id,
3523
3644
  label: `${row.label}${row.active ? " ←" : ""}`,
3524
- detail: `${rowStatusLabel(row)}${row.modelCount ? ` · ${row.modelCount} modelli` : ""}`,
3645
+ detail: `${rowStatusLabel(row)}${row.modelCount ? (" " + tr("· {p0} modelli", "· {p0} models", { p0: row.modelCount })) : ""}`,
3525
3646
  value: row,
3526
3647
  }));
3527
3648
  const configured = rows.filter((r) => r.configured).length;
3528
3649
  const picked = await selectOption({
3529
- title: `provider — ${configured} configurati su ${rows.length}`,
3530
- hint: "↑/↓ scegli · Enter apri · digita per cercare · Esc chiudi",
3650
+ title: tr("provider — {p0} configurati su {p1}", "providers — {p0} of {p1} configured", { p0: configured, p1: rows.length }),
3651
+ hint: tr("↑/↓ scegli · Enter apri · digita per cercare · Esc chiudi", "↑/↓ select · Enter open · type to search · Esc close"),
3531
3652
  options,
3532
3653
  ctl,
3533
3654
  theme,
@@ -3563,7 +3684,7 @@ async function providerActions(row, harness, ctl, theme, terminalColumns) {
3563
3684
  case "set-key": {
3564
3685
  const ok = await setKeyFlow(fresh, harness, ctl, theme);
3565
3686
  if (ok)
3566
- ctl.write(theme.green(`\n ${fresh.label}: chiave salvata e provider attivato.\n`));
3687
+ ctl.write(theme.green(("\n " + tr("{p0}: chiave salvata e provider attivato.", "{p0}: key saved and provider activated.", { p0: fresh.label }) + "\n")));
3567
3688
  break;
3568
3689
  }
3569
3690
  case "set-endpoint": {
@@ -3582,11 +3703,11 @@ async function providerActions(row, harness, ctl, theme, terminalColumns) {
3582
3703
  }
3583
3704
  case "remove": {
3584
3705
  const confirmed = await selectOption({
3585
- title: `rimuovere la credenziale di ${fresh.label}?`,
3586
- hint: "la chiave salvata verra' eliminata dal config",
3706
+ title: tr("rimuovere la credenziale di {p0}?", "Remove the credential for {p0}?", { p0: fresh.label }),
3707
+ hint: tr("la chiave salvata verra' eliminata dal config", "The saved key will be deleted from configuration"),
3587
3708
  options: [
3588
- { key: "no", label: "No, mantieni", detail: "", value: false },
3589
- { key: "yes", label: "Sì, rimuovi", detail: "", value: true },
3709
+ { key: "no", label: tr("No, mantieni", "No, keep it"), detail: "", value: false },
3710
+ { key: "yes", label: tr("Sì, rimuovi", "Yes, remove it"), detail: "", value: true },
3590
3711
  ],
3591
3712
  currentKey: "no",
3592
3713
  ctl,
@@ -3597,10 +3718,10 @@ async function providerActions(row, harness, ctl, theme, terminalColumns) {
3597
3718
  const res = removeProvider(harness.env, fresh.id);
3598
3719
  if (res.ok) {
3599
3720
  harness.reloadProviders();
3600
- ctl.write(theme.green(`\n ${fresh.label}: credenziale rimossa.\n`));
3721
+ ctl.write(theme.green(("\n " + tr("{p0}: credenziale rimossa.", "{p0}: credential removed.", { p0: fresh.label }) + "\n")));
3601
3722
  }
3602
3723
  else {
3603
- ctl.write(theme.yellow(`\n ${res.error ?? "rimozione non riuscita"}\n`));
3724
+ ctl.write(theme.yellow(`\n ${res.error ?? tr("rimozione non riuscita", "removal failed")}\n`));
3604
3725
  }
3605
3726
  return "back";
3606
3727
  }
@@ -3613,28 +3734,28 @@ async function providerActions(row, harness, ctl, theme, terminalColumns) {
3613
3734
  // rete), la salva e ricostruisce il registro. Ritorna true se salvata.
3614
3735
  async function setKeyFlow(row, harness, ctl, theme) {
3615
3736
  ctl.write(theme.dim(`\n ${providerKeyHint(row.spec)}\n`));
3616
- const key = await ctl.promptInput(`API key per ${row.label} (Invio conferma · Esc annulla)`);
3737
+ const key = await ctl.promptInput(tr("API key per {p0} (Invio conferma · Esc annulla)", "API key for {p0} (Enter confirm · Esc cancel)", { p0: row.label }));
3617
3738
  if (!key)
3618
3739
  return false;
3619
3740
  const trimmed = key.trim();
3620
3741
  // Validazione di forma per OpenRouter (sk-or-...). Avviso non bloccante.
3621
3742
  if (row.id === "openrouter" && !looksLikeApiKey(trimmed)) {
3622
- ctl.write(theme.yellow("\n attenzione: la chiave non sembra una OpenRouter key (sk-or-...). Provo comunque.\n"));
3743
+ ctl.write(theme.yellow(("\n " + tr("attenzione: la chiave non sembra una OpenRouter key (sk-or-...). Provo comunque.", "Warning: the key does not look like an OpenRouter key (sk-or-...). Trying anyway.") + "\n")));
3623
3744
  }
3624
3745
  // Validazione di rete (oggi solo OpenRouter espone /key). Best effort.
3625
3746
  if (row.id === "openrouter") {
3626
- ctl.setStatus(theme.dim("verifico la chiave su OpenRouter…"));
3747
+ ctl.setStatus(theme.dim(tr("verifico la chiave su OpenRouter…", "Checking the key on OpenRouter…")));
3627
3748
  try {
3628
3749
  const v = await validateApiKey(trimmed);
3629
3750
  ctl.setStatus(null);
3630
3751
  if (!v.ok) {
3631
- ctl.write(theme.yellow(`\n chiave non valida: ${v.error ?? "verifica fallita"}. Non salvata.\n`));
3752
+ ctl.write(theme.yellow(("\n " + tr("chiave non valida: {p0}. Non salvata.", "Invalid key: {p0}. Not saved.", { p0: v.error ?? tr("verifica fallita", "verification failed") }) + "\n")));
3632
3753
  return false;
3633
3754
  }
3634
3755
  const credit = v.limitRemaining === null || v.limitRemaining === undefined
3635
- ? "credito illimitato"
3636
- : `credito residuo ${v.limitRemaining.toFixed(2)}`;
3637
- ctl.write(theme.green(`\n chiave valida${v.label ? ` (${v.label})` : ""} · ${credit}\n`));
3756
+ ? tr("credito illimitato", "unlimited credit")
3757
+ : tr("credito residuo {p0}", "remaining credit {p0}", { p0: v.limitRemaining.toFixed(2) });
3758
+ ctl.write(theme.green(("\n " + tr("chiave valida{p0} · {p1}", "valid key{p0} · {p1}", { p0: v.label ? ` (${v.label})` : "", p1: credit }) + "\n")));
3638
3759
  }
3639
3760
  catch {
3640
3761
  ctl.setStatus(null);
@@ -3642,7 +3763,7 @@ async function setKeyFlow(row, harness, ctl, theme) {
3642
3763
  }
3643
3764
  const res = saveProviderKey(harness.env, row.id, trimmed);
3644
3765
  if (!res.ok) {
3645
- ctl.write(theme.yellow(`\n ${res.error ?? "salvataggio non riuscito"}\n`));
3766
+ ctl.write(theme.yellow(`\n ${res.error ?? tr("salvataggio non riuscito", "save failed")}\n`));
3646
3767
  return false;
3647
3768
  }
3648
3769
  // La env scoped serve a reloadProviders (che la rilegge dal disco), ma la
@@ -3654,13 +3775,13 @@ async function setKeyFlow(row, harness, ctl, theme) {
3654
3775
  }
3655
3776
  // Flusso "imposta endpoint": chiede una baseUrl (vuota = ripristina default).
3656
3777
  async function setEndpointFlow(row, harness, ctl, theme) {
3657
- ctl.write(theme.dim(`\n endpoint attuale: ${row.baseUrl ?? "(default del provider)"} · vuoto = ripristina default\n`));
3658
- const url = await ctl.promptInput(`baseUrl per ${row.label} (Invio conferma · Esc annulla)`);
3778
+ ctl.write(theme.dim(("\n " + tr("endpoint attuale: {p0} · vuoto = ripristina default", "Current endpoint: {p0} · empty = restore default", { p0: row.baseUrl ?? tr("(default del provider)", "(provider default)") }) + "\n")));
3779
+ const url = await ctl.promptInput(tr("baseUrl per {p0} (Invio conferma · Esc annulla)", "baseUrl for {p0} (Enter confirm · Esc cancel)", { p0: row.label }));
3659
3780
  if (url === null)
3660
3781
  return; // Esc
3661
3782
  const res = saveProviderBaseUrl(harness.env, row.id, url);
3662
3783
  if (!res.ok) {
3663
- ctl.write(theme.yellow(`\n ${res.error ?? "salvataggio non riuscito"}\n`));
3784
+ ctl.write(theme.yellow(`\n ${res.error ?? tr("salvataggio non riuscito", "save failed")}\n`));
3664
3785
  return;
3665
3786
  }
3666
3787
  const norm = row.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
@@ -3669,26 +3790,26 @@ async function setEndpointFlow(row, harness, ctl, theme) {
3669
3790
  else
3670
3791
  delete harness.env[`TALOS_${norm}_BASE_URL`];
3671
3792
  harness.reloadProviders();
3672
- ctl.write(theme.green(`\n ${row.label}: endpoint ${url.trim() ? "aggiornato" : "ripristinato al default"}.\n`));
3793
+ ctl.write(theme.green(("\n " + tr("{p0}: endpoint {p1}.", "{p0}: endpoint {p1}.", { p0: row.label, p1: url.trim() ? tr("aggiornato", "updated") : tr("ripristinato al default", "restored to default") }) + "\n")));
3673
3794
  }
3674
3795
  // Flusso OAuth (oggi: OpenRouter PKCE). Mostra l'URL da aprire nel browser e
3675
3796
  // raccoglie il code dal callback, poi scambia e salva la chiave.
3676
3797
  async function oauthFlow(row, harness, ctl, theme, terminalColumns) {
3677
3798
  if (row.id !== "openrouter") {
3678
- transientNotice(ctl, "OAuth non disponibile per questo provider.", "failed", theme);
3799
+ transientNotice(ctl, tr("OAuth non disponibile per questo provider.", "OAuth is not available for this provider."), "failed", theme);
3679
3800
  return;
3680
3801
  }
3681
3802
  // Il login e' una vista operativa temporanea: non deve diventare parte della
3682
3803
  // conversazione. L'overlay viene aggiornato in place e rimosso al termine.
3683
3804
  let authUrl = "";
3684
- let authState = "avvio il server locale…";
3805
+ let authState = tr("avvio il server locale…", "Starting the local server…");
3685
3806
  const drawOAuth = () => {
3686
3807
  const lines = [authState];
3687
3808
  if (authUrl)
3688
- lines.push("", "Se il browser non si apre, copia questo link:", authUrl);
3689
- lines.push("", "Questa schermata si chiudera' automaticamente.");
3809
+ lines.push("", tr("Se il browser non si apre, copia questo link:", "If the browser does not open, copy this link:"), authUrl);
3810
+ lines.push("", tr("Questa schermata si chiudera' automaticamente.", "This screen will close automatically."));
3690
3811
  ctl.setOverlay([
3691
- theme.bold(" OpenRouter · accesso via browser"),
3812
+ theme.bold((" " + tr("OpenRouter · accesso via browser", "OpenRouter · browser sign-in"))),
3692
3813
  theme.dark(` ${"─".repeat(Math.max(12, terminalColumns() - 4))}`),
3693
3814
  ...lines.flatMap((line) => wrapPlain(line || " ", Math.max(20, terminalColumns() - 4)))
3694
3815
  .map((line) => ` ${theme.silver(line)}`),
@@ -3699,19 +3820,19 @@ async function oauthFlow(row, harness, ctl, theme, terminalColumns) {
3699
3820
  switch (ev.phase) {
3700
3821
  case "url_built":
3701
3822
  authUrl = ev.detail ?? authUrl;
3702
- authState = "apro il browser per autorizzare…";
3823
+ authState = tr("apro il browser per autorizzare…", "Opening the browser for authorization…");
3703
3824
  break;
3704
3825
  case "browser_opened":
3705
- authState = "browser aperto · in attesa dell'autorizzazione…";
3826
+ authState = tr("browser aperto · in attesa dell'autorizzazione…", "Browser opened · waiting for authorization…");
3706
3827
  break;
3707
3828
  case "browser_failed":
3708
- authState = "apri il link manualmente · in attesa dell'autorizzazione…";
3829
+ authState = tr("apri il link manualmente · in attesa dell'autorizzazione…", "Open the link manually · waiting for authorization…");
3709
3830
  break;
3710
3831
  case "awaiting_code":
3711
- authState = "in attesa dell'autorizzazione nel browser…";
3832
+ authState = tr("in attesa dell'autorizzazione nel browser…", "Waiting for authorization in the browser…");
3712
3833
  break;
3713
3834
  case "exchanging":
3714
- authState = "autorizzazione ricevuta · attivo la API key…";
3835
+ authState = tr("autorizzazione ricevuta · attivo la API key…", "Authorization received · activating the API key…");
3715
3836
  break;
3716
3837
  default:
3717
3838
  break;
@@ -3742,20 +3863,20 @@ async function oauthFlow(row, harness, ctl, theme, terminalColumns) {
3742
3863
  saveProviderKey(harness.env, row.id, result.apiKey);
3743
3864
  harness.env["TALOS_OPENROUTER_API_KEY"] = result.apiKey;
3744
3865
  harness.reloadProviders();
3745
- transientNotice(ctl, "OpenRouter collegato e attivato", "done", theme);
3866
+ transientNotice(ctl, tr("OpenRouter collegato e attivato", "OpenRouter connected and activated"), "done", theme);
3746
3867
  return;
3747
3868
  }
3748
3869
  // Fallimento del flusso automatico (browser/timeout/callback/server):
3749
3870
  // ricadiamo sul percorso manuale con un flow nuovo e coerente (PKCE
3750
3871
  // proprio + authUrl mostrato all'utente, che incolla il code).
3751
- const reason = result.error ?? "callback automatico non riuscito";
3872
+ const reason = result.error ?? tr("callback automatico non riuscito", "automatic callback failed");
3752
3873
  const manualFlow = startOAuthFlow(harness.env, callbackUrlForPort());
3753
3874
  const manualLines = [];
3754
3875
  const manual = await runManualCodeFallback({
3755
3876
  print: (line) => { manualLines.push(line); },
3756
3877
  prompt: async (label) => {
3757
3878
  await showReadonlyOverlay({
3758
- title: "OpenRouter · autorizzazione manuale",
3879
+ title: tr("OpenRouter · autorizzazione manuale", "OpenRouter · manual authorization"),
3759
3880
  lines: manualLines,
3760
3881
  ctl,
3761
3882
  theme,
@@ -3763,25 +3884,25 @@ async function oauthFlow(row, harness, ctl, theme, terminalColumns) {
3763
3884
  });
3764
3885
  return ctl.promptInput(label);
3765
3886
  },
3766
- }, manualFlow, { reason });
3887
+ }, manualFlow, { reason, translate: tr });
3767
3888
  if (!manual.ok || !manual.apiKey) {
3768
3889
  if (manual.error === "cancelled") {
3769
- transientNotice(ctl, "OAuth annullato", "done", theme);
3890
+ transientNotice(ctl, tr("OAuth annullato", "OAuth cancelled"), "done", theme);
3770
3891
  }
3771
3892
  else {
3772
- transientNotice(ctl, `OAuth non riuscito: ${manual.error ?? "scambio fallito"}`, "failed", theme);
3893
+ transientNotice(ctl, tr("OAuth non riuscito: {p0}", "OAuth failed: {p0}", { p0: manual.error ?? tr("scambio fallito", "exchange failed") }), "failed", theme);
3773
3894
  }
3774
3895
  return;
3775
3896
  }
3776
3897
  saveProviderKey(harness.env, row.id, manual.apiKey);
3777
3898
  harness.env["TALOS_OPENROUTER_API_KEY"] = manual.apiKey;
3778
3899
  harness.reloadProviders();
3779
- transientNotice(ctl, "OpenRouter collegato e attivato", "done", theme);
3900
+ transientNotice(ctl, tr("OpenRouter collegato e attivato", "OpenRouter connected and activated"), "done", theme);
3780
3901
  }
3781
3902
  catch (err) {
3782
3903
  ctl.setStatus(null);
3783
3904
  ctl.setOverlay(null);
3784
- transientNotice(ctl, `errore OAuth: ${err.message}`, "failed", theme);
3905
+ transientNotice(ctl, tr("errore OAuth: {p0}", "OAuth error: {p0}", { p0: err.message }), "failed", theme);
3785
3906
  }
3786
3907
  }
3787
3908
  function transientNotice(ctl, label, state, theme) {
@@ -3794,18 +3915,22 @@ async function pickModel(harness, ctl, theme, terminalColumns) {
3794
3915
  .filter((row) => !row.configured || row.needsRefresh)
3795
3916
  .map((row) => ({
3796
3917
  key: `access:${row.id}`,
3797
- label: `${row.needsRefresh ? "Rinnova" : "Collega"} ${row.label}`,
3918
+ label: `${row.needsRefresh ? tr("Rinnova", "Renew") : tr("Collega", "Connect")} ${row.label}`,
3798
3919
  detail: `${loginStatusLabel(row)} · ${row.detail}`,
3799
3920
  value: { providerId: `access:${row.id}`, modelId: "", fullModel: `access:${row.id}` },
3800
3921
  }));
3801
- const options = [...accessOptions, ...modelOptions(providers, dynamic)];
3922
+ const options = [{ key: "fusion", label: "Fusion",
3923
+ detail: tr("Jev decide (modello, focus, permessi), l'agente esegue · piani a pagamento", "Jev decides (model, focus, permissions), the agent executes · paid plans"),
3924
+ value: { providerId: "fusion", modelId: "", fullModel: "fusion" } }, ...accessOptions, ...modelOptions(providers, dynamic)];
3802
3925
  if (!options.length) {
3803
- ctl.write(theme.yellow("\n nessun provider configurato. Usa /login per collegare un abbonamento o una API key.\n"));
3926
+ ctl.write(theme.yellow(("\n " + tr("nessun provider configurato. Usa /login per collegare un abbonamento o una API key.", "No provider configured. Use /login to connect a subscription or an API key.") + "\n")));
3804
3927
  return false;
3805
3928
  }
3806
3929
  const current = canonicalModel(harness.config.model);
3807
3930
  const selected = await selectOption({
3808
- title: "Modelli",
3931
+ title: tr("Modelli", "Models"),
3932
+ modelList: true,
3933
+ fullHeight: true,
3809
3934
  options,
3810
3935
  currentKey: current?.fullModel,
3811
3936
  ctl,
@@ -3815,6 +3940,8 @@ async function pickModel(harness, ctl, theme, terminalColumns) {
3815
3940
  });
3816
3941
  if (!selected)
3817
3942
  return false;
3943
+ if (selected.value.providerId === "fusion")
3944
+ return pickFusion(harness, ctl, theme, terminalColumns);
3818
3945
  if (selected.value.providerId.startsWith("access:")) {
3819
3946
  await loginMenu(harness, ctl, theme, terminalColumns);
3820
3947
  return false;
@@ -3827,15 +3954,15 @@ async function pickModel(harness, ctl, theme, terminalColumns) {
3827
3954
  async function pickAuthMethod(choice, harness, ctl, theme, terminalColumns) {
3828
3955
  const options = authOptions(choice, harness.providers.list());
3829
3956
  if (!options.length) {
3830
- ctl.write(theme.yellow(`\n nessun metodo di accesso configurato per ${choice.fullModel}.\n`));
3957
+ ctl.write(theme.yellow(("\n " + tr("nessun metodo di accesso configurato per {p0}.", "No sign-in method configured for {p0}.", { p0: choice.fullModel }) + "\n")));
3831
3958
  ctl.write(theme.dim(authHint(choice.providerId) + "\n"));
3832
3959
  return null;
3833
3960
  }
3834
3961
  if (!requiresAuthChoice(choice.providerId))
3835
3962
  return options[0] ?? null;
3836
3963
  return selectOption({
3837
- title: "metodo accesso",
3838
- hint: "↑/↓ muovi Enter conferma Esc/q annulla",
3964
+ title: tr("metodo accesso", "sign-in method"),
3965
+ hint: tr("↑/↓ muovi Enter conferma Esc/q annulla", "↑/↓ move Enter confirm Esc/q cancel"),
3839
3966
  options,
3840
3967
  currentKey: harness.config.model,
3841
3968
  ctl,
@@ -3862,7 +3989,9 @@ function modelOptions(providers, dynamic) {
3862
3989
  const detail = dynById.get(model)?.detail;
3863
3990
  out.push({
3864
3991
  key: full,
3865
- label: `${spec.label} ${model}`,
3992
+ label: model,
3993
+ group: spec.label,
3994
+ modelInfo: dynById.get(model)?.modelInfo,
3866
3995
  detail: `${authSummary(spec.id, authProviders)}${detail ? ` · ${detail}` : ""}`,
3867
3996
  value: { providerId: spec.id, modelId: model, fullModel: full },
3868
3997
  });
@@ -3881,10 +4010,11 @@ async function loadDynamicModels(providers, ctl, theme, env) {
3881
4010
  continue;
3882
4011
  map.set(spec.id, catalog.list(spec.id)
3883
4012
  .filter(m => m.status !== "deprecated" && m.outputModalities.includes("text") && m.toolCall !== false)
3884
- .map(m => ({ id: m.id, detail: modelDetail(m) })));
4013
+ .map(m => ({ id: m.id, detail: modelDetail(m), modelInfo: { context: m.context,
4014
+ reasoning: m.reasoning, cost: m.cost } })));
3885
4015
  }
3886
4016
  if (providers.some((p) => p.id === "openrouter")) {
3887
- ctl.setStatus(theme.dim("carico catalogo OpenRouter…"));
4017
+ ctl.setStatus(theme.dim(tr("carico catalogo OpenRouter…", "Loading the OpenRouter catalog…")));
3888
4018
  try {
3889
4019
  const models = await fetchOpenRouterModels(env);
3890
4020
  if (models.length) {
@@ -3910,7 +4040,7 @@ function authOptions(choice, providers) {
3910
4040
  .map((provider) => ({
3911
4041
  key: `${provider.id}/${choice.modelId}`,
3912
4042
  label: authLabel(choice.providerId, provider.id),
3913
- detail: provider.label ?? provider.id,
4043
+ detail: providerLabel(provider),
3914
4044
  value: `${provider.id}/${choice.modelId}`,
3915
4045
  }));
3916
4046
  }
@@ -3926,14 +4056,14 @@ function authProviderIds(providerId) {
3926
4056
  }
3927
4057
  function authLabel(canonicalProviderId, actualProviderId) {
3928
4058
  if (canonicalProviderId === "anthropic" && actualProviderId === "claude-code")
3929
- return "OAuth abbonamento";
4059
+ return tr("OAuth abbonamento", "OAuth subscription");
3930
4060
  if (canonicalProviderId === "anthropic" && actualProviderId === "anthropic")
3931
4061
  return "API key";
3932
4062
  if (canonicalProviderId === "openai" && actualProviderId === "chatgpt")
3933
- return "OAuth abbonamento";
4063
+ return tr("OAuth abbonamento", "OAuth subscription");
3934
4064
  if (canonicalProviderId === "openai" && actualProviderId === "openai")
3935
4065
  return "API key";
3936
- return actualProviderId === "ollama" ? "locale" : "API key";
4066
+ return actualProviderId === "ollama" ? tr("locale", "local") : "API key";
3937
4067
  }
3938
4068
  function authSummary(providerId, providers) {
3939
4069
  return providers.map((provider) => authLabel(providerId, provider.id)).join(" / ");
@@ -3965,11 +4095,11 @@ function canonicalProviderId(providerId) {
3965
4095
  }
3966
4096
  function authHint(providerId) {
3967
4097
  if (providerId === "anthropic")
3968
- return " Configura CLAUDE_CODE OAuth con `claude` oppure ANTHROPIC_API_KEY.";
4098
+ return (" " + tr("Configura CLAUDE_CODE OAuth con `claude` oppure ANTHROPIC_API_KEY.", "Configure CLAUDE_CODE OAuth with `claude` or ANTHROPIC_API_KEY."));
3969
4099
  if (providerId === "openai")
3970
- return " Configura ChatGPT OAuth con `codex` oppure OPENAI_API_KEY.";
4100
+ return (" " + tr("Configura ChatGPT OAuth con `codex` oppure OPENAI_API_KEY.", "Configure ChatGPT OAuth with `codex` or OPENAI_API_KEY."));
3971
4101
  const spec = PROVIDER_CATALOG.find((item) => item.id === providerId);
3972
- return spec?.envKey ? ` Configura ${spec.envKey}.` : " Verifica la configurazione del provider.";
4102
+ return spec?.envKey ? (" " + tr("Configura {p0}.", "Configure {p0}.", { p0: spec.envKey })) : (" " + tr("Verifica la configurazione del provider.", "Check the provider configuration."));
3973
4103
  }
3974
4104
  function unique(values) {
3975
4105
  return [...new Set(values)];
@@ -3997,7 +4127,7 @@ async function showReadonlyOverlay(opts) {
3997
4127
  opts.theme.bold(` ${opts.title}`),
3998
4128
  opts.theme.dark(` ${"─".repeat(Math.max(8, width - 4))}`),
3999
4129
  ...body.map((line) => ` ${opts.theme.silver(truncate(line, Math.max(12, width - 4)))}`),
4000
- opts.theme.dark(` ${allLines.length > pageSize() ? `↑/↓ scorri · ${offset + 1}-${Math.min(offset + pageSize(), allLines.length)}/${allLines.length} · ` : ""}Esc/Invio/q chiude`),
4130
+ opts.theme.dark(` ${allLines.length > pageSize() ? `${tr("↑/↓ scorri", "↑/↓ scroll")} · ${offset + 1}-${Math.min(offset + pageSize(), allLines.length)}/${allLines.length} · ` : ""}${tr("Esc/Invio/q chiude", "Esc/Enter/q closes")}`),
4001
4131
  ];
4002
4132
  opts.ctl.setOverlay(rows);
4003
4133
  };