@di-code/coding-agent 0.1.4 → 0.1.7

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 (111) hide show
  1. package/README.md +79 -42
  2. package/dist/cli.d.ts +15 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +96 -17
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/clipboard-image.d.ts +3 -3
  7. package/dist/core/clipboard-image.d.ts.map +1 -1
  8. package/dist/core/clipboard-image.js +26 -13
  9. package/dist/core/clipboard-image.js.map +1 -1
  10. package/dist/core/resources/loader.d.ts.map +1 -1
  11. package/dist/core/resources/loader.js +15 -42
  12. package/dist/core/resources/loader.js.map +1 -1
  13. package/dist/core/resources/types.d.ts +6 -9
  14. package/dist/core/resources/types.d.ts.map +1 -1
  15. package/dist/core/resources/types.js.map +1 -1
  16. package/dist/core/session/session-manager.d.ts +12 -1
  17. package/dist/core/session/session-manager.d.ts.map +1 -1
  18. package/dist/core/session/session-manager.js +66 -3
  19. package/dist/core/session/session-manager.js.map +1 -1
  20. package/dist/core/session/session-storage.d.ts.map +1 -1
  21. package/dist/core/session/session-storage.js +29 -15
  22. package/dist/core/session/session-storage.js.map +1 -1
  23. package/dist/core/session/types.d.ts +6 -1
  24. package/dist/core/session/types.d.ts.map +1 -1
  25. package/dist/core/session/types.js +1 -1
  26. package/dist/core/session/types.js.map +1 -1
  27. package/dist/core/session.d.ts +23 -1
  28. package/dist/core/session.d.ts.map +1 -1
  29. package/dist/core/session.js +86 -13
  30. package/dist/core/session.js.map +1 -1
  31. package/dist/core/system-prompt.js +2 -2
  32. package/dist/core/system-prompt.js.map +1 -1
  33. package/dist/core/tools/file-search.d.ts +14 -0
  34. package/dist/core/tools/file-search.d.ts.map +1 -0
  35. package/dist/core/tools/file-search.js +99 -0
  36. package/dist/core/tools/file-search.js.map +1 -0
  37. package/dist/core/tools/glob.d.ts +11 -0
  38. package/dist/core/tools/glob.d.ts.map +1 -0
  39. package/dist/core/tools/glob.js +44 -0
  40. package/dist/core/tools/glob.js.map +1 -0
  41. package/dist/core/tools/grep.d.ts +13 -0
  42. package/dist/core/tools/grep.d.ts.map +1 -0
  43. package/dist/core/tools/grep.js +67 -0
  44. package/dist/core/tools/grep.js.map +1 -0
  45. package/dist/core/tools/load-skill.d.ts +10 -0
  46. package/dist/core/tools/load-skill.d.ts.map +1 -0
  47. package/dist/core/tools/load-skill.js +22 -0
  48. package/dist/core/tools/load-skill.js.map +1 -0
  49. package/dist/core/tools/path-boundary.d.ts +2 -0
  50. package/dist/core/tools/path-boundary.d.ts.map +1 -1
  51. package/dist/core/tools/path-boundary.js +4 -0
  52. package/dist/core/tools/path-boundary.js.map +1 -1
  53. package/dist/core/user-data.d.ts +3 -0
  54. package/dist/core/user-data.d.ts.map +1 -0
  55. package/dist/core/user-data.js +8 -0
  56. package/dist/core/user-data.js.map +1 -0
  57. package/dist/entry.js +13 -0
  58. package/dist/entry.js.map +1 -1
  59. package/dist/extensions/runtime.js +2 -2
  60. package/dist/extensions/runtime.js.map +1 -1
  61. package/dist/i18n.d.ts +5 -0
  62. package/dist/i18n.d.ts.map +1 -0
  63. package/dist/i18n.js +225 -0
  64. package/dist/i18n.js.map +1 -0
  65. package/dist/index.d.ts +3 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +3 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/main.d.ts +17 -0
  70. package/dist/main.d.ts.map +1 -1
  71. package/dist/main.js +243 -11
  72. package/dist/main.js.map +1 -1
  73. package/dist/mcp/config.d.ts +33 -0
  74. package/dist/mcp/config.d.ts.map +1 -0
  75. package/dist/mcp/config.js +211 -0
  76. package/dist/mcp/config.js.map +1 -0
  77. package/dist/mcp/loader.d.ts +21 -0
  78. package/dist/mcp/loader.d.ts.map +1 -0
  79. package/dist/mcp/loader.js +60 -0
  80. package/dist/mcp/loader.js.map +1 -0
  81. package/dist/mcp/tool-adapter.d.ts +5 -0
  82. package/dist/mcp/tool-adapter.d.ts.map +1 -0
  83. package/dist/mcp/tool-adapter.js +149 -0
  84. package/dist/mcp/tool-adapter.js.map +1 -0
  85. package/dist/modes/interactive-components.d.ts +2 -2
  86. package/dist/modes/interactive-components.d.ts.map +1 -1
  87. package/dist/modes/interactive-components.js +30 -33
  88. package/dist/modes/interactive-components.js.map +1 -1
  89. package/dist/modes/interactive-state.d.ts.map +1 -1
  90. package/dist/modes/interactive-state.js +2 -1
  91. package/dist/modes/interactive-state.js.map +1 -1
  92. package/dist/modes/interactive.d.ts +9 -0
  93. package/dist/modes/interactive.d.ts.map +1 -1
  94. package/dist/modes/interactive.js +209 -63
  95. package/dist/modes/interactive.js.map +1 -1
  96. package/dist/modes/tree-selector.d.ts +30 -0
  97. package/dist/modes/tree-selector.d.ts.map +1 -0
  98. package/dist/modes/tree-selector.js +205 -0
  99. package/dist/modes/tree-selector.js.map +1 -0
  100. package/dist/plugins/loader.d.ts +19 -1
  101. package/dist/plugins/loader.d.ts.map +1 -1
  102. package/dist/plugins/loader.js +21 -2
  103. package/dist/plugins/loader.js.map +1 -1
  104. package/dist/provider-onboarding.d.ts.map +1 -1
  105. package/dist/provider-onboarding.js +119 -22
  106. package/dist/provider-onboarding.js.map +1 -1
  107. package/dist/startup.d.ts +34 -2
  108. package/dist/startup.d.ts.map +1 -1
  109. package/dist/startup.js +278 -9
  110. package/dist/startup.js.map +1 -1
  111. package/package.json +6 -4
@@ -1,31 +1,38 @@
1
1
  import { statSync } from "node:fs";
2
- import { extname, resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { extname, join, resolve } from "node:path";
3
4
  import { CombinedAutocompleteProvider, Editor, Key, matchesKey, SelectList, SettingsList, } from "@di-code/tui";
4
5
  import { cleanupStaleClipboardImages, clipboardImageDirectory, isClipboardImagePath, readClipboardImagePath, removeClipboardImage, } from "../core/clipboard-image.js";
5
6
  import { extractImageAttachments } from "../core/image-input.js";
7
+ import { DEFAULT_LOCALE, translate } from "../i18n.js";
6
8
  import { showInteractiveProviderOnboarding, } from "../provider-onboarding.js";
7
- import { loadStartupConfiguration, removeGlobalProviderApiKey, resolveStartupRuntime } from "../startup.js";
9
+ import { removeGlobalProviderApiKey, saveGlobalLocale, saveGlobalModelSelection, saveGlobalThinkingLevel, } from "../startup.js";
8
10
  import { AutocompleteMenu, InteractiveChat, InteractiveComposer, InteractiveFooter, InteractiveHeader, } from "./interactive-components.js";
9
11
  import { InteractiveLayout } from "./interactive-layout.js";
10
12
  import { InteractiveProjection } from "./interactive-state.js";
13
+ import { TreeSelector } from "./tree-selector.js";
11
14
  export { InteractiveProjection } from "./interactive-state.js";
12
15
  const IMAGE_EXTENSIONS = new Set([".gif", ".jpeg", ".jpg", ".png", ".webp"]);
13
16
  const PASTE_IMAGE_KEY = process.platform === "win32" ? Key.alt("v") : Key.ctrl("v");
14
17
  const PASTE_IMAGE_SHORTCUT = process.platform === "win32" ? "Alt+V" : "Ctrl+V";
15
- const BUILTIN_SLASH_COMMANDS = [
16
- { name: "help", description: "Show interactive commands" },
17
- { name: "clear", description: "Clear visible messages" },
18
- { name: "model", description: "Open the model selector" },
19
- { name: "session", description: "Open the session selector" },
20
- { name: "theme", description: "Open the theme selector" },
21
- { name: "settings", description: "Open the settings selector" },
22
- { name: "login", description: "Choose a Provider and save its global API key" },
23
- { name: "logout", description: "Remove the current Provider global API key" },
24
- { name: "compact", description: "Compact the persisted context now" },
25
- { name: "usage", description: "Show token and cost usage" },
26
- { name: "retry", description: "Retry the last failed prompt" },
27
- { name: "steer", description: "Steer the running agent" },
28
- ];
18
+ function builtinSlashCommands(locale) {
19
+ const t = (key) => translate(locale, key);
20
+ return [
21
+ { name: "help", description: t("showInteractiveCommands") },
22
+ { name: "clear", description: t("clearVisibleMessages") },
23
+ { name: "model", description: t("openModelSelector") },
24
+ { name: "session", description: t("openSessionSelector") },
25
+ { name: "tree", description: t("openTreeSelector") },
26
+ { name: "theme", description: t("openThemeSelector") },
27
+ { name: "settings", description: t("openSettingsSelector") },
28
+ { name: "login", description: t("chooseProvider") },
29
+ { name: "logout", description: t("removeProviderKey") },
30
+ { name: "compact", description: t("compactContext") },
31
+ { name: "usage", description: t("showUsage") },
32
+ { name: "retry", description: t("retryPrompt") },
33
+ { name: "steer", description: t("steerAgent") },
34
+ ];
35
+ }
29
36
  function asImageAttachmentReference(pasted, cwd) {
30
37
  if (pasted.includes("\n"))
31
38
  return pasted;
@@ -66,6 +73,7 @@ export class InteractiveMode {
66
73
  sessionChoices;
67
74
  extensionHost;
68
75
  providerOnboarding;
76
+ agentDir;
69
77
  readClipboardImagePath;
70
78
  clipboardDirectory;
71
79
  clipboardFiles = new Set();
@@ -85,6 +93,7 @@ export class InteractiveMode {
85
93
  spinnerTimer;
86
94
  shutdownEmitted = false;
87
95
  preserveClipboardFiles = false;
96
+ locale;
88
97
  constructor(options) {
89
98
  this.session = options.session;
90
99
  this.projection.configureFilePreview(this.session.allowedRoot, () => this.refresh());
@@ -93,8 +102,10 @@ export class InteractiveMode {
93
102
  this.sessionChoices = [...(options.sessions ?? [])];
94
103
  this.extensionHost = options.extensionHost;
95
104
  this.providerOnboarding = options.providerOnboarding;
105
+ this.agentDir = resolve(options.agentDir ?? join(homedir(), ".di-code"));
106
+ this.locale = options.locale ?? DEFAULT_LOCALE;
96
107
  this.readClipboardImagePath = options.readClipboardImagePath ?? readClipboardImagePath;
97
- this.clipboardDirectory = clipboardImageDirectory(this.session.allowedRoot);
108
+ this.clipboardDirectory = clipboardImageDirectory(this.agentDir, this.session.allowedRoot);
98
109
  const autocomplete = {
99
110
  getSuggestions: (context, autocompleteOptions) => new CombinedAutocompleteProvider(this.listSlashCommands(), this.session.allowedRoot).getSuggestions(context, autocompleteOptions),
100
111
  applyCompletion: (context, item, prefix) => new CombinedAutocompleteProvider(this.listSlashCommands(), this.session.allowedRoot).applyCompletion(context, item, prefix),
@@ -102,9 +113,16 @@ export class InteractiveMode {
102
113
  this.editor = new Editor({
103
114
  maxHeight: 3,
104
115
  autocomplete,
116
+ submitAutocomplete: (context) => /^\/[^\s/]*$/.test(context.text.slice(0, context.cursor)),
105
117
  });
106
118
  this.editor.onSubmit = (text) => void this.submit(text);
107
- this.editor.onEscape = () => this.activeAbort?.abort();
119
+ this.editor.onEscape = () => {
120
+ if (!this.activeAbort)
121
+ return;
122
+ this.projection.setStatus(translate(this.locale, "cancelled"));
123
+ this.activeAbort.abort();
124
+ this.refresh();
125
+ };
108
126
  this.editor.onCommand = (data) => this.handleCommand(data);
109
127
  this.editor.onInterrupt = () => this.exit();
110
128
  this.editor.onChange = () => {
@@ -120,6 +138,7 @@ export class InteractiveMode {
120
138
  ...this.projection.state,
121
139
  model: `${this.session.modelId}${this.session.thinkingLevel ? `(${this.session.thinkingLevel})` : ""}`,
122
140
  theme: this.theme,
141
+ locale: this.locale,
123
142
  pasteImageShortcut: PASTE_IMAGE_SHORTCUT,
124
143
  });
125
144
  this.root = new InteractiveLayout({
@@ -141,7 +160,7 @@ export class InteractiveMode {
141
160
  this.projection.replaceTranscript(this.session.transcript);
142
161
  this.projection.setUsage(this.session.usage);
143
162
  this.subscribeToSession();
144
- void cleanupStaleClipboardImages(this.session.allowedRoot).catch((cause) => {
163
+ void cleanupStaleClipboardImages(this.agentDir, this.session.allowedRoot).catch((cause) => {
145
164
  this.projection.setError(cause instanceof Error ? cause.message : String(cause));
146
165
  this.refresh();
147
166
  });
@@ -190,6 +209,12 @@ export class InteractiveMode {
190
209
  try {
191
210
  const level = this.session.cycleThinkingLevel();
192
211
  this.projection.setStatus(level ? `thinking=${level}` : "Current model does not support thinking levels.");
212
+ if (level) {
213
+ void saveGlobalThinkingLevel(this.agentDir, this.session.providerId, this.session.modelId, level).catch((cause) => {
214
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
215
+ this.refresh();
216
+ });
217
+ }
193
218
  }
194
219
  catch (cause) {
195
220
  this.projection.setError(cause instanceof Error ? cause.message : String(cause));
@@ -227,7 +252,7 @@ export class InteractiveMode {
227
252
  try {
228
253
  const path = await this.readClipboardImagePath(this.clipboardDirectory);
229
254
  if (!path) {
230
- this.projection.setStatus("Clipboard has no supported image.");
255
+ this.projection.setStatus(this.locale === "zh-CN" ? "剪贴板中没有受支持的图片。" : "Clipboard has no supported image.");
231
256
  }
232
257
  else {
233
258
  this.editor.insertTextAtCursor(path);
@@ -241,9 +266,10 @@ export class InteractiveMode {
241
266
  this.refresh();
242
267
  }
243
268
  openThemeSelector() {
269
+ const t = (key) => translate(this.locale, key);
244
270
  const list = new SelectList([
245
- { value: "dark", label: "Dark", description: "Dark terminal theme" },
246
- { value: "light", label: "Light", description: "Light terminal theme" },
271
+ { value: "dark", label: t("dark"), description: t("darkTheme") },
272
+ { value: "light", label: t("light"), description: t("lightTheme") },
247
273
  ]);
248
274
  list.onSelect = (item) => {
249
275
  this.theme = item.value === "light" ? "light" : "dark";
@@ -264,6 +290,10 @@ export class InteractiveMode {
264
290
  try {
265
291
  this.session.setModel(item.value);
266
292
  this.projection.setStatus(`model=${item.value}`);
293
+ void saveGlobalModelSelection(this.agentDir, this.session.providerId, this.session.modelId).catch((cause) => {
294
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
295
+ this.refresh();
296
+ });
267
297
  this.closeOverlay();
268
298
  this.refresh();
269
299
  }
@@ -275,11 +305,12 @@ export class InteractiveMode {
275
305
  this.showOverlay(list);
276
306
  }
277
307
  openSessionSelector() {
308
+ const t = (key) => translate(this.locale, key);
278
309
  const list = new SelectList([
279
310
  {
280
311
  value: "__current__",
281
- label: "Current session",
282
- description: this.session.sessionFile ?? "In-memory session",
312
+ label: t("currentSession"),
313
+ description: this.session.sessionFile ?? t("inMemorySession"),
283
314
  },
284
315
  ...this.sessionChoices.map((choice) => ({
285
316
  value: choice.id,
@@ -303,28 +334,124 @@ export class InteractiveMode {
303
334
  list.onCancel = () => this.closeOverlay();
304
335
  this.showOverlay(list);
305
336
  }
337
+ openTreeSelector() {
338
+ if (this.promptInFlight || this.compactionInFlight || this.sessionSwitching) {
339
+ this.projection.setError(this.locale === "zh-CN"
340
+ ? "提示词运行时不能浏览会话树。"
341
+ : "Cannot browse the session tree while a prompt is running.");
342
+ this.refresh();
343
+ return;
344
+ }
345
+ if (this.session.sessionTree.length === 0) {
346
+ this.projection.setStatus(this.locale === "zh-CN" ? "当前会话还没有历史节点。" : "The current session has no history nodes.");
347
+ this.refresh();
348
+ return;
349
+ }
350
+ const selector = new TreeSelector({
351
+ nodes: this.session.sessionTree,
352
+ leafId: this.session.sessionLeafId,
353
+ locale: this.locale,
354
+ onContinue: (entry) => {
355
+ this.closeOverlay();
356
+ void this.navigateTreeEntry(entry.id);
357
+ },
358
+ onEdit: (entry) => {
359
+ if (entry.type !== "message" || entry.message.role !== "user") {
360
+ this.projection.setError(this.locale === "zh-CN" ? "只能编辑历史用户消息。" : "Only historical user messages can be edited.");
361
+ this.refresh();
362
+ return;
363
+ }
364
+ this.closeOverlay();
365
+ void this.navigateTreeEntry(entry.id);
366
+ },
367
+ onSummarize: (entry) => {
368
+ this.closeOverlay();
369
+ void this.summarizeTreeBranch(entry.id);
370
+ },
371
+ onCancel: () => this.closeOverlay(),
372
+ });
373
+ this.showOverlay(selector, true, "80%");
374
+ }
375
+ async navigateTreeEntry(entryId) {
376
+ try {
377
+ const result = await this.session.navigateTree(entryId);
378
+ if (!this.started)
379
+ return false;
380
+ this.projection.replaceTranscript(this.session.transcript);
381
+ this.editor.setValue(result.editorText ?? "");
382
+ this.projection.setStatus(result.imagesOmitted
383
+ ? this.locale === "zh-CN"
384
+ ? "已恢复文本;请重新附加图片。"
385
+ : "Text restored; reattach images before sending."
386
+ : `tree=${result.selectedEntryId}`);
387
+ return true;
388
+ }
389
+ catch (cause) {
390
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
391
+ return false;
392
+ }
393
+ finally {
394
+ this.refresh();
395
+ }
396
+ }
397
+ async summarizeTreeBranch(entryId) {
398
+ if (this.compactionInFlight || this.promptInFlight)
399
+ return;
400
+ this.compactionInFlight = true;
401
+ this.projection.setStatus(this.locale === "zh-CN" ? "正在为分支生成摘要。" : "Summarizing selected branch.");
402
+ this.refresh();
403
+ try {
404
+ if (!(await this.navigateTreeEntry(entryId)))
405
+ return;
406
+ await this.session.compact();
407
+ this.editor.setValue("");
408
+ this.projection.replaceTranscript(this.session.transcript);
409
+ this.projection.setStatus(this.locale === "zh-CN" ? "摘要分支已创建。" : "Summary branch created.");
410
+ }
411
+ catch (cause) {
412
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
413
+ }
414
+ finally {
415
+ this.compactionInFlight = false;
416
+ this.refresh();
417
+ }
418
+ }
306
419
  openSettingsSelector() {
420
+ const t = (key) => translate(this.locale, key);
307
421
  const list = new SettingsList([
308
422
  {
309
423
  id: "compaction",
310
- label: "Context compaction",
311
- currentValue: this.session.compactionEnabled ? "on" : "off",
312
- values: ["on", "off"],
313
- description: "Summarize older persisted context before the model limit is reached.",
424
+ label: t("contextCompaction"),
425
+ currentValue: this.session.compactionEnabled ? t("on") : t("off"),
426
+ values: [t("on"), t("off")],
314
427
  },
315
- ]);
428
+ {
429
+ id: "locale",
430
+ label: t("language"),
431
+ currentValue: this.locale,
432
+ values: ["en", "zh-CN"],
433
+ },
434
+ ], { title: t("settings") });
316
435
  list.onChange = (id, value) => {
436
+ if (id === "locale") {
437
+ this.locale = value === "zh-CN" ? "zh-CN" : "en";
438
+ void saveGlobalLocale(this.agentDir, this.locale).catch((cause) => {
439
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
440
+ this.refresh();
441
+ });
442
+ this.closeOverlay();
443
+ this.openSettingsSelector();
444
+ return;
445
+ }
317
446
  if (id !== "compaction")
318
447
  return;
319
448
  try {
320
- const enabled = this.session.setCompactionEnabled(value === "on");
321
- list.updateValue(id, enabled ? "on" : "off");
322
- this.projection.setStatus(enabled === (value === "on")
323
- ? `compaction=${enabled ? "on" : "off"}`
324
- : "compaction unavailable without a persisted session");
449
+ const enabled = this.session.setCompactionEnabled(value === t("on"));
450
+ list.updateValue(id, enabled ? t("on") : t("off"));
451
+ this.projection.setStatus(enabled === (value === t("on")) ? `compaction=${enabled ? t("on") : t("off")}` : t("compactionUnavailable"));
325
452
  }
326
453
  catch (cause) {
327
- list.updateValue(id, this.session.compactionEnabled ? "on" : "off");
454
+ list.updateValue(id, this.session.compactionEnabled ? t("on") : t("off"));
328
455
  this.projection.setError(cause instanceof Error ? cause.message : String(cause));
329
456
  this.closeOverlay();
330
457
  }
@@ -333,15 +460,16 @@ export class InteractiveMode {
333
460
  list.onCancel = () => this.closeOverlay();
334
461
  this.showOverlay(list);
335
462
  }
336
- showOverlay(component) {
463
+ showOverlay(component, preserveLastLine = false, maxHeight = "60%") {
337
464
  this.editor.cancelAutocomplete();
338
465
  this.closeAutocompleteOverlay();
339
466
  this.closeOverlay();
340
467
  this.overlay = this.tui.showOverlay(component, {
341
468
  width: "70%",
342
- maxHeight: "60%",
469
+ maxHeight,
343
470
  anchor: "center",
344
471
  margin: 1,
472
+ preserveLastLine,
345
473
  });
346
474
  }
347
475
  closeOverlay() {
@@ -361,7 +489,7 @@ export class InteractiveMode {
361
489
  const editorBounds = this.root.getEditorBounds(this.tui.columns);
362
490
  this.autocompleteOverlay = this.tui.showOverlay(menu, {
363
491
  width: "55%",
364
- maxHeight: 8,
492
+ maxHeight: 9,
365
493
  anchor: "bottom-left",
366
494
  placement: {
367
495
  anchorRow: editorBounds.end - 1,
@@ -369,6 +497,7 @@ export class InteractiveMode {
369
497
  preferred: "below",
370
498
  },
371
499
  margin: 1,
500
+ preserveLastLine: true,
372
501
  nonCapturing: true,
373
502
  });
374
503
  this.refresh();
@@ -399,6 +528,11 @@ export class InteractiveMode {
399
528
  this.unsubscribeSession = this.session.subscribeSession((event) => {
400
529
  if (!this.started)
401
530
  return;
531
+ if (event.type === "tree_navigated") {
532
+ this.projection.replaceTranscript(this.session.transcript);
533
+ this.refresh();
534
+ return;
535
+ }
402
536
  if (event.type === "queue_update") {
403
537
  this.steeringPrompts = [...event.steering];
404
538
  this.refreshQueue();
@@ -411,7 +545,7 @@ export class InteractiveMode {
411
545
  }
412
546
  async switchSession(choice) {
413
547
  if (this.promptInFlight || this.sessionSwitching) {
414
- this.projection.setError("Cannot switch sessions while a prompt is running.");
548
+ this.projection.setError(this.locale === "zh-CN" ? "提示词运行时不能切换会话。" : "Cannot switch sessions while a prompt is running.");
415
549
  this.refresh();
416
550
  return;
417
551
  }
@@ -425,8 +559,8 @@ export class InteractiveMode {
425
559
  this.unsubscribeSession?.();
426
560
  this.session = next;
427
561
  this.projection.configureFilePreview(next.allowedRoot, () => this.refresh());
428
- this.clipboardDirectory = clipboardImageDirectory(next.allowedRoot);
429
- void cleanupStaleClipboardImages(next.allowedRoot);
562
+ this.clipboardDirectory = clipboardImageDirectory(this.agentDir, next.allowedRoot);
563
+ void cleanupStaleClipboardImages(this.agentDir, next.allowedRoot);
430
564
  this.queuedPrompts = [];
431
565
  this.steeringPrompts = [];
432
566
  this.refreshQueue();
@@ -458,12 +592,16 @@ export class InteractiveMode {
458
592
  return;
459
593
  }
460
594
  if (this.sessionSwitching) {
461
- this.projection.setError("A session is opening; wait before submitting a prompt.");
595
+ this.projection.setError(this.locale === "zh-CN"
596
+ ? "会话正在打开,请等待后再提交提示词。"
597
+ : "A session is opening; wait before submitting a prompt.");
462
598
  this.refresh();
463
599
  return;
464
600
  }
465
601
  if (this.compactionInFlight) {
466
- this.projection.setError("A compaction is running; wait before submitting a prompt.");
602
+ this.projection.setError(this.locale === "zh-CN"
603
+ ? "正在压缩上下文,请等待后再提交提示词。"
604
+ : "A compaction is running; wait before submitting a prompt.");
467
605
  this.refresh();
468
606
  return;
469
607
  }
@@ -509,22 +647,28 @@ export class InteractiveMode {
509
647
  async submitSteering(text) {
510
648
  const prompt = text.trim();
511
649
  if (prompt.length === 0) {
512
- this.projection.setError("Steering content must not be empty.");
650
+ this.projection.setError(this.locale === "zh-CN" ? "引导内容不能为空。" : "Steering content must not be empty.");
513
651
  this.refresh();
514
652
  return;
515
653
  }
516
654
  if (this.sessionSwitching) {
517
- this.projection.setError("A session is opening; wait before steering the agent.");
655
+ this.projection.setError(this.locale === "zh-CN"
656
+ ? "会话正在打开,请等待后再引导 Agent。"
657
+ : "A session is opening; wait before steering the agent.");
518
658
  this.refresh();
519
659
  return;
520
660
  }
521
661
  if (this.compactionInFlight) {
522
- this.projection.setError("A compaction is running; wait before steering the agent.");
662
+ this.projection.setError(this.locale === "zh-CN"
663
+ ? "正在压缩上下文,请等待后再引导 Agent。"
664
+ : "A compaction is running; wait before steering the agent.");
523
665
  this.refresh();
524
666
  return;
525
667
  }
526
668
  if (!this.promptInFlight) {
527
- this.projection.setError("Steering is only available while a prompt is running.");
669
+ this.projection.setError(this.locale === "zh-CN"
670
+ ? "只能在提示词运行时引导 Agent。"
671
+ : "Steering is only available while a prompt is running.");
528
672
  this.refresh();
529
673
  return;
530
674
  }
@@ -570,7 +714,7 @@ export class InteractiveMode {
570
714
  break;
571
715
  case "clear":
572
716
  this.projection.clearVisibleMessages();
573
- this.projection.setStatus("visible messages cleared");
717
+ this.projection.setStatus(translate(this.locale, "visibleMessagesCleared"));
574
718
  break;
575
719
  case "model":
576
720
  this.openModelSelector();
@@ -578,6 +722,9 @@ export class InteractiveMode {
578
722
  case "session":
579
723
  this.openSessionSelector();
580
724
  return;
725
+ case "tree":
726
+ this.openTreeSelector();
727
+ return;
581
728
  case "theme":
582
729
  this.openThemeSelector();
583
730
  return;
@@ -600,7 +747,7 @@ export class InteractiveMode {
600
747
  if (this.lastFailedPrompt && !this.promptInFlight)
601
748
  void this.submit(this.lastFailedPrompt, true);
602
749
  else
603
- this.projection.setStatus("nothing to retry");
750
+ this.projection.setStatus(translate(this.locale, "nothingToRetry"));
604
751
  break;
605
752
  default: {
606
753
  if (this.extensionHost?.listCommands().some((entry) => entry.name === command)) {
@@ -628,7 +775,11 @@ export class InteractiveMode {
628
775
  }
629
776
  this.editor.cancelAutocomplete();
630
777
  this.closeAutocompleteOverlay();
631
- void showInteractiveProviderOnboarding({ ...this.providerOnboarding, tui: this.tui })
778
+ void showInteractiveProviderOnboarding({
779
+ ...this.providerOnboarding,
780
+ configuration: { ...this.providerOnboarding.configuration, locale: this.locale },
781
+ tui: this.tui,
782
+ })
632
783
  .then((runtime) => {
633
784
  if (!this.started)
634
785
  return;
@@ -660,7 +811,6 @@ export class InteractiveMode {
660
811
  return;
661
812
  }
662
813
  const providerId = this.session.providerId;
663
- const modelId = this.session.modelId;
664
814
  try {
665
815
  const removed = await removeGlobalProviderApiKey(this.providerOnboarding.agentDir, providerId);
666
816
  if (!removed) {
@@ -668,14 +818,7 @@ export class InteractiveMode {
668
818
  this.refresh();
669
819
  return;
670
820
  }
671
- const configuration = await loadStartupConfiguration(this.session.allowedRoot, this.providerOnboarding.configuration.environment, this.providerOnboarding.agentDir);
672
- const runtime = resolveStartupRuntime({
673
- ...configuration.environment,
674
- DI_CODE_PROVIDER: providerId,
675
- DI_CODE_MODEL: modelId,
676
- }, configuration.providers, configuration.defaults);
677
- this.session.setRuntime(runtime.provider, runtime.model);
678
- this.projection.setStatus(`global API key removed for provider=${providerId}; environment variables may still apply`);
821
+ this.projection.setStatus(`global API key removed for provider=${providerId}; it remains active until this session ends`);
679
822
  }
680
823
  catch (cause) {
681
824
  this.projection.setError(cause instanceof Error ? cause.message : String(cause));
@@ -687,7 +830,7 @@ export class InteractiveMode {
687
830
  name: `skill:${skill.name}`,
688
831
  description: skill.description,
689
832
  }));
690
- return [...BUILTIN_SLASH_COMMANDS, ...(this.extensionHost?.listCommands() ?? []), ...skills];
833
+ return [...builtinSlashCommands(this.locale), ...(this.extensionHost?.listCommands() ?? []), ...skills];
691
834
  }
692
835
  isLoadedSkillCommand(input) {
693
836
  const name = /^\/skill:([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s|$)/.exec(input)?.[1];
@@ -695,20 +838,23 @@ export class InteractiveMode {
695
838
  }
696
839
  formatUsage() {
697
840
  const usage = this.session.usage;
841
+ if (this.locale === "zh-CN") {
842
+ return `用量:请求=${usage.requestCount} 输入=${usage.inputTokens} 输出=${usage.outputTokens} 总计=${usage.totalTokens} 费用=$${usage.cost.total.toFixed(4)} 上下文=${usage.estimatedContextTokens}/${usage.contextWindow}`;
843
+ }
698
844
  return `usage: requests=${usage.requestCount} input=${usage.inputTokens} output=${usage.outputTokens} total=${usage.totalTokens} cost=$${usage.cost.total.toFixed(4)} context=${usage.estimatedContextTokens}/${usage.contextWindow}`;
699
845
  }
700
846
  async runManualCompaction() {
701
847
  if (this.promptInFlight || this.compactionInFlight) {
702
- this.projection.setError("Cannot compact while a prompt is running.");
848
+ this.projection.setError(this.locale === "zh-CN" ? "提示词运行时不能压缩上下文。" : "Cannot compact while a prompt is running.");
703
849
  this.refresh();
704
850
  return;
705
851
  }
706
852
  this.compactionInFlight = true;
707
- this.projection.setStatus("starting manual compaction");
853
+ this.projection.setStatus(translate(this.locale, "manualCompactionStarting"));
708
854
  this.refresh();
709
855
  try {
710
856
  await this.session.compact();
711
- this.projection.setStatus("manual compaction complete");
857
+ this.projection.setStatus(translate(this.locale, "manualCompactionComplete"));
712
858
  }
713
859
  catch (cause) {
714
860
  this.projection.setError(cause instanceof Error ? cause.message : String(cause));