@vascend/talos 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/dist/core/agent.d.ts +16 -3
  2. package/dist/core/agent.js +114 -34
  3. package/dist/core/background.d.ts +10 -3
  4. package/dist/core/background.js +55 -9
  5. package/dist/core/bootstrap.js +1 -0
  6. package/dist/core/compaction.js +1 -1
  7. package/dist/core/config-storage.d.ts +5 -0
  8. package/dist/core/config-storage.js +83 -0
  9. package/dist/core/config.d.ts +11 -2
  10. package/dist/core/config.js +24 -8
  11. package/dist/core/context-ledger.js +2 -1
  12. package/dist/core/context-policy.d.ts +1 -1
  13. package/dist/core/context-policy.js +10 -7
  14. package/dist/core/credentials.js +29 -30
  15. package/dist/core/effort.d.ts +1 -1
  16. package/dist/core/effort.js +5 -1
  17. package/dist/core/execution-guidance.d.ts +1 -1
  18. package/dist/core/execution-guidance.js +2 -0
  19. package/dist/core/file-coordination.d.ts +3 -0
  20. package/dist/core/file-coordination.js +28 -0
  21. package/dist/core/file-lease-store.d.ts +3 -0
  22. package/dist/core/file-lease-store.js +28 -12
  23. package/dist/core/fusion.d.ts +24 -0
  24. package/dist/core/fusion.js +80 -0
  25. package/dist/core/model-limits.d.ts +1 -0
  26. package/dist/core/model-limits.js +5 -1
  27. package/dist/core/permission-queue.js +15 -15
  28. package/dist/core/request-plan.js +4 -3
  29. package/dist/core/session-naming.js +4 -0
  30. package/dist/core/session.d.ts +4 -2
  31. package/dist/core/session.js +13 -4
  32. package/dist/core/settings.d.ts +2 -0
  33. package/dist/core/settings.js +5 -16
  34. package/dist/core/step-policy.d.ts +18 -0
  35. package/dist/core/step-policy.js +6 -0
  36. package/dist/core/tool-artifacts.d.ts +2 -0
  37. package/dist/core/tool-artifacts.js +21 -3
  38. package/dist/core/tool-result-index.js +29 -33
  39. package/dist/core/tool-result-store.js +6 -5
  40. package/dist/core/types.d.ts +10 -3
  41. package/dist/core/user-inbox.d.ts +9 -0
  42. package/dist/core/user-inbox.js +22 -3
  43. package/dist/core/user-input.js +1 -1
  44. package/dist/fusion/autopilot-fast.d.ts +59 -0
  45. package/dist/fusion/autopilot-fast.js +213 -0
  46. package/dist/fusion/autopilot.d.ts +75 -0
  47. package/dist/fusion/autopilot.js +308 -0
  48. package/dist/fusion/focus.d.ts +30 -0
  49. package/dist/fusion/focus.js +122 -0
  50. package/dist/fusion/gate.d.ts +30 -0
  51. package/dist/fusion/gate.js +95 -0
  52. package/dist/fusion/jev.d.ts +71 -0
  53. package/dist/fusion/jev.js +164 -0
  54. package/dist/fusion/receipts.d.ts +25 -0
  55. package/dist/fusion/receipts.js +34 -0
  56. package/dist/fusion/route.d.ts +25 -0
  57. package/dist/fusion/route.js +84 -0
  58. package/dist/fusion/run.d.ts +4 -0
  59. package/dist/fusion/run.js +186 -0
  60. package/dist/horizon/engine.d.ts +20 -1
  61. package/dist/horizon/engine.js +57 -0
  62. package/dist/horizon/index.d.ts +2 -2
  63. package/dist/horizon/index.js +1 -1
  64. package/dist/horizon/types.d.ts +29 -0
  65. package/dist/index.js +12 -0
  66. package/dist/providers/anthropic-oauth.d.ts +8 -3
  67. package/dist/providers/anthropic-oauth.js +13 -10
  68. package/dist/providers/anthropic.d.ts +9 -2
  69. package/dist/providers/anthropic.js +64 -21
  70. package/dist/providers/cli-models.d.ts +30 -0
  71. package/dist/providers/cli-models.js +194 -0
  72. package/dist/providers/model-effort.js +1 -1
  73. package/dist/providers/openai-oauth.d.ts +8 -3
  74. package/dist/providers/openai-oauth.js +9 -3
  75. package/dist/providers/openrouter-manual-fallback.d.ts +3 -1
  76. package/dist/providers/openrouter-manual-fallback.js +20 -20
  77. package/dist/providers/registry.js +3 -2
  78. package/dist/providers/retry.d.ts +3 -0
  79. package/dist/providers/retry.js +44 -1
  80. package/dist/remote/cifratura.d.ts +12 -0
  81. package/dist/remote/cifratura.js +66 -0
  82. package/dist/remote/index.d.ts +15 -0
  83. package/dist/remote/index.js +65 -0
  84. package/dist/remote/relay-client.d.ts +58 -0
  85. package/dist/remote/relay-client.js +315 -0
  86. package/dist/remote/sessione-remota.d.ts +60 -0
  87. package/dist/remote/sessione-remota.js +266 -0
  88. package/dist/sdk.d.ts +7 -2
  89. package/dist/sdk.js +3 -1
  90. package/dist/tools/builtin.js +41 -20
  91. package/dist/tools/capabilities.d.ts +3 -1
  92. package/dist/tools/capabilities.js +5 -2
  93. package/dist/tools/computer-use.js +2 -1
  94. package/dist/tools/pagination.js +1 -1
  95. package/dist/tools/program.js +34 -21
  96. package/dist/tools/team-tools.js +11 -6
  97. package/dist/tui/account-entry.js +14 -9
  98. package/dist/tui/agent-label.d.ts +2 -0
  99. package/dist/tui/agent-label.js +5 -0
  100. package/dist/tui/agent-sessions-screen.d.ts +12 -2
  101. package/dist/tui/agent-sessions-screen.js +62 -16
  102. package/dist/tui/agent-sessions.d.ts +14 -0
  103. package/dist/tui/agent-sessions.js +67 -6
  104. package/dist/tui/bottom-input.d.ts +46 -2
  105. package/dist/tui/bottom-input.js +358 -59
  106. package/dist/tui/branding.js +8 -4
  107. package/dist/tui/capability-lists.d.ts +10 -0
  108. package/dist/tui/capability-lists.js +41 -0
  109. package/dist/tui/command-output-registry.js +4 -0
  110. package/dist/tui/command-transcript.d.ts +1 -1
  111. package/dist/tui/command-transcript.js +17 -21
  112. package/dist/tui/commands.js +4 -1
  113. package/dist/tui/compaction-status.d.ts +7 -0
  114. package/dist/tui/compaction-status.js +39 -0
  115. package/dist/tui/config-recovery.d.ts +12 -0
  116. package/dist/tui/config-recovery.js +54 -0
  117. package/dist/tui/context-meter.d.ts +3 -0
  118. package/dist/tui/context-meter.js +35 -8
  119. package/dist/tui/diff.d.ts +9 -0
  120. package/dist/tui/diff.js +43 -19
  121. package/dist/tui/fusion-menu.d.ts +5 -0
  122. package/dist/tui/fusion-menu.js +89 -0
  123. package/dist/tui/i18n-catalog.js +312 -1
  124. package/dist/tui/live-command.d.ts +1 -0
  125. package/dist/tui/live-command.js +9 -0
  126. package/dist/tui/live-output.d.ts +1 -0
  127. package/dist/tui/live-output.js +11 -0
  128. package/dist/tui/model-details.js +13 -12
  129. package/dist/tui/model-picker.js +17 -7
  130. package/dist/tui/model-selection.js +19 -10
  131. package/dist/tui/native-scrollback.js +5 -0
  132. package/dist/tui/prompt-queue.d.ts +3 -0
  133. package/dist/tui/prompt-queue.js +19 -1
  134. package/dist/tui/provider-label.d.ts +2 -0
  135. package/dist/tui/provider-label.js +9 -0
  136. package/dist/tui/provider-menu.js +25 -24
  137. package/dist/tui/queue-screen.js +2 -1
  138. package/dist/tui/remote-command.d.ts +7 -0
  139. package/dist/tui/remote-command.js +79 -0
  140. package/dist/tui/repl.d.ts +8 -2
  141. package/dist/tui/repl.js +355 -311
  142. package/dist/tui/review-store.d.ts +1 -0
  143. package/dist/tui/review-store.js +28 -7
  144. package/dist/tui/review-team.js +4 -3
  145. package/dist/tui/runtime-error.d.ts +2 -0
  146. package/dist/tui/runtime-error.js +26 -0
  147. package/dist/tui/selector-mouse.d.ts +1 -0
  148. package/dist/tui/selector-mouse.js +1 -1
  149. package/dist/tui/selector.d.ts +7 -0
  150. package/dist/tui/selector.js +19 -2
  151. package/dist/tui/session-controls.d.ts +26 -0
  152. package/dist/tui/session-controls.js +65 -0
  153. package/dist/tui/session-naming-notice.js +5 -3
  154. package/dist/tui/session-replay.d.ts +1 -0
  155. package/dist/tui/session-replay.js +101 -32
  156. package/dist/tui/session-transcript-layout.d.ts +9 -0
  157. package/dist/tui/session-transcript-layout.js +41 -0
  158. package/dist/tui/startup.js +63 -33
  159. package/dist/tui/status-line.d.ts +1 -1
  160. package/dist/tui/status-line.js +20 -12
  161. package/dist/tui/talos-art-data.d.ts +1 -0
  162. package/dist/tui/talos-art-data.js +2 -0
  163. package/dist/tui/talos-art.d.ts +1 -0
  164. package/dist/tui/talos-art.js +9 -0
  165. package/dist/tui/task-labels.d.ts +1 -0
  166. package/dist/tui/task-labels.js +8 -0
  167. package/dist/tui/terminal-text.d.ts +2 -1
  168. package/dist/tui/terminal-text.js +28 -2
  169. package/dist/tui/theme.js +2 -2
  170. package/dist/tui/tool-presentation.d.ts +3 -0
  171. package/dist/tui/tool-presentation.js +98 -0
  172. package/dist/tui/tool-style.js +4 -0
  173. package/dist/tui/transcript-store.d.ts +25 -0
  174. package/dist/tui/transcript-store.js +151 -0
  175. package/dist/tui/usage-footer.d.ts +1 -1
  176. package/dist/tui/usage-footer.js +3 -1
  177. package/dist/tui/vascend-id.d.ts +49 -0
  178. package/dist/tui/vascend-id.js +321 -0
  179. package/dist/tui/web-login-screen.js +4 -3
  180. package/dist/tui/web-login.d.ts +7 -15
  181. package/dist/tui/web-login.js +38 -78
  182. package/dist/tui/worker-transcripts.d.ts +31 -0
  183. package/dist/tui/worker-transcripts.js +91 -0
  184. package/dist/vascend/candidate-collector.d.ts +6 -5
  185. package/dist/vascend/candidate-collector.js +44 -43
  186. package/dist/vascend/context-lint.js +32 -2
  187. package/dist/vascend/context-market-selection.js +12 -5
  188. package/dist/vascend/context-pack-render.js +10 -4
  189. package/dist/vascend/context-packet-mirrors.d.ts +8 -0
  190. package/dist/vascend/context-packet-mirrors.js +24 -0
  191. package/dist/vascend/context-plan-events.d.ts +4 -0
  192. package/dist/vascend/context-plan-events.js +44 -0
  193. package/dist/vascend/context-snapshot.js +4 -2
  194. package/dist/vascend/context-store-reader.d.ts +20 -0
  195. package/dist/vascend/context-store-reader.js +46 -0
  196. package/dist/vascend/file-lock.d.ts +1 -0
  197. package/dist/vascend/file-lock.js +26 -0
  198. package/dist/vascend/granular-kingdom.d.ts +1 -0
  199. package/dist/vascend/granular-kingdom.js +22 -15
  200. package/dist/vascend/kingdom-bus.d.ts +1 -0
  201. package/dist/vascend/kingdom-bus.js +3 -0
  202. package/dist/vascend/kingdom-channel.d.ts +2 -0
  203. package/dist/vascend/kingdom-channel.js +3 -0
  204. package/dist/vascend/kingdom-live.d.ts +32 -4
  205. package/dist/vascend/kingdom-live.js +149 -13
  206. package/dist/vascend/kingdom-run.d.ts +5 -1
  207. package/dist/vascend/kingdom-run.js +6 -2
  208. package/dist/vascend/kingdom.d.ts +3 -0
  209. package/dist/vascend/kingdom.js +20 -13
  210. package/dist/vascend/memory-packet.d.ts +2 -1
  211. package/dist/vascend/memory-packet.js +31 -44
  212. package/dist/vascend/plan-store.d.ts +3 -1
  213. package/dist/vascend/plan-store.js +6 -4
  214. package/dist/vascend/room-memory.d.ts +10 -1
  215. package/dist/vascend/room-memory.js +42 -2
  216. package/dist/vascend/runtime.d.ts +3 -11
  217. package/dist/vascend/runtime.js +11 -123
  218. package/dist/vascend/session-search.js +2 -0
  219. package/dist/vascend/team-session.js +3 -2
  220. package/dist/vascend/vision.d.ts +3 -1
  221. package/dist/vascend/vision.js +6 -4
  222. package/dist/vascend/worker-session.d.ts +20 -0
  223. package/dist/vascend/worker-session.js +83 -0
  224. package/package.json +1 -1
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,20 +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";
12
13
  import { readSessionUsage } from "../core/session-usage.js";
13
14
  import { storageHealth } from "../core/storage-health.js";
14
15
  import { storageWarning } from "./storage-warning.js";
15
16
  import { ContextMeter, contextDetails } from "./context-meter.js";
16
- import { buildUsageFooter, subscriptionDetails, SESSION_USAGE_EXPLANATION } from "./usage-footer.js";
17
+ import { buildUsageFooter, subscriptionDetails, sessionUsageExplanation } from "./usage-footer.js";
17
18
  import { copyText, lastAssistantText } from "./clipboard-text.js";
19
+ import { sendWorker, stopWorker, deleteWorker, workerFooter, bindWorkerInbox, wakeWorker } from "./session-controls.js";
18
20
  import { withCommandFeedback, notifyStatus } from "./command-feedback.js";
19
21
  import { loadModelsDev, cachedModelCatalog } from "../providers/models-dev.js";
20
22
  import { modelEfforts } from "../providers/model-effort.js";
21
23
  import { modelDetail } from "./model-details.js";
24
+ import { providerLabel } from "./provider-label.js";
22
25
  import { applyModelChoice, pickEffort } from "./model-selection.js";
26
+ import { pickFusion } from "./fusion-menu.js";
23
27
  import { PROVIDER_CATALOG } from "../providers/catalog.js";
24
28
  import { fetchOpenRouterModels, openRouterDetail } from "../providers/openrouter.js";
25
29
  import { providerRows, actionsFor, rowStatusLabel, providerKeyHint, saveProviderKey, saveProviderBaseUrl, removeProvider, } from "./provider-menu.js";
@@ -32,6 +36,7 @@ import { frostPaint, makeTheme, renderAssistantSegment, renderAssistantTurnEnd,
32
36
  import { renderVascendTerminalLogo } from "./logo.js";
33
37
  import { enterAccount } from "./account-entry.js";
34
38
  import { tr } from "./i18n.js";
39
+ import { runtimeError, isUserInterruption } from "./runtime-error.js";
35
40
  import { LANGUAGES, normalizePreference } from "./locales.js";
36
41
  import { languagePreference, saveLanguagePreference } from "./language-preference.js";
37
42
  import { leggiOnboarding } from "./onboarding.js";
@@ -48,7 +53,7 @@ import { restoreQueuedInput } from "./restore-queued-input.js";
48
53
  import { ReviewStore } from "./review-store.js";
49
54
  import { ReviewScreen, reviewText } from "./review-screen.js";
50
55
  import { fitLine, oneLine, terminalWidth, wrapText } from "./terminal-text.js";
51
- import { livePreview, liveResultSummary } from "./live-output.js";
56
+ import { livePreview, liveResultSummary, backgroundResultSummary } from "./live-output.js";
52
57
  import { commandSource, commandPreview, commandHeader, commandOutput, commandResultState } from "./command-transcript.js";
53
58
  import { mcpName, mcpHeader, mcpPreview, mcpOutput } from "./mcp-transcript.js";
54
59
  import { loadedSkill, skillHeader, skillPreview } from "./skill-transcript.js";
@@ -61,6 +66,7 @@ import { readClipboardImage } from "./clipboard-image.js";
61
66
  import { queryBackgroundMode } from "./term-bg.js";
62
67
  import { runKingdomWarm, warmConcurrency, decomposeKingdom, looksBigGoal, collectConflicts, formatConflictList } from "../vascend/kingdom.js";
63
68
  import { worktreeMode } from "../vascend/worktree.js";
69
+ import { kingdomLive } from "../vascend/kingdom-live.js";
64
70
  import { runKingdomForHost } from "../vascend/kingdom-run.js";
65
71
  import { kingdomBus } from "../vascend/kingdom-bus.js";
66
72
  import { resolveKingdomBus } from "../vascend/kingdom-channel.js";
@@ -85,11 +91,16 @@ import { saveGlobalSettings } from "../core/settings.js";
85
91
  import { autoApproves, nextPermMode, permModeLabel } from "./perm-mode.js";
86
92
  import { backgroundManager } from "../core/background.js";
87
93
  import { BUILTIN_COMMANDS, visibleBuiltinNames, commandsByCategory, suggestCommands } from "./commands.js";
88
- import { TASK_STATUS_LABELS } from "./task-labels.js";
89
- import { rimuoviLicenzaLocale, rimuoviTokenVascend, revocaSessione, tokenVascend, } from "./vascend-login.js";
90
- 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";
91
100
  import { listVibeModes, parseVibeMode } from "../core/vibe-modes.js";
92
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";
93
104
  // (BUILTIN_COMMANDS spostato in ./commands.ts con categorie: vedi import in testa.)
94
105
  // Sorgente dinamica dei nomi comando (builtin + estensioni + template + skill).
95
106
  function allCommandNames(harness, templates) {
@@ -327,6 +338,8 @@ export async function startRepl(deps) {
327
338
  const stats = new SessionUsageState(costTracker);
328
339
  stats.restore(harness.config.sessionDir, harness.session.id, harness.session.tree().find(n => n.message.role === "assistant")?.ts);
329
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);
330
343
  review.reset(sessionConversation(harness.session));
331
344
  review.sessions.restoreMain(readSessionUsage(harness.config.sessionDir, harness.session.id));
332
345
  const reviewContext = () => {
@@ -353,7 +366,7 @@ export async function startRepl(deps) {
353
366
  ? last.cacheReadTokens / Math.max(1, last.inputTokens + last.cacheReadTokens)
354
367
  : undefined;
355
368
  return {
356
- provider: current ? (current.provider.label ?? current.provider.id) : "non configurato",
369
+ provider: current ? providerLabel(current.provider) : tr("non configurato", "not configured"),
357
370
  sessionId: harness.session.id,
358
371
  sessionNodes: stima.nodi,
359
372
  headId: harness.session.head,
@@ -416,7 +429,7 @@ export async function startRepl(deps) {
416
429
  const startupMeta = () => {
417
430
  const resolved = harness.providers.resolve(harness.config.model);
418
431
  return {
419
- provider: resolved ? (resolved.provider.label ?? resolved.provider.id) : "non configurato",
432
+ provider: resolved ? providerLabel(resolved.provider) : tr("non configurato", "not configured"),
420
433
  sessionId: harness.session.id,
421
434
  sessionNodes: harness.session.tree().length,
422
435
  headId: harness.session.head,
@@ -450,6 +463,7 @@ export async function startRepl(deps) {
450
463
  // questa sessione soltanto: dopo /new, `txt #1` sarebbe un altro testo.
451
464
  const pasteRegistry = new PasteRegistry();
452
465
  const resetSessionUi = (ctl) => {
466
+ kingdomLive.clearAll();
453
467
  ctl.clearConversation?.();
454
468
  review.reset(sessionConversation(harness.session));
455
469
  review.sessions.restoreMain(readSessionUsage(harness.config.sessionDir, harness.session.id));
@@ -507,6 +521,7 @@ export async function startRepl(deps) {
507
521
  try {
508
522
  if (!line.startsWith("/")) {
509
523
  ctl.write(renderUserMessage(line, theme, terminalColumns(), fromQueue ? tr("Dalla coda · consegnato", "From queue · delivered") : undefined));
524
+ messaggioUtente(line);
510
525
  const imageParts = queued ? queued.parts.filter(part => part.type === "image") : imageRegistry.imagePartsFor(line);
511
526
  // In chat resta il riferimento compatto (`txt #1`); al modello va il
512
527
  // testo per intero. L'espansione avviene qui, dopo l'echo e prima del
@@ -524,6 +539,10 @@ export async function startRepl(deps) {
524
539
  ctl.write(reviewText(review, reviewContext(), theme, terminalColumns(), section));
525
540
  return;
526
541
  }
542
+ if (/^\/(?:agents|agenti)\s*$/i.test(line)) {
543
+ ctl.showAgents?.();
544
+ return;
545
+ }
527
546
  // /img <path>: allega esplicitamente un'immagine. La registra come "img #N"
528
547
  // e ne mostra l'anteprima inline; non avvia un turno (e' solo l'allegato).
529
548
  const imgCmd = /^\/img(?:\s+(.+))?$/i.exec(line);
@@ -634,7 +653,7 @@ export async function startRepl(deps) {
634
653
  ? { done: all.filter((t) => t.status === "completed").length, total: all.length, castles: castles.length }
635
654
  : null;
636
655
  return {
637
- model: harness.config.model,
656
+ model: harness.config.fusion ? `Fusion: Jev + ${harness.config.model}` : harness.config.model,
638
657
  provider: m.provider,
639
658
  cwd: harness.config.cwd,
640
659
  effort: harness.config.effort || "default",
@@ -656,15 +675,25 @@ export async function startRepl(deps) {
656
675
  };
657
676
  const input = new BottomInput({
658
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),
659
686
  createReviewScreen: (section) => new ReviewScreen(review, () => theme, reviewContext, section),
660
687
  nativeScroll: env["TALOS_NATIVE_SCROLL"] !== "0",
661
688
  headerLines: () => buildTerminalHeader(statusData(), theme),
662
689
  startupLines: () => buildTerminalStartup(statusData(), theme),
663
690
  promptText: () => theme.accent(harness.config.ui.prompt),
664
- 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")),
665
692
  composerPadding: () => !harness.session.head,
666
693
  composerStyle: (s) => theme.composerText?.(s) ?? theme.chatText(s),
667
694
  attachmentStyle: (s) => theme.attachmentRef?.(s) ?? s,
695
+ currentModel: () => harness.config.model,
696
+ limitWarningStyle: s => theme.yellow(s),
668
697
  belowInputLines: () => {
669
698
  if (!harness.session.head)
670
699
  return [buildStartupRuntime(statusData(), theme)];
@@ -699,14 +728,14 @@ export async function startRepl(deps) {
699
728
  // Ctrl+B: manda in background il comando shell in corso; se non ce n'e'
700
729
  // uno, mostra la lista dei task in background.
701
730
  onBackground: () => {
702
- const task = backgroundManager.requestBackground();
731
+ const task = backgroundManager.requestBackground(harness.session.id);
703
732
  if (task)
704
733
  input.write(theme.dim(`\n ⤓ in background [${task.id}]: ${truncate(task.command, 60)}\n`));
705
734
  else
706
735
  void showBackgroundPanel(input, theme, terminalColumns);
707
736
  },
708
737
  // Suffisso spinner: suggerisce Ctrl+B solo quando c'e' un comando in corso.
709
- runningHint: () => backgroundManager.hasForeground() ? "Ctrl+B sfondo" : null,
738
+ runningHint: () => backgroundManager.hasForeground(harness.session.id) ? "Ctrl+B sfondo" : null,
710
739
  prepareQueuedInput: text => [{ type: "text", text: pasteRegistry.expand(text) }, ...imageRegistry.imagePartsFor(text)],
711
740
  queuePersistence: () => new PromptQueueFile(harness.config.sessionDir, harness.session.id),
712
741
  restoreQueuedInput: entry => restoreQueuedInput(entry, imageRegistry),
@@ -716,6 +745,18 @@ export async function startRepl(deps) {
716
745
  if (result === "quit")
717
746
  input.requestQuit();
718
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
+ },
719
760
  // Incollo: un path/data-URI immagine diventa "img #N", un testo lungo
720
761
  // diventa "txt #N", il resto si inserisce come testo normale.
721
762
  onPaste: onPasteInput,
@@ -728,23 +769,28 @@ export async function startRepl(deps) {
728
769
  return span ? { start: span.start, end: span.end } : null;
729
770
  },
730
771
  onReady: () => {
731
- input.write(buildTerminalWelcome(terminalColumns(), theme));
732
- if (accessoNota)
733
- input.write(wrapText(accessoNota, Math.max(2, terminalColumns() - 3), true).map((line) => theme.muted(" " + line.trim())).join("\n") + "\n");
734
- input.write("\n");
772
+ input.write(buildTerminalWelcome(terminalColumns(), theme, accessoNota));
735
773
  if (harness.session.head)
736
774
  replaySession(harness.session, input, theme, terminalColumns());
737
775
  if (deps.resumePicker)
738
776
  void processLine("/resume", input, new AbortController().signal);
739
777
  },
740
778
  onQuit: () => {
779
+ kingdomLive.clearAll();
780
+ review.sessions.conversations.reset();
741
781
  void harness.host.emit("session_shutdown", {});
742
- stdout.write("\n");
743
- 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();
744
788
  },
745
789
  });
746
790
  // Le notify delle estensioni vanno nella regione viva (no corruzione).
747
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() });
748
794
  // UI reale per le estensioni: confirm/select/input passano dalla TUI (cosi'
749
795
  // un'estensione puo' fare domande in chat come il tool builtin ask_user).
750
796
  harness.host.bind({
@@ -785,7 +831,11 @@ export async function startRepl(deps) {
785
831
  backgroundManager.onFinish((t) => {
786
832
  if (!t.detached)
787
833
  return; // i comandi in primo piano mostrano gia' il risultato inline
788
- 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("✗");
789
839
  input.write(`\n ${mark} background ${t.id} ` +
790
840
  theme.dim(`[${t.status}${t.exitCode != null ? ` · uscita ${t.exitCode}` : ""}] ${truncate(t.command, 50)} · /bg ${t.id} per l'output\n`));
791
841
  });
@@ -846,7 +896,7 @@ export async function startRepl(deps) {
846
896
  processLine: (raw, signal) => processLine(raw, ctl, signal),
847
897
  prompt,
848
898
  requestClose: () => rl.close(),
849
- 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`)),
850
900
  installAbort: (abort) => {
851
901
  const onSigint = () => abort();
852
902
  process.once("SIGINT", onSigint);
@@ -889,15 +939,26 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
889
939
  // Gate permessi: selezione Sì/No dalla UI con le frecce (stile Claude Code),
890
940
  // niente piu' digitazione di y/n. Le modalita' "accetta modifiche"/"auto"
891
941
  // (Shift+Tab) saltano il prompt per le azioni che approvano.
892
- const confirm = async (action, detail) => {
942
+ const confirm = async (action, detail, requestSignal = signal) => {
893
943
  if (autoApproves(getPermMode(), action))
894
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) => {
895
949
  const choice = await selectOption({
896
- title: `consenti: ${action}`,
897
- 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,
898
959
  options: [
899
- { key: "yes", label: "Sì, esegui", detail: "consenti questa azione", value: true },
900
- { 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 },
901
962
  ],
902
963
  currentKey: "yes",
903
964
  ctl,
@@ -907,11 +968,12 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
907
968
  return choice?.value === true;
908
969
  };
909
970
  // Domanda all'utente in chat (tool ask_user): opzioni -> picker; libera -> input.
910
- const ask = async (question, options) => {
971
+ const ask = async (question, options, requestSignal = signal) => {
911
972
  if (ctl.queueQuestion)
912
- return ctl.queueQuestion(question, options, signal);
973
+ return ctl.queueQuestion(question, options, requestSignal);
913
974
  if (options && options.length) {
914
975
  const choice = await selectOption({
976
+ signal: requestSignal,
915
977
  title: question,
916
978
  question: true,
917
979
  options: options.map((o) => ({ label: o, detail: "", value: o })),
@@ -922,15 +984,20 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
922
984
  return choice?.value ?? null;
923
985
  }
924
986
  ctl.write(renderQuestion(question, theme, terminalColumns()));
925
- 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)"));
926
988
  };
927
989
  // Kingdoms Mode invocabile dall'agente (tool kingdoms_warm): esegue i castelli
928
990
  // in parallelo (vassalli) e ritorna i report. Il consenso e' gia' stato dato
929
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
+ };
930
996
  const spawnKingdom = async (castles, kopts) => {
931
997
  if (kopts?.detach)
932
998
  return runKingdomForHost({
933
- 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,
934
1001
  ...(kopts.baseBranch ? { baseBranch: kopts.baseBranch } : {}),
935
1002
  });
936
1003
  // Obiettivo complessivo del regno: esplicito > goal grezzo del turno.
@@ -962,6 +1029,7 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
962
1029
  concurrency: conc,
963
1030
  ask,
964
1031
  onEvent: live.onEvent,
1032
+ retainSessions: "queued",
965
1033
  worktree: wt,
966
1034
  ...(kopts?.baseBranch ? { baseBranch: kopts.baseBranch } : {}),
967
1035
  onNotice: (text) => notifyStatus(ctl, theme.accent(" ⎇ ") + theme.dim(text) + "\n"),
@@ -1028,6 +1096,7 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
1028
1096
  ctl.setStatus(tr("avvio", "starting"));
1029
1097
  let turnFailed = false;
1030
1098
  const wrappedSink = (event) => {
1099
+ osservaEvento(event);
1031
1100
  if (event.type === "error" || (event.type === "turn_end" && event.stopReason === "error"))
1032
1101
  turnFailed = true;
1033
1102
  ctl.review?.record(event);
@@ -1078,7 +1147,7 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
1078
1147
  const turnInput = imageParts && imageParts.length
1079
1148
  ? [{ type: "text", text: input }, ...imageParts]
1080
1149
  : input;
1081
- await runTurn({ ...deps.harness.deps(confirm), env: { ...env, TALOS_AUTO_TITLE: "1" }, 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);
1082
1151
  if (turnFailed || signal.aborted)
1083
1152
  return;
1084
1153
  // REVISORE di team: per i task complessi (molte stanze) con stanze fatte e
@@ -1143,8 +1212,14 @@ async function drive(deps, input, ctl, theme, terminalColumns, stats, signal, ge
1143
1212
  }
1144
1213
  }
1145
1214
  catch (err) {
1146
- ctl.review?.record({ type: "error", message: err.message });
1147
- 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
+ }
1148
1223
  }
1149
1224
  finally {
1150
1225
  ctl.review?.finish(signal.aborted);
@@ -1230,7 +1305,7 @@ async function runTeamReviewer(deps, vascend, sdir, sid, ctl, theme, terminalCol
1230
1305
  }
1231
1306
  vascend.loadPlan(sdir, sid);
1232
1307
  }
1233
- export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1308
+ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan, replay = false) {
1234
1309
  let textBuffer = "";
1235
1310
  let assistantSegments = 0;
1236
1311
  let assistantStarted = false;
@@ -1238,6 +1313,11 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1238
1313
  const writeNotice = (text, failed = false) => {
1239
1314
  notifyStatus(ctl, text, failed);
1240
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
+ };
1241
1321
  // Correlazione call->risultato e contenuto PRE-scrittura, per gli avvisi del
1242
1322
  // regno e per il diff dei tool di scrittura.
1243
1323
  const calls = new Map();
@@ -1247,6 +1327,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1247
1327
  const mcpCards = new Map();
1248
1328
  const toolCards = new Map();
1249
1329
  const skillCards = new Map();
1330
+ const backgroundPolls = new Map();
1250
1331
  let newInputTok = 0; // input non in cache del run (cumulativo)
1251
1332
  let cacheTok = 0; // cache letta/scritta nel run (cumulativo)
1252
1333
  let outTok = 0; // token generati nel run (cumulativi)
@@ -1302,21 +1383,21 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1302
1383
  }
1303
1384
  // Riposa il bullet allo stato statico (accent) come il resto dello scrollback.
1304
1385
  if (settle && liveDot && ctl.setLogLine) {
1305
- 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));
1306
1387
  }
1307
1388
  liveDot = null;
1308
1389
  };
1309
1390
  const startDot = (index, name, tail) => {
1310
1391
  stopDot(false); // chiudi l'eventuale precedente senza ridisegnare (verra' sovrascritto)
1311
- if (!ctl.setLogLine)
1312
- return; // fallback readline: niente animazione scrollback
1392
+ if (!ctl.setLogLine || replay)
1393
+ return; // Replay has no timers or terminal side effects.
1313
1394
  liveDot = { index, name, tail };
1314
1395
  liveFrame = 0;
1315
1396
  dotTimer = setInterval(() => {
1316
1397
  if (!liveDot || !ctl.setLogLine)
1317
1398
  return;
1318
1399
  liveFrame += 1;
1319
- 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));
1320
1401
  }, 140);
1321
1402
  };
1322
1403
  const beginCommand = () => {
@@ -1325,7 +1406,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1325
1406
  stopDot();
1326
1407
  ctl.write("\n");
1327
1408
  };
1328
- return (event) => {
1409
+ const sink = (event) => {
1329
1410
  switch (event.type) {
1330
1411
  case "context_window":
1331
1412
  ctl.refresh?.();
@@ -1356,7 +1437,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1356
1437
  // come evento status separato.
1357
1438
  lastTruncated = event.stopReason === "max_tokens";
1358
1439
  if (event.stopReason === "max_tokens") {
1359
- 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`));
1360
1441
  }
1361
1442
  break;
1362
1443
  case "status":
@@ -1365,6 +1446,15 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1365
1446
  break;
1366
1447
  // Attivita' del modello senza testo (es. streaming di un input grande).
1367
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
+ }
1368
1458
  break;
1369
1459
  case "text":
1370
1460
  ctl.setStatus(tr("risposta in corso", "responding"));
@@ -1379,6 +1469,10 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1379
1469
  parent.child = true;
1380
1470
  }
1381
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
+ }
1382
1476
  // Compact protocol wrappers emit real child calls. Show those commands
1383
1477
  // once, without a duplicate execute(args) envelope in the transcript.
1384
1478
  if (event.call.name === "execute" || event.call.name === "tool_invoke" || event.call.name === "run_program") {
@@ -1442,11 +1536,11 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1442
1536
  const st = String(event.call.input["status"] ?? "completed");
1443
1537
  const where = slug === "master" ? "" : `${slug}/`;
1444
1538
  if (st === "running")
1445
- ctl.setRoomStatus?.(`attività ${where}${task} avviata`, "running");
1539
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "running"), "running");
1446
1540
  else if (st === "failed")
1447
- ctl.setRoomStatus?.(`attività ${where}${task} non riuscita`, "failed");
1541
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "failed"), "failed");
1448
1542
  else
1449
- ctl.setRoomStatus?.(`attività ${where}${task} completata`, "done");
1543
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "completed"), "done");
1450
1544
  break;
1451
1545
  }
1452
1546
  // Each operation owns a small separate block. Full data stays in review.
@@ -1459,7 +1553,7 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1459
1553
  calls.set(event.call.id, event.call);
1460
1554
  // Cattura il contenuto PRIMA della scrittura, per il diff (il file non
1461
1555
  // e' ancora stato toccato a questo punto del loop).
1462
- if (event.call.name === "write_file") {
1556
+ if (!replay && event.call.name === "write_file") {
1463
1557
  const p = event.call.input["path"];
1464
1558
  if (typeof p === "string" && p) {
1465
1559
  const abs = isAbsolute(p) ? p : resolve(cwd, p);
@@ -1475,18 +1569,15 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1475
1569
  const argRendered = typeof pathArg === "string" && pathArg && argText === pathArg
1476
1570
  ? fileLink(isAbsolute(pathArg) ? pathArg : resolve(cwd, pathArg), theme.dim(pathArg))
1477
1571
  : theme.dim(argText);
1478
- // Nome del tool COLORATO per categoria (read/write/shell/search/...): a
1479
- // colpo d'occhio si distingue il tipo di azione. Git eredita la categoria
1480
- // shell (e' un comando bash).
1481
- const nameRendered = gitSum
1482
- ? paintCategoryName("shell", "Git")
1483
- : 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);
1484
1574
  // Coda statica della riga (tutto cio' che segue il bullet): la riusiamo
1485
1575
  // sia per la scrittura iniziale sia per l'animazione del bullet.
1486
- const tail = nameRendered + theme.dim("(") + argRendered + theme.dim(")");
1487
- 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();
1488
1579
  ctl.write(header + "\n");
1489
- toolCards.set(event.call.id, { end: ctl.logLength(), header });
1580
+ toolCards.set(event.call.id, { index, end: ctl.logLength(), header });
1490
1581
  if (event.call.name === "ask_user")
1491
1582
  startDot(ctl.logLength() - 1, event.call.name, tail);
1492
1583
  break;
@@ -1507,6 +1598,20 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1507
1598
  parent.isError = event.result.isError;
1508
1599
  }
1509
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
+ }
1510
1615
  const skillCard = skillCards.get(event.result.callId);
1511
1616
  if (skillCard) {
1512
1617
  skillCards.delete(event.result.callId);
@@ -1558,7 +1663,8 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1558
1663
  const card = commandCards.get(event.result.callId);
1559
1664
  if (card && call) {
1560
1665
  const state = commandResultState(event.result.output, event.result.isError);
1561
- 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]);
1562
1668
  if (retained)
1563
1669
  ctl.setLogLine?.(card.index, commandHeader(card.firstRow, state, theme, card.columns));
1564
1670
  // Parallel calls can complete in a different order. Repeat the
@@ -1588,11 +1694,11 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1588
1694
  const st = String(call.input["status"] ?? "completed");
1589
1695
  const where = slug === "master" ? "" : `${slug}/`;
1590
1696
  if (st === "running")
1591
- ctl.setRoomStatus?.(`attività ${where}${task} avviata`, "running");
1697
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "running"), "running");
1592
1698
  else if (st === "failed")
1593
- ctl.setRoomStatus?.(`attività ${where}${task} non riuscita`, "failed");
1699
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "failed"), "failed");
1594
1700
  else
1595
- ctl.setRoomStatus?.(`attività ${where}${task} completata`, "done");
1701
+ ctl.setRoomStatus?.(taskActivityLabel(`${where}${task}`, "completed"), "done");
1596
1702
  break;
1597
1703
  }
1598
1704
  // Detailed plans are available in the task panel.
@@ -1603,6 +1709,23 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1603
1709
  notifyStatus(ctl, notice);
1604
1710
  break;
1605
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
+ }
1606
1729
  // Associate out-of-order results with their own operation rather than
1607
1730
  // appending several anonymous result lines below the newest call.
1608
1731
  if (toolCard && toolCard.end !== ctl.logLength())
@@ -1612,16 +1735,9 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1612
1735
  // muted (piu' leggibile del dim slavato, ma chiaramente secondario).
1613
1736
  {
1614
1737
  const summary = liveResultSummary(event.result.output, event.result.isError ? null : resultSummary(call, event.result.output), terminalColumns());
1615
- ctl.write(theme.dark(" │ ") + (event.result.isError ? theme.red("× ") : theme.dark("⎿ ")) +
1738
+ ctl.write(theme.dark(" └ ") + (event.result.isError ? theme.red("× ") : "") +
1616
1739
  (event.result.isError ? theme.red(summary) : theme.dim(summary)) + "\n");
1617
1740
  }
1618
- // Keep the change preview alongside its operation.
1619
- if (call && !event.result.isError) {
1620
- const diff = diffForCall(call, before, event.result.output, theme, terminalColumns());
1621
- if (diff) {
1622
- ctl.write(previewDiff(diff, theme, terminalColumns() - 1));
1623
- }
1624
- }
1625
1741
  break;
1626
1742
  }
1627
1743
  case "session_naming":
@@ -1670,7 +1786,12 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1670
1786
  ctl.setLogLine?.(card.index, commandHeader(card.firstRow, "stopped", theme, card.columns));
1671
1787
  }
1672
1788
  commandCards.clear();
1673
- 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
+ }
1674
1795
  break;
1675
1796
  case "done":
1676
1797
  for (const card of mcpCards.values()) {
@@ -1689,11 +1810,15 @@ export function makeSink(theme, terminalColumns, cwd, env, ctl, plan) {
1689
1810
  // Auto-continuazione esaurita: il run finisce ancora troncato. Avvisa di
1690
1811
  // spezzare il compito (o alzare TALOS_MAX_AUTOCONTINUE).
1691
1812
  if (lastTruncated) {
1692
- 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`));
1693
1814
  }
1694
1815
  break;
1695
1816
  }
1696
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
+ } });
1697
1822
  }
1698
1823
  // Attribuzione token alle stanze + riepilogo per castello (metodo Danilov).
1699
1824
  // All'avvio della stanza (running) salva lo snapshot di outTok; a stanza
@@ -1882,10 +2007,10 @@ function renderVascendToolNotice(call, output, theme, columns) {
1882
2007
  const st = String(call.input["status"] ?? "completed");
1883
2008
  const where = slug === "master" ? "" : `${slug}/`;
1884
2009
  if (st === "running")
1885
- return renderVascendNotice("start", `attività ${where}${task} avviata`, theme);
2010
+ return renderVascendNotice("start", taskActivityLabel(`${where}${task}`, "running"), theme);
1886
2011
  if (st === "failed")
1887
- return renderVascendNotice("fail", `attività ${where}${task} non riuscita`, theme);
1888
- 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);
1889
2014
  }
1890
2015
  return null;
1891
2016
  }
@@ -1897,26 +2022,26 @@ function diffForCall(call, pre, output, theme, columns) {
1897
2022
  const append = call.input["append"] === true;
1898
2023
  const next = String(call.input["content"] ?? "");
1899
2024
  const existed = (pre ?? "") !== "";
1900
- const verb = append ? "aggiunto" : existed ? "modificato" : "creato";
2025
+ const verb = append ? tr("aggiunto", "appended") : existed ? tr("modificato", "modified") : tr("creato", "created");
1901
2026
  // In append mostriamo solo l'aggiunta (base vuota); altrimenti il delta vs file.
1902
- 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;
1903
2028
  }
1904
2029
  if (call.name === "edit_file" && output.startsWith("modificato:")) {
1905
2030
  const path = typeof call.input["path"] === "string" ? call.input["path"] : undefined;
1906
2031
  const oldS = String(call.input["old_string"] ?? "");
1907
2032
  const newS = String(call.input["new_string"] ?? "");
1908
- 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;
1909
2034
  }
1910
- return "";
2035
+ return undefined;
1911
2036
  }
1912
2037
  // Soglia riferita alle righe del confronto, escluse intestazione e separatore.
1913
- function previewDiff(full, theme, width) {
2038
+ function previewDiff(full, theme, width, hiddenRows = 0) {
1914
2039
  const lines = full.replace(/\n$/, "").split("\n");
1915
- const limit = 100 + 2;
1916
- if (lines.length <= limit)
2040
+ const limit = 100;
2041
+ if (lines.length <= limit && hiddenRows === 0)
1917
2042
  return full;
1918
2043
  const head = lines.slice(0, limit);
1919
- const hidden = lines.length - limit;
2044
+ const hidden = Math.max(0, lines.length - limit) + hiddenRows;
1920
2045
  const hint = ` … altre ${hidden} righe · Ctrl+O scegli file`;
1921
2046
  head.push(theme.dim(fitLine(terminalWidth(hint) <= width ? hint : ` Ctrl+O · +${hidden} righe`, width)));
1922
2047
  return head.join("\n") + "\n";
@@ -2119,115 +2244,6 @@ function resumeInto(harness, vascend, id, ctl, theme, tcols, resetSessionUi) {
2119
2244
  replaySession(loaded, ctl, theme, tcols());
2120
2245
  return true;
2121
2246
  }
2122
- // Etichetta leggibile del tool (stile Claude Code: Read/Write/Update/Bash...).
2123
- function toolLabel(name) {
2124
- const map = {
2125
- read_file: "Read",
2126
- write_file: "Write",
2127
- edit_file: "Update",
2128
- list_dir: "List",
2129
- bash: "Bash",
2130
- bash_output: "BashOutput",
2131
- web_fetch: "Fetch",
2132
- ask_user: "Ask",
2133
- wait: "Wait",
2134
- tool_search: "ToolSearch",
2135
- tool_invoke: "Invoke",
2136
- skill_search: "SkillSearch",
2137
- skill_load: "SkillLoad",
2138
- output_page: "Page",
2139
- kingdoms_warm: tr("Collaboratori", "Collaborators"),
2140
- kingdom_send: "Send",
2141
- kingdom_inbox: "Inbox",
2142
- kingdom_wait: "WaitMsg",
2143
- glob: "Glob",
2144
- grep: "Grep",
2145
- recall: "Recall",
2146
- image_generate: "Image",
2147
- vascend_plan: tr("Lista attività", "Task list"),
2148
- vascend_mark: tr("Stato attività", "Task status"),
2149
- vascend_note: "Note",
2150
- vascend_skill: "Skill",
2151
- vascend_addtasks: tr("Aggiungi attività", "Add tasks"),
2152
- vascend_addrooms: tr("Aggiungi attività", "Add tasks"),
2153
- vascend_status: tr("Lista attività", "Task list"),
2154
- };
2155
- return map[name] ?? name;
2156
- }
2157
- // Argomento mostrato accanto al tool: il path se c'e', il comando per bash,
2158
- // altrimenti gli argomenti compatti.
2159
- function toolArg(call) {
2160
- const p = call.input["path"];
2161
- if (call.name === "run_program") {
2162
- const code = String(call.input["code"] ?? "");
2163
- return tr("JavaScript · {count} righe · Ctrl+R dettagli", "JavaScript · {count} lines · Ctrl+R details", { count: code.split("\n").length });
2164
- }
2165
- if (call.name === "tool_invoke") {
2166
- const inner = String(call.input["name"] ?? "");
2167
- const a = call.input["args"] && typeof call.input["args"] === "object" ? compactArgs(call.input["args"]) : "";
2168
- return truncate(`${inner}${a && a !== "{}" ? ` ${a}` : ""}`, 56);
2169
- }
2170
- if (call.name === "tool_search" || call.name === "skill_search")
2171
- return truncate(String(call.input["query"] ?? ""), 50);
2172
- if (call.name === "skill_load")
2173
- return String(call.input["name"] ?? "");
2174
- if (call.name === "grep" || call.name === "glob")
2175
- return truncate(String(call.input["pattern"] ?? ""), 50);
2176
- if (call.name === "wait") {
2177
- const until = call.input["until"];
2178
- return typeof until === "string" && until ? `until ${truncate(until, 44)}` : `${call.input["seconds"] ?? 5}s`;
2179
- }
2180
- // Tool del metodo Danilov: argomento LEGGIBILE invece del JSON grezzo
2181
- // (es. Mark(T01 → running) al posto di Mark({"task":"T01","status":"running"})).
2182
- const danilov = vascendArg(call);
2183
- if (danilov !== null)
2184
- return danilov;
2185
- if (typeof p === "string" && p)
2186
- return p;
2187
- if (call.name === "bash")
2188
- return truncate(oneLine(String(call.input["command"] ?? "")), 60);
2189
- return compactArgs(call.input);
2190
- }
2191
- // Argomento LEGGIBILE per i tool del metodo Danilov: trasforma l'input grezzo
2192
- // in una sintesi human-friendly. Ritorna null se non e' un tool Danilov noto
2193
- // (cosi' toolArg prosegue con la sua logica standard).
2194
- function vascendArg(call) {
2195
- const i = call.input;
2196
- const castle = typeof i["castle"] === "string" && i["castle"] && i["castle"] !== "master" ? ` @${i["castle"]}` : "";
2197
- switch (call.name) {
2198
- case "vascend_mark": {
2199
- const task = String(i["task"] ?? "");
2200
- const status = String(i["status"] ?? "completed");
2201
- return truncate(`${task} → ${status}${castle}`, 56);
2202
- }
2203
- case "vascend_plan": {
2204
- const tasks = Array.isArray(i["tasks"]) ? i["tasks"].length : 0;
2205
- const title = typeof i["title"] === "string" ? i["title"] : "";
2206
- const n = tr("{count} attività", "{count} tasks", { count: tasks });
2207
- return truncate(title ? `${title} · ${n}${castle}` : `${n}${castle}`, 60);
2208
- }
2209
- case "vascend_addtasks": {
2210
- const tasks = Array.isArray(i["tasks"]) ? i["tasks"].length : 0;
2211
- return truncate("+" + tr("{count} attività", "{count} tasks", { count: tasks }) + castle, 56);
2212
- }
2213
- case "vascend_note": {
2214
- const task = typeof i["task"] === "string" && i["task"] ? `${i["task"]}: ` : "";
2215
- const text = String(i["text"] ?? i["note"] ?? "");
2216
- return truncate(`${task}${text}`, 56);
2217
- }
2218
- case "vascend_skill": {
2219
- const task = typeof i["task"] === "string" && i["task"] ? `${i["task"]}: ` : "";
2220
- const skills = Array.isArray(i["skills"]) ? i["skills"].join(", ") : String(i["skills"] ?? i["skill"] ?? "");
2221
- return truncate(`${task}${skills}`, 56);
2222
- }
2223
- default:
2224
- return null;
2225
- }
2226
- }
2227
- function compactArgs(input) {
2228
- const s = JSON.stringify(input);
2229
- return s.length > 80 ? s.slice(0, 77) + "..." : s;
2230
- }
2231
2247
  function firstLine(s) {
2232
2248
  const line = s.split("\n")[0] ?? "";
2233
2249
  return line.length > 100 ? line.slice(0, 97) + "..." : line;
@@ -2323,6 +2339,10 @@ function controlCollapsible(ctl, channel, action) {
2323
2339
  // Ritorna: "quit" per uscire, input da inviare al modello, o null se il
2324
2340
  // comando e' stato gestito internamente.
2325
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
+ }
2326
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 }));
2327
2347
  }
2328
2348
  async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi) {
@@ -2330,6 +2350,12 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2330
2350
  const args = rest.join(" ");
2331
2351
  const { harness } = deps;
2332
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
+ }
2333
2359
  case "review":
2334
2360
  case "revisione": {
2335
2361
  if (ctl.showReview)
@@ -2476,15 +2502,29 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2476
2502
  // Esegue/riapre il comando scelto come se digitato.
2477
2503
  return handleSlash(`/${selected.value}`, deps, ctl, theme, terminalColumns, themeCtl, templates, vascend, stats, signal, resetSessionUi);
2478
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
+ }
2479
2513
  case "logout": {
2480
2514
  // Esce dall'account Vascend: revoca best-effort sul backend, poi rimuove
2481
2515
  // il token locale. Al prossimo avvio il cancello chiedera' l'accesso.
2482
- const token = tokenVascend(harness.env);
2483
- 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);
2484
2520
  ctl.write(theme.dim("\n nessun accesso Vascend attivo\n"));
2485
2521
  return null;
2486
2522
  }
2487
- 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);
2488
2528
  rimuoviTokenVascend(harness.env);
2489
2529
  // Via anche la prova offline: un logout che la lasciasse in giro
2490
2530
  // regalerebbe una finestra offline a chi e' appena uscito.
@@ -2528,7 +2568,10 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2528
2568
  await pickModel(harness, ctl, theme, terminalColumns);
2529
2569
  return null;
2530
2570
  }
2531
- 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);
2532
2575
  return null;
2533
2576
  }
2534
2577
  case "models": {
@@ -2539,22 +2582,22 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2539
2582
  case "themes": {
2540
2583
  if (args) {
2541
2584
  if (themeCtl.apply(args)) {
2542
- 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")));
2543
2586
  }
2544
2587
  else {
2545
- ctl.write(theme.yellow(`\n tema sconosciuto: ${args}`) +
2546
- 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")));
2547
2590
  }
2548
2591
  return null;
2549
2592
  }
2550
2593
  const options = themeCtl.available.map((t) => ({
2551
2594
  key: t.name,
2552
2595
  label: t.name,
2553
- detail: t.mode === "light" ? "chiaro" : "scuro",
2596
+ detail: t.mode === "light" ? tr("chiaro", "light") : tr("scuro", "dark"),
2554
2597
  value: t.name,
2555
2598
  }));
2556
2599
  const selected = await selectOption({
2557
- title: "Temi",
2600
+ title: tr("Temi", "Themes"),
2558
2601
  options,
2559
2602
  currentKey: themeCtl.activeName(),
2560
2603
  ctl,
@@ -2562,7 +2605,7 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2562
2605
  terminalColumns,
2563
2606
  });
2564
2607
  if (selected && themeCtl.apply(selected.value)) {
2565
- 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")));
2566
2609
  }
2567
2610
  return null;
2568
2611
  }
@@ -2576,39 +2619,39 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2576
2619
  const snapshot = stats.cost.snapshot(harness.config.model);
2577
2620
  const last = stats.last;
2578
2621
  const context = (stats.context ?? new ContextMeter()).read(harness);
2579
- const lines = [theme.bold("\n Consumi della sessione") + theme.dim(` · ${harness.session.name || harness.session.id}`)];
2580
- 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()}`));
2581
2624
  if (stats.incomplete)
2582
- 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."))));
2583
2626
  if (stats.missing)
2584
- 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 }))));
2585
2628
  if (stats.unsaved)
2586
- 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."))));
2587
2630
  if (snapshot.totalTurns === 0) {
2588
- 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."))));
2589
2632
  }
2590
2633
  else {
2591
- lines.push(theme.silver(` chiamate modello: ${formatTokenCount(snapshot.totalTurns)}`));
2592
- lines.push(theme.silver(` input totale: ${formatTokenCount(snapshot.totalPromptTokens)}`));
2593
- lines.push(theme.dim(` nuovo: ${formatTokenCount(snapshot.totalInputTokens)}`));
2594
- lines.push(theme.dim(` cache letta: ${formatTokenCount(snapshot.totalCacheReadTokens)}`));
2595
- 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) }))));
2596
2639
  lines.push(theme.silver(` output: ${formatTokenCount(snapshot.totalOutputTokens)}`));
2597
- 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) }))));
2598
2641
  }
2599
2642
  if (last) {
2600
- lines.push(theme.bold("\n ultima chiamata"));
2601
- 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) }))));
2602
2645
  lines.push(theme.dim(` output ${formatTokenCount(last.outputTokens)}`));
2603
2646
  }
2604
2647
  if (snapshot.perModel.length) {
2605
- 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)"))));
2606
2649
  for (const row of snapshot.perModel)
2607
- 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 }))));
2608
2651
  }
2609
2652
  const providerId = harness.providers.resolve(harness.config.model)?.provider.id ?? "";
2610
2653
  lines.push(...subscriptionDetails(providerId, stats.limits?.get(providerId), theme));
2611
- 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"))));
2612
2655
  lines.push(...contextDetails(context).map(line => theme.dim(` ${line}`)));
2613
2656
  ctl.write(lines.join("\n") + "\n");
2614
2657
  return null;
@@ -2658,10 +2701,10 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2658
2701
  const csub = args.trim();
2659
2702
  if (csub === "close" || csub === "open" || csub === "toggle") {
2660
2703
  if (!controlCollapsible(ctl, "config", csub) && csub === "close") {
2661
- 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")));
2662
2705
  }
2663
2706
  else if (csub === "close") {
2664
- 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")));
2665
2708
  }
2666
2709
  return null;
2667
2710
  }
@@ -2669,21 +2712,21 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2669
2712
  const cfg = harness.config;
2670
2713
  const resolved = harness.providers.resolve(cfg.model);
2671
2714
  const provider = resolved
2672
- ? (resolved.provider.label ?? resolved.provider.id)
2673
- : "non configurato";
2715
+ ? providerLabel(resolved.provider)
2716
+ : tr("non configurato", "not configured");
2674
2717
  const settings = [
2675
- { key: "model", label: "Modello", detail: cfg.model, value: "model" },
2676
- { key: "provider", label: "Provider e accessi", detail: provider, value: "provider" },
2677
- { key: "vibe", label: "Profilo operativo", detail: cfg.vibeMode, value: "vibe" },
2678
- { 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" },
2679
2722
  { key: "cwd", label: "Workspace", detail: cfg.cwd, value: "cwd" },
2680
- { key: "sessions", label: "Sessioni", detail: harness.session.id, value: "sessions" },
2681
- { key: "extensions", label: "Estensioni", detail: `${harness.host.inventory().filter((item) => item.loaded).length} caricate`, value: "extensions" },
2682
- { 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" },
2683
2726
  ];
2684
2727
  const selected = await selectOption({
2685
- title: "Impostazioni",
2686
- 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"),
2687
2730
  options: settings,
2688
2731
  fullHeight: true,
2689
2732
  wide: true,
@@ -2698,22 +2741,22 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
2698
2741
  const cfg = harness.config;
2699
2742
  const files = cfg.loadedConfigFiles.length
2700
2743
  ? cfg.loadedConfigFiles.map((file) => ` ${file}`).join("\n")
2701
- : " nessun file caricato";
2702
- const body = theme.dim(` modello: ${cfg.model}\n`) +
2703
- 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`) +
2704
2747
  theme.dim(` vibeMode: ${cfg.vibeMode}\n`) +
2705
2748
  theme.dim(` sessionDir: ${cfg.sessionDir}\n`) +
2706
2749
  theme.dim(` contextFiles: ${cfg.contextFiles.join(", ")}\n`) +
2707
- theme.dim(` skill: ${harness.skills.length}${harness.skillCommandsEnabled ? "" : " (comandi off)"}${harness.skills.length ? ` (${harness.skills.map((s) => s.name).join(", ")})` : ""}\n`) +
2708
- theme.dim(` progetto fidato: ${harness.projectTrusted ? "si" : "no (risorse locali non caricate)"}\n`) +
2709
- theme.bold("\n tema\n") +
2710
- theme.dim(` attivo: ${themeCtl.activeName()}${themeCtl.spec() ? ` (${themeCtl.spec()?.mode === "light" ? "chiaro" : "scuro"})` : ""}\n`) +
2711
- 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")) +
2712
2755
  theme.dim(` banner: ${cfg.ui.banner}\n`) +
2713
2756
  theme.dim(` prompt: ${JSON.stringify(cfg.ui.prompt)}\n`) +
2714
2757
  theme.bold("\n file\n") +
2715
2758
  theme.dim(`${files}`);
2716
- 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);
2717
2760
  return null;
2718
2761
  }
2719
2762
  case "vibe":
@@ -3074,13 +3117,11 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
3074
3117
  ctl.write(theme.green(`\n ${result.message}\n`));
3075
3118
  return null;
3076
3119
  }
3077
- // Compact UNIFICATO: una sola operazione che (1) compatta il contesto vivo
3078
- // con riassunto LLM (stile Pi) e (2) salva il checkpoint Vascend/Danilov in
3079
- // .vascend-compact.md per la ripresa a freddo. /vascend-compact e' un alias.
3080
- 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.
3081
3122
  case "compact": {
3082
3123
  const hasModel = Boolean(harness.providers.resolve(harness.config.model));
3083
- let feedback = "Checkpoint salvato; nessun modello configurato per compattare.";
3124
+ let feedback = "Nessun modello configurato; contesto conservato.";
3084
3125
  let failed = false;
3085
3126
  // 1) Compattazione LLM del contesto della sessione.
3086
3127
  if (hasModel) {
@@ -3103,16 +3144,16 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
3103
3144
  }, args || undefined);
3104
3145
  if (done) {
3105
3146
  const after = meter.read(harness);
3106
- feedback = `Contesto compattato: ~${formatTokenCount(before)} -> ~${formatTokenCount(after.tokens)} token`;
3147
+ feedback = tr("Contesto compattato: ~{before} -> ~{after} token", "Context compacted: ~{before} -> ~{after} tokens", { before: formatTokenCount(before), after: formatTokenCount(after.tokens) });
3107
3148
  }
3108
3149
  else {
3109
3150
  feedback = controller.signal.aborted
3110
- ? "Compattazione interrotta; contesto conservato."
3111
- : "Nessuna compattazione necessaria; contesto conservato.";
3151
+ ? tr("Compattazione interrotta; contesto conservato.", "Compaction interrupted; context preserved.")
3152
+ : tr("Nessuna compattazione necessaria; contesto conservato.", "No compaction needed; context preserved.");
3112
3153
  }
3113
3154
  }
3114
3155
  catch (err) {
3115
- feedback = `Compattazione non riuscita: ${err.message}`;
3156
+ feedback = tr("Compattazione non riuscita: {detail}", "Compaction failed: {detail}", { detail: runtimeError(err.message) });
3116
3157
  failed = true;
3117
3158
  }
3118
3159
  finally {
@@ -3122,9 +3163,6 @@ async function handleSlashCommand(line, deps, ctl, theme, terminalColumns, theme
3122
3163
  ctl.setStatus(null);
3123
3164
  }
3124
3165
  }
3125
- // 2) Checkpoint Vascend/Danilov sullo stato (gia' compattato) della sessione.
3126
- vascend.loadPlan(harness.config.sessionDir, harness.config.planStoreId ?? harness.config.roomStoreId ?? harness.session.id);
3127
- vascend.compact(harness.session.history());
3128
3166
  notifyStatus(ctl, feedback, failed);
3129
3167
  return null;
3130
3168
  }
@@ -3483,7 +3521,7 @@ async function loginMenu(harness, ctl, theme, terminalColumns) {
3483
3521
  const rows = loginRows(harness.env, harness.providers);
3484
3522
  const picked = await selectOption({
3485
3523
  title: tr("accesso ai modelli", "model sign-in"),
3486
- 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"),
3487
3525
  options: rows.map((row) => ({
3488
3526
  key: row.id,
3489
3527
  label: row.label,
@@ -3511,7 +3549,7 @@ async function loginMenu(harness, ctl, theme, terminalColumns) {
3511
3549
  }
3512
3550
  const action = await selectOption({
3513
3551
  title: `${row.label} · ${loginStatusLabel(row)}`,
3514
- hint: row.command ? `comando: ${row.command.join(" ")}` : "",
3552
+ hint: row.command ? tr("comando: {p0}", "command: {p0}", { p0: row.command.join(" ") }) : "",
3515
3553
  options: [
3516
3554
  {
3517
3555
  key: "login",
@@ -3545,10 +3583,10 @@ async function loginMenu(harness, ctl, theme, terminalColumns) {
3545
3583
  harness.reloadProviders();
3546
3584
  const refreshed = loginRows(harness.env, harness.providers).find((item) => item.id === row.id);
3547
3585
  if (completed && refreshed?.configured && !refreshed.needsRefresh) {
3548
- 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")));
3549
3587
  }
3550
3588
  else if (completed) {
3551
- 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")));
3552
3590
  }
3553
3591
  }
3554
3592
  }
@@ -3557,11 +3595,11 @@ async function runInteractiveLogin(command, harness, ctl, theme) {
3557
3595
  if (!executable)
3558
3596
  return false;
3559
3597
  if (!stdin.isTTY || !stdout.isTTY) {
3560
- 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")));
3561
3599
  return false;
3562
3600
  }
3563
- ctl.write(theme.bold(`\n avvio ${command.join(" ")}\n`));
3564
- 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")));
3565
3603
  ctl.pause();
3566
3604
  ctl.setOverlay(null);
3567
3605
  ctl.setStatus(null);
@@ -3580,7 +3618,7 @@ async function runInteractiveLogin(command, harness, ctl, theme) {
3580
3618
  child.once("exit", (value) => resolve(value ?? -1));
3581
3619
  });
3582
3620
  if (code !== 0) {
3583
- 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")));
3584
3622
  return false;
3585
3623
  }
3586
3624
  return true;
@@ -3604,13 +3642,13 @@ async function providerMenu(harness, ctl, theme, terminalColumns) {
3604
3642
  const options = rows.map((row) => ({
3605
3643
  key: row.id,
3606
3644
  label: `${row.label}${row.active ? " ←" : ""}`,
3607
- detail: `${rowStatusLabel(row)}${row.modelCount ? ` · ${row.modelCount} modelli` : ""}`,
3645
+ detail: `${rowStatusLabel(row)}${row.modelCount ? (" " + tr("· {p0} modelli", "· {p0} models", { p0: row.modelCount })) : ""}`,
3608
3646
  value: row,
3609
3647
  }));
3610
3648
  const configured = rows.filter((r) => r.configured).length;
3611
3649
  const picked = await selectOption({
3612
- title: `provider — ${configured} configurati su ${rows.length}`,
3613
- 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"),
3614
3652
  options,
3615
3653
  ctl,
3616
3654
  theme,
@@ -3646,7 +3684,7 @@ async function providerActions(row, harness, ctl, theme, terminalColumns) {
3646
3684
  case "set-key": {
3647
3685
  const ok = await setKeyFlow(fresh, harness, ctl, theme);
3648
3686
  if (ok)
3649
- 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")));
3650
3688
  break;
3651
3689
  }
3652
3690
  case "set-endpoint": {
@@ -3665,11 +3703,11 @@ async function providerActions(row, harness, ctl, theme, terminalColumns) {
3665
3703
  }
3666
3704
  case "remove": {
3667
3705
  const confirmed = await selectOption({
3668
- title: `rimuovere la credenziale di ${fresh.label}?`,
3669
- 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"),
3670
3708
  options: [
3671
- { key: "no", label: "No, mantieni", detail: "", value: false },
3672
- { 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 },
3673
3711
  ],
3674
3712
  currentKey: "no",
3675
3713
  ctl,
@@ -3680,10 +3718,10 @@ async function providerActions(row, harness, ctl, theme, terminalColumns) {
3680
3718
  const res = removeProvider(harness.env, fresh.id);
3681
3719
  if (res.ok) {
3682
3720
  harness.reloadProviders();
3683
- 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")));
3684
3722
  }
3685
3723
  else {
3686
- 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`));
3687
3725
  }
3688
3726
  return "back";
3689
3727
  }
@@ -3696,28 +3734,28 @@ async function providerActions(row, harness, ctl, theme, terminalColumns) {
3696
3734
  // rete), la salva e ricostruisce il registro. Ritorna true se salvata.
3697
3735
  async function setKeyFlow(row, harness, ctl, theme) {
3698
3736
  ctl.write(theme.dim(`\n ${providerKeyHint(row.spec)}\n`));
3699
- 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 }));
3700
3738
  if (!key)
3701
3739
  return false;
3702
3740
  const trimmed = key.trim();
3703
3741
  // Validazione di forma per OpenRouter (sk-or-...). Avviso non bloccante.
3704
3742
  if (row.id === "openrouter" && !looksLikeApiKey(trimmed)) {
3705
- 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")));
3706
3744
  }
3707
3745
  // Validazione di rete (oggi solo OpenRouter espone /key). Best effort.
3708
3746
  if (row.id === "openrouter") {
3709
- 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…")));
3710
3748
  try {
3711
3749
  const v = await validateApiKey(trimmed);
3712
3750
  ctl.setStatus(null);
3713
3751
  if (!v.ok) {
3714
- 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")));
3715
3753
  return false;
3716
3754
  }
3717
3755
  const credit = v.limitRemaining === null || v.limitRemaining === undefined
3718
- ? "credito illimitato"
3719
- : `credito residuo ${v.limitRemaining.toFixed(2)}`;
3720
- 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")));
3721
3759
  }
3722
3760
  catch {
3723
3761
  ctl.setStatus(null);
@@ -3725,7 +3763,7 @@ async function setKeyFlow(row, harness, ctl, theme) {
3725
3763
  }
3726
3764
  const res = saveProviderKey(harness.env, row.id, trimmed);
3727
3765
  if (!res.ok) {
3728
- 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`));
3729
3767
  return false;
3730
3768
  }
3731
3769
  // La env scoped serve a reloadProviders (che la rilegge dal disco), ma la
@@ -3737,13 +3775,13 @@ async function setKeyFlow(row, harness, ctl, theme) {
3737
3775
  }
3738
3776
  // Flusso "imposta endpoint": chiede una baseUrl (vuota = ripristina default).
3739
3777
  async function setEndpointFlow(row, harness, ctl, theme) {
3740
- ctl.write(theme.dim(`\n endpoint attuale: ${row.baseUrl ?? "(default del provider)"} · vuoto = ripristina default\n`));
3741
- 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 }));
3742
3780
  if (url === null)
3743
3781
  return; // Esc
3744
3782
  const res = saveProviderBaseUrl(harness.env, row.id, url);
3745
3783
  if (!res.ok) {
3746
- 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`));
3747
3785
  return;
3748
3786
  }
3749
3787
  const norm = row.id.toUpperCase().replace(/[^A-Z0-9]/g, "_");
@@ -3752,26 +3790,26 @@ async function setEndpointFlow(row, harness, ctl, theme) {
3752
3790
  else
3753
3791
  delete harness.env[`TALOS_${norm}_BASE_URL`];
3754
3792
  harness.reloadProviders();
3755
- 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")));
3756
3794
  }
3757
3795
  // Flusso OAuth (oggi: OpenRouter PKCE). Mostra l'URL da aprire nel browser e
3758
3796
  // raccoglie il code dal callback, poi scambia e salva la chiave.
3759
3797
  async function oauthFlow(row, harness, ctl, theme, terminalColumns) {
3760
3798
  if (row.id !== "openrouter") {
3761
- 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);
3762
3800
  return;
3763
3801
  }
3764
3802
  // Il login e' una vista operativa temporanea: non deve diventare parte della
3765
3803
  // conversazione. L'overlay viene aggiornato in place e rimosso al termine.
3766
3804
  let authUrl = "";
3767
- let authState = "avvio il server locale…";
3805
+ let authState = tr("avvio il server locale…", "Starting the local server…");
3768
3806
  const drawOAuth = () => {
3769
3807
  const lines = [authState];
3770
3808
  if (authUrl)
3771
- lines.push("", "Se il browser non si apre, copia questo link:", authUrl);
3772
- 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."));
3773
3811
  ctl.setOverlay([
3774
- theme.bold(" OpenRouter · accesso via browser"),
3812
+ theme.bold((" " + tr("OpenRouter · accesso via browser", "OpenRouter · browser sign-in"))),
3775
3813
  theme.dark(` ${"─".repeat(Math.max(12, terminalColumns() - 4))}`),
3776
3814
  ...lines.flatMap((line) => wrapPlain(line || " ", Math.max(20, terminalColumns() - 4)))
3777
3815
  .map((line) => ` ${theme.silver(line)}`),
@@ -3782,19 +3820,19 @@ async function oauthFlow(row, harness, ctl, theme, terminalColumns) {
3782
3820
  switch (ev.phase) {
3783
3821
  case "url_built":
3784
3822
  authUrl = ev.detail ?? authUrl;
3785
- authState = "apro il browser per autorizzare…";
3823
+ authState = tr("apro il browser per autorizzare…", "Opening the browser for authorization…");
3786
3824
  break;
3787
3825
  case "browser_opened":
3788
- authState = "browser aperto · in attesa dell'autorizzazione…";
3826
+ authState = tr("browser aperto · in attesa dell'autorizzazione…", "Browser opened · waiting for authorization…");
3789
3827
  break;
3790
3828
  case "browser_failed":
3791
- 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…");
3792
3830
  break;
3793
3831
  case "awaiting_code":
3794
- authState = "in attesa dell'autorizzazione nel browser…";
3832
+ authState = tr("in attesa dell'autorizzazione nel browser…", "Waiting for authorization in the browser…");
3795
3833
  break;
3796
3834
  case "exchanging":
3797
- authState = "autorizzazione ricevuta · attivo la API key…";
3835
+ authState = tr("autorizzazione ricevuta · attivo la API key…", "Authorization received · activating the API key…");
3798
3836
  break;
3799
3837
  default:
3800
3838
  break;
@@ -3825,20 +3863,20 @@ async function oauthFlow(row, harness, ctl, theme, terminalColumns) {
3825
3863
  saveProviderKey(harness.env, row.id, result.apiKey);
3826
3864
  harness.env["TALOS_OPENROUTER_API_KEY"] = result.apiKey;
3827
3865
  harness.reloadProviders();
3828
- transientNotice(ctl, "OpenRouter collegato e attivato", "done", theme);
3866
+ transientNotice(ctl, tr("OpenRouter collegato e attivato", "OpenRouter connected and activated"), "done", theme);
3829
3867
  return;
3830
3868
  }
3831
3869
  // Fallimento del flusso automatico (browser/timeout/callback/server):
3832
3870
  // ricadiamo sul percorso manuale con un flow nuovo e coerente (PKCE
3833
3871
  // proprio + authUrl mostrato all'utente, che incolla il code).
3834
- const reason = result.error ?? "callback automatico non riuscito";
3872
+ const reason = result.error ?? tr("callback automatico non riuscito", "automatic callback failed");
3835
3873
  const manualFlow = startOAuthFlow(harness.env, callbackUrlForPort());
3836
3874
  const manualLines = [];
3837
3875
  const manual = await runManualCodeFallback({
3838
3876
  print: (line) => { manualLines.push(line); },
3839
3877
  prompt: async (label) => {
3840
3878
  await showReadonlyOverlay({
3841
- title: "OpenRouter · autorizzazione manuale",
3879
+ title: tr("OpenRouter · autorizzazione manuale", "OpenRouter · manual authorization"),
3842
3880
  lines: manualLines,
3843
3881
  ctl,
3844
3882
  theme,
@@ -3846,25 +3884,25 @@ async function oauthFlow(row, harness, ctl, theme, terminalColumns) {
3846
3884
  });
3847
3885
  return ctl.promptInput(label);
3848
3886
  },
3849
- }, manualFlow, { reason });
3887
+ }, manualFlow, { reason, translate: tr });
3850
3888
  if (!manual.ok || !manual.apiKey) {
3851
3889
  if (manual.error === "cancelled") {
3852
- transientNotice(ctl, "OAuth annullato", "done", theme);
3890
+ transientNotice(ctl, tr("OAuth annullato", "OAuth cancelled"), "done", theme);
3853
3891
  }
3854
3892
  else {
3855
- 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);
3856
3894
  }
3857
3895
  return;
3858
3896
  }
3859
3897
  saveProviderKey(harness.env, row.id, manual.apiKey);
3860
3898
  harness.env["TALOS_OPENROUTER_API_KEY"] = manual.apiKey;
3861
3899
  harness.reloadProviders();
3862
- transientNotice(ctl, "OpenRouter collegato e attivato", "done", theme);
3900
+ transientNotice(ctl, tr("OpenRouter collegato e attivato", "OpenRouter connected and activated"), "done", theme);
3863
3901
  }
3864
3902
  catch (err) {
3865
3903
  ctl.setStatus(null);
3866
3904
  ctl.setOverlay(null);
3867
- transientNotice(ctl, `errore OAuth: ${err.message}`, "failed", theme);
3905
+ transientNotice(ctl, tr("errore OAuth: {p0}", "OAuth error: {p0}", { p0: err.message }), "failed", theme);
3868
3906
  }
3869
3907
  }
3870
3908
  function transientNotice(ctl, label, state, theme) {
@@ -3877,13 +3915,15 @@ async function pickModel(harness, ctl, theme, terminalColumns) {
3877
3915
  .filter((row) => !row.configured || row.needsRefresh)
3878
3916
  .map((row) => ({
3879
3917
  key: `access:${row.id}`,
3880
- label: `${row.needsRefresh ? "Rinnova" : "Collega"} ${row.label}`,
3918
+ label: `${row.needsRefresh ? tr("Rinnova", "Renew") : tr("Collega", "Connect")} ${row.label}`,
3881
3919
  detail: `${loginStatusLabel(row)} · ${row.detail}`,
3882
3920
  value: { providerId: `access:${row.id}`, modelId: "", fullModel: `access:${row.id}` },
3883
3921
  }));
3884
- 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)];
3885
3925
  if (!options.length) {
3886
- 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")));
3887
3927
  return false;
3888
3928
  }
3889
3929
  const current = canonicalModel(harness.config.model);
@@ -3900,6 +3940,8 @@ async function pickModel(harness, ctl, theme, terminalColumns) {
3900
3940
  });
3901
3941
  if (!selected)
3902
3942
  return false;
3943
+ if (selected.value.providerId === "fusion")
3944
+ return pickFusion(harness, ctl, theme, terminalColumns);
3903
3945
  if (selected.value.providerId.startsWith("access:")) {
3904
3946
  await loginMenu(harness, ctl, theme, terminalColumns);
3905
3947
  return false;
@@ -3912,15 +3954,15 @@ async function pickModel(harness, ctl, theme, terminalColumns) {
3912
3954
  async function pickAuthMethod(choice, harness, ctl, theme, terminalColumns) {
3913
3955
  const options = authOptions(choice, harness.providers.list());
3914
3956
  if (!options.length) {
3915
- 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")));
3916
3958
  ctl.write(theme.dim(authHint(choice.providerId) + "\n"));
3917
3959
  return null;
3918
3960
  }
3919
3961
  if (!requiresAuthChoice(choice.providerId))
3920
3962
  return options[0] ?? null;
3921
3963
  return selectOption({
3922
- title: "metodo accesso",
3923
- 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"),
3924
3966
  options,
3925
3967
  currentKey: harness.config.model,
3926
3968
  ctl,
@@ -3949,6 +3991,7 @@ function modelOptions(providers, dynamic) {
3949
3991
  key: full,
3950
3992
  label: model,
3951
3993
  group: spec.label,
3994
+ modelInfo: dynById.get(model)?.modelInfo,
3952
3995
  detail: `${authSummary(spec.id, authProviders)}${detail ? ` · ${detail}` : ""}`,
3953
3996
  value: { providerId: spec.id, modelId: model, fullModel: full },
3954
3997
  });
@@ -3967,10 +4010,11 @@ async function loadDynamicModels(providers, ctl, theme, env) {
3967
4010
  continue;
3968
4011
  map.set(spec.id, catalog.list(spec.id)
3969
4012
  .filter(m => m.status !== "deprecated" && m.outputModalities.includes("text") && m.toolCall !== false)
3970
- .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 } })));
3971
4015
  }
3972
4016
  if (providers.some((p) => p.id === "openrouter")) {
3973
- ctl.setStatus(theme.dim("carico catalogo OpenRouter…"));
4017
+ ctl.setStatus(theme.dim(tr("carico catalogo OpenRouter…", "Loading the OpenRouter catalog…")));
3974
4018
  try {
3975
4019
  const models = await fetchOpenRouterModels(env);
3976
4020
  if (models.length) {
@@ -3996,7 +4040,7 @@ function authOptions(choice, providers) {
3996
4040
  .map((provider) => ({
3997
4041
  key: `${provider.id}/${choice.modelId}`,
3998
4042
  label: authLabel(choice.providerId, provider.id),
3999
- detail: provider.label ?? provider.id,
4043
+ detail: providerLabel(provider),
4000
4044
  value: `${provider.id}/${choice.modelId}`,
4001
4045
  }));
4002
4046
  }
@@ -4012,14 +4056,14 @@ function authProviderIds(providerId) {
4012
4056
  }
4013
4057
  function authLabel(canonicalProviderId, actualProviderId) {
4014
4058
  if (canonicalProviderId === "anthropic" && actualProviderId === "claude-code")
4015
- return "OAuth abbonamento";
4059
+ return tr("OAuth abbonamento", "OAuth subscription");
4016
4060
  if (canonicalProviderId === "anthropic" && actualProviderId === "anthropic")
4017
4061
  return "API key";
4018
4062
  if (canonicalProviderId === "openai" && actualProviderId === "chatgpt")
4019
- return "OAuth abbonamento";
4063
+ return tr("OAuth abbonamento", "OAuth subscription");
4020
4064
  if (canonicalProviderId === "openai" && actualProviderId === "openai")
4021
4065
  return "API key";
4022
- return actualProviderId === "ollama" ? "locale" : "API key";
4066
+ return actualProviderId === "ollama" ? tr("locale", "local") : "API key";
4023
4067
  }
4024
4068
  function authSummary(providerId, providers) {
4025
4069
  return providers.map((provider) => authLabel(providerId, provider.id)).join(" / ");
@@ -4051,11 +4095,11 @@ function canonicalProviderId(providerId) {
4051
4095
  }
4052
4096
  function authHint(providerId) {
4053
4097
  if (providerId === "anthropic")
4054
- 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."));
4055
4099
  if (providerId === "openai")
4056
- 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."));
4057
4101
  const spec = PROVIDER_CATALOG.find((item) => item.id === providerId);
4058
- 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."));
4059
4103
  }
4060
4104
  function unique(values) {
4061
4105
  return [...new Set(values)];
@@ -4083,7 +4127,7 @@ async function showReadonlyOverlay(opts) {
4083
4127
  opts.theme.bold(` ${opts.title}`),
4084
4128
  opts.theme.dark(` ${"─".repeat(Math.max(8, width - 4))}`),
4085
4129
  ...body.map((line) => ` ${opts.theme.silver(truncate(line, Math.max(12, width - 4)))}`),
4086
- 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")}`),
4087
4131
  ];
4088
4132
  opts.ctl.setOverlay(rows);
4089
4133
  };