@di-code/coding-agent 0.1.3 → 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 (147) hide show
  1. package/README.md +91 -30
  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/context-builder.d.ts.map +1 -1
  11. package/dist/core/context-builder.js +6 -2
  12. package/dist/core/context-builder.js.map +1 -1
  13. package/dist/core/image-input.d.ts +1 -1
  14. package/dist/core/image-input.d.ts.map +1 -1
  15. package/dist/core/image-input.js +55 -10
  16. package/dist/core/image-input.js.map +1 -1
  17. package/dist/core/resources/loader.d.ts.map +1 -1
  18. package/dist/core/resources/loader.js +15 -42
  19. package/dist/core/resources/loader.js.map +1 -1
  20. package/dist/core/resources/types.d.ts +6 -9
  21. package/dist/core/resources/types.d.ts.map +1 -1
  22. package/dist/core/resources/types.js.map +1 -1
  23. package/dist/core/session/session-manager.d.ts +12 -1
  24. package/dist/core/session/session-manager.d.ts.map +1 -1
  25. package/dist/core/session/session-manager.js +66 -3
  26. package/dist/core/session/session-manager.js.map +1 -1
  27. package/dist/core/session/session-storage.d.ts.map +1 -1
  28. package/dist/core/session/session-storage.js +30 -15
  29. package/dist/core/session/session-storage.js.map +1 -1
  30. package/dist/core/session/types.d.ts +6 -1
  31. package/dist/core/session/types.d.ts.map +1 -1
  32. package/dist/core/session/types.js +1 -1
  33. package/dist/core/session/types.js.map +1 -1
  34. package/dist/core/session.d.ts +38 -3
  35. package/dist/core/session.d.ts.map +1 -1
  36. package/dist/core/session.js +167 -9
  37. package/dist/core/session.js.map +1 -1
  38. package/dist/core/system-prompt.js +2 -2
  39. package/dist/core/system-prompt.js.map +1 -1
  40. package/dist/core/tools/bash.d.ts +2 -2
  41. package/dist/core/tools/bash.d.ts.map +1 -1
  42. package/dist/core/tools/bash.js.map +1 -1
  43. package/dist/core/tools/edit-diff.d.ts +25 -0
  44. package/dist/core/tools/edit-diff.d.ts.map +1 -0
  45. package/dist/core/tools/edit-diff.js +136 -0
  46. package/dist/core/tools/edit-diff.js.map +1 -0
  47. package/dist/core/tools/edit.d.ts +13 -4
  48. package/dist/core/tools/edit.d.ts.map +1 -1
  49. package/dist/core/tools/edit.js +64 -30
  50. package/dist/core/tools/edit.js.map +1 -1
  51. package/dist/core/tools/file-search.d.ts +14 -0
  52. package/dist/core/tools/file-search.d.ts.map +1 -0
  53. package/dist/core/tools/file-search.js +99 -0
  54. package/dist/core/tools/file-search.js.map +1 -0
  55. package/dist/core/tools/glob.d.ts +11 -0
  56. package/dist/core/tools/glob.d.ts.map +1 -0
  57. package/dist/core/tools/glob.js +44 -0
  58. package/dist/core/tools/glob.js.map +1 -0
  59. package/dist/core/tools/grep.d.ts +13 -0
  60. package/dist/core/tools/grep.d.ts.map +1 -0
  61. package/dist/core/tools/grep.js +67 -0
  62. package/dist/core/tools/grep.js.map +1 -0
  63. package/dist/core/tools/load-skill.d.ts +10 -0
  64. package/dist/core/tools/load-skill.d.ts.map +1 -0
  65. package/dist/core/tools/load-skill.js +22 -0
  66. package/dist/core/tools/load-skill.js.map +1 -0
  67. package/dist/core/tools/path-boundary.d.ts +2 -0
  68. package/dist/core/tools/path-boundary.d.ts.map +1 -1
  69. package/dist/core/tools/path-boundary.js +4 -0
  70. package/dist/core/tools/path-boundary.js.map +1 -1
  71. package/dist/core/tools/read.d.ts +2 -2
  72. package/dist/core/tools/read.d.ts.map +1 -1
  73. package/dist/core/tools/read.js.map +1 -1
  74. package/dist/core/tools/write.d.ts +2 -2
  75. package/dist/core/tools/write.d.ts.map +1 -1
  76. package/dist/core/tools/write.js.map +1 -1
  77. package/dist/core/user-data.d.ts +3 -0
  78. package/dist/core/user-data.d.ts.map +1 -0
  79. package/dist/core/user-data.js +8 -0
  80. package/dist/core/user-data.js.map +1 -0
  81. package/dist/entry.js +22 -2
  82. package/dist/entry.js.map +1 -1
  83. package/dist/extensions/runtime.js +2 -2
  84. package/dist/extensions/runtime.js.map +1 -1
  85. package/dist/i18n.d.ts +5 -0
  86. package/dist/i18n.d.ts.map +1 -0
  87. package/dist/i18n.js +223 -0
  88. package/dist/i18n.js.map +1 -0
  89. package/dist/index.d.ts +3 -0
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +3 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/main.d.ts +20 -1
  94. package/dist/main.d.ts.map +1 -1
  95. package/dist/main.js +272 -17
  96. package/dist/main.js.map +1 -1
  97. package/dist/mcp/config.d.ts +33 -0
  98. package/dist/mcp/config.d.ts.map +1 -0
  99. package/dist/mcp/config.js +211 -0
  100. package/dist/mcp/config.js.map +1 -0
  101. package/dist/mcp/loader.d.ts +21 -0
  102. package/dist/mcp/loader.d.ts.map +1 -0
  103. package/dist/mcp/loader.js +60 -0
  104. package/dist/mcp/loader.js.map +1 -0
  105. package/dist/mcp/tool-adapter.d.ts +5 -0
  106. package/dist/mcp/tool-adapter.d.ts.map +1 -0
  107. package/dist/mcp/tool-adapter.js +149 -0
  108. package/dist/mcp/tool-adapter.js.map +1 -0
  109. package/dist/modes/interactive-components.d.ts +2 -0
  110. package/dist/modes/interactive-components.d.ts.map +1 -1
  111. package/dist/modes/interactive-components.js +55 -31
  112. package/dist/modes/interactive-components.js.map +1 -1
  113. package/dist/modes/interactive-diff.d.ts +8 -0
  114. package/dist/modes/interactive-diff.d.ts.map +1 -0
  115. package/dist/modes/interactive-diff.js +78 -0
  116. package/dist/modes/interactive-diff.js.map +1 -0
  117. package/dist/modes/interactive-layout.d.ts +4 -0
  118. package/dist/modes/interactive-layout.d.ts.map +1 -1
  119. package/dist/modes/interactive-layout.js +4 -0
  120. package/dist/modes/interactive-layout.js.map +1 -1
  121. package/dist/modes/interactive-state.d.ts +5 -0
  122. package/dist/modes/interactive-state.d.ts.map +1 -1
  123. package/dist/modes/interactive-state.js +65 -11
  124. package/dist/modes/interactive-state.js.map +1 -1
  125. package/dist/modes/interactive.d.ts +19 -0
  126. package/dist/modes/interactive.d.ts.map +1 -1
  127. package/dist/modes/interactive.js +351 -52
  128. package/dist/modes/interactive.js.map +1 -1
  129. package/dist/modes/tree-selector.d.ts +30 -0
  130. package/dist/modes/tree-selector.d.ts.map +1 -0
  131. package/dist/modes/tree-selector.js +205 -0
  132. package/dist/modes/tree-selector.js.map +1 -0
  133. package/dist/plugins/loader.d.ts +19 -1
  134. package/dist/plugins/loader.d.ts.map +1 -1
  135. package/dist/plugins/loader.js +21 -2
  136. package/dist/plugins/loader.js.map +1 -1
  137. package/dist/provider-onboarding.d.ts +8 -1
  138. package/dist/provider-onboarding.d.ts.map +1 -1
  139. package/dist/provider-onboarding.js +160 -40
  140. package/dist/provider-onboarding.js.map +1 -1
  141. package/dist/rpc-entry.js +1 -1
  142. package/dist/rpc-entry.js.map +1 -1
  143. package/dist/startup.d.ts +34 -3
  144. package/dist/startup.d.ts.map +1 -1
  145. package/dist/startup.js +407 -87
  146. package/dist/startup.js.map +1 -1
  147. package/package.json +7 -4
@@ -1,15 +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";
8
+ import { showInteractiveProviderOnboarding, } from "../provider-onboarding.js";
9
+ import { removeGlobalProviderApiKey, saveGlobalLocale } from "../startup.js";
6
10
  import { AutocompleteMenu, InteractiveChat, InteractiveComposer, InteractiveFooter, InteractiveHeader, } from "./interactive-components.js";
7
11
  import { InteractiveLayout } from "./interactive-layout.js";
8
12
  import { InteractiveProjection } from "./interactive-state.js";
13
+ import { TreeSelector } from "./tree-selector.js";
9
14
  export { InteractiveProjection } from "./interactive-state.js";
10
15
  const IMAGE_EXTENSIONS = new Set([".gif", ".jpeg", ".jpg", ".png", ".webp"]);
11
16
  const PASTE_IMAGE_KEY = process.platform === "win32" ? Key.alt("v") : Key.ctrl("v");
12
17
  const PASTE_IMAGE_SHORTCUT = process.platform === "win32" ? "Alt+V" : "Ctrl+V";
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
+ }
13
36
  function asImageAttachmentReference(pasted, cwd) {
14
37
  if (pasted.includes("\n"))
15
38
  return pasted;
@@ -37,6 +60,10 @@ function normalizeDroppedImagePrompt(prompt, cwd) {
37
60
  ? prompt
38
61
  : `${imageReference}${normalizeDroppedImagePrompt(prefix[2] ?? "", cwd)}`;
39
62
  }
63
+ function steeringCommandArgument(prompt) {
64
+ const match = /^\/steer(?:\s+([\s\S]*))?$/i.exec(prompt);
65
+ return match ? (match[1] ?? "").trim() : undefined;
66
+ }
40
67
  export class InteractiveMode {
41
68
  projection = new InteractiveProjection();
42
69
  root;
@@ -45,6 +72,8 @@ export class InteractiveMode {
45
72
  tui;
46
73
  sessionChoices;
47
74
  extensionHost;
75
+ providerOnboarding;
76
+ agentDir;
48
77
  readClipboardImagePath;
49
78
  clipboardDirectory;
50
79
  clipboardFiles = new Set();
@@ -56,6 +85,7 @@ export class InteractiveMode {
56
85
  started = false;
57
86
  lastFailedPrompt;
58
87
  queuedPrompts = [];
88
+ steeringPrompts = [];
59
89
  onExit;
60
90
  theme = "dark";
61
91
  overlay;
@@ -63,29 +93,22 @@ export class InteractiveMode {
63
93
  spinnerTimer;
64
94
  shutdownEmitted = false;
65
95
  preserveClipboardFiles = false;
96
+ locale;
66
97
  constructor(options) {
67
98
  this.session = options.session;
99
+ this.projection.configureFilePreview(this.session.allowedRoot, () => this.refresh());
68
100
  this.tui = options.tui;
69
101
  this.onExit = options.onExit;
70
102
  this.sessionChoices = [...(options.sessions ?? [])];
71
103
  this.extensionHost = options.extensionHost;
104
+ this.providerOnboarding = options.providerOnboarding;
105
+ this.agentDir = resolve(options.agentDir ?? join(homedir(), ".di-code"));
106
+ this.locale = options.locale ?? DEFAULT_LOCALE;
72
107
  this.readClipboardImagePath = options.readClipboardImagePath ?? readClipboardImagePath;
73
- this.clipboardDirectory = clipboardImageDirectory(this.session.allowedRoot);
74
- const commands = [
75
- { name: "help", description: "Show interactive commands" },
76
- { name: "clear", description: "Clear visible messages" },
77
- { name: "model", description: "Open the model selector" },
78
- { name: "session", description: "Open the session selector" },
79
- { name: "theme", description: "Open the theme selector" },
80
- { name: "settings", description: "Open the settings selector" },
81
- { name: "compact", description: "Compact the persisted context now" },
82
- { name: "usage", description: "Show token and cost usage" },
83
- { name: "retry", description: "Retry the last failed prompt" },
84
- ];
85
- const allCommands = [...commands, ...(this.extensionHost?.listCommands() ?? [])];
108
+ this.clipboardDirectory = clipboardImageDirectory(this.agentDir, this.session.allowedRoot);
86
109
  const autocomplete = {
87
- getSuggestions: (context, autocompleteOptions) => new CombinedAutocompleteProvider(allCommands, this.session.allowedRoot).getSuggestions(context, autocompleteOptions),
88
- applyCompletion: (context, item, prefix) => new CombinedAutocompleteProvider(allCommands, this.session.allowedRoot).applyCompletion(context, item, prefix),
110
+ getSuggestions: (context, autocompleteOptions) => new CombinedAutocompleteProvider(this.listSlashCommands(), this.session.allowedRoot).getSuggestions(context, autocompleteOptions),
111
+ applyCompletion: (context, item, prefix) => new CombinedAutocompleteProvider(this.listSlashCommands(), this.session.allowedRoot).applyCompletion(context, item, prefix),
89
112
  };
90
113
  this.editor = new Editor({
91
114
  maxHeight: 3,
@@ -106,8 +129,9 @@ export class InteractiveMode {
106
129
  this.editor.onAutocompleteChange = () => this.updateAutocompleteOverlay();
107
130
  const readViewState = () => ({
108
131
  ...this.projection.state,
109
- model: this.session.modelId,
132
+ model: `${this.session.modelId}${this.session.thinkingLevel ? `(${this.session.thinkingLevel})` : ""}`,
110
133
  theme: this.theme,
134
+ locale: this.locale,
111
135
  pasteImageShortcut: PASTE_IMAGE_SHORTCUT,
112
136
  });
113
137
  this.root = new InteractiveLayout({
@@ -129,7 +153,7 @@ export class InteractiveMode {
129
153
  this.projection.replaceTranscript(this.session.transcript);
130
154
  this.projection.setUsage(this.session.usage);
131
155
  this.subscribeToSession();
132
- void cleanupStaleClipboardImages(this.session.allowedRoot).catch((cause) => {
156
+ void cleanupStaleClipboardImages(this.agentDir, this.session.allowedRoot).catch((cause) => {
133
157
  this.projection.setError(cause instanceof Error ? cause.message : String(cause));
134
158
  this.refresh();
135
159
  });
@@ -170,6 +194,21 @@ export class InteractiveMode {
170
194
  this.onExit?.();
171
195
  }
172
196
  handleCommand(data) {
197
+ if (matchesKey(data, Key.alt("s"))) {
198
+ void this.submitSteering(this.editor.getValue());
199
+ return true;
200
+ }
201
+ if (data === "\x1b[Z") {
202
+ try {
203
+ const level = this.session.cycleThinkingLevel();
204
+ this.projection.setStatus(level ? `thinking=${level}` : "Current model does not support thinking levels.");
205
+ }
206
+ catch (cause) {
207
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
208
+ }
209
+ this.refresh();
210
+ return true;
211
+ }
173
212
  if (data === "\x14") {
174
213
  this.openThemeSelector();
175
214
  return true;
@@ -200,7 +239,7 @@ export class InteractiveMode {
200
239
  try {
201
240
  const path = await this.readClipboardImagePath(this.clipboardDirectory);
202
241
  if (!path) {
203
- this.projection.setStatus("Clipboard has no supported image.");
242
+ this.projection.setStatus(this.locale === "zh-CN" ? "剪贴板中没有受支持的图片。" : "Clipboard has no supported image.");
204
243
  }
205
244
  else {
206
245
  this.editor.insertTextAtCursor(path);
@@ -214,9 +253,10 @@ export class InteractiveMode {
214
253
  this.refresh();
215
254
  }
216
255
  openThemeSelector() {
256
+ const t = (key) => translate(this.locale, key);
217
257
  const list = new SelectList([
218
- { value: "dark", label: "Dark", description: "Dark terminal theme" },
219
- { value: "light", label: "Light", description: "Light terminal theme" },
258
+ { value: "dark", label: t("dark"), description: t("darkTheme") },
259
+ { value: "light", label: t("light"), description: t("lightTheme") },
220
260
  ]);
221
261
  list.onSelect = (item) => {
222
262
  this.theme = item.value === "light" ? "light" : "dark";
@@ -248,11 +288,12 @@ export class InteractiveMode {
248
288
  this.showOverlay(list);
249
289
  }
250
290
  openSessionSelector() {
291
+ const t = (key) => translate(this.locale, key);
251
292
  const list = new SelectList([
252
293
  {
253
294
  value: "__current__",
254
- label: "Current session",
255
- description: this.session.sessionFile ?? "In-memory session",
295
+ label: t("currentSession"),
296
+ description: this.session.sessionFile ?? t("inMemorySession"),
256
297
  },
257
298
  ...this.sessionChoices.map((choice) => ({
258
299
  value: choice.id,
@@ -276,28 +317,124 @@ export class InteractiveMode {
276
317
  list.onCancel = () => this.closeOverlay();
277
318
  this.showOverlay(list);
278
319
  }
320
+ openTreeSelector() {
321
+ if (this.promptInFlight || this.compactionInFlight || this.sessionSwitching) {
322
+ this.projection.setError(this.locale === "zh-CN"
323
+ ? "提示词运行时不能浏览会话树。"
324
+ : "Cannot browse the session tree while a prompt is running.");
325
+ this.refresh();
326
+ return;
327
+ }
328
+ if (this.session.sessionTree.length === 0) {
329
+ this.projection.setStatus(this.locale === "zh-CN" ? "当前会话还没有历史节点。" : "The current session has no history nodes.");
330
+ this.refresh();
331
+ return;
332
+ }
333
+ const selector = new TreeSelector({
334
+ nodes: this.session.sessionTree,
335
+ leafId: this.session.sessionLeafId,
336
+ locale: this.locale,
337
+ onContinue: (entry) => {
338
+ this.closeOverlay();
339
+ void this.navigateTreeEntry(entry.id);
340
+ },
341
+ onEdit: (entry) => {
342
+ if (entry.type !== "message" || entry.message.role !== "user") {
343
+ this.projection.setError(this.locale === "zh-CN" ? "只能编辑历史用户消息。" : "Only historical user messages can be edited.");
344
+ this.refresh();
345
+ return;
346
+ }
347
+ this.closeOverlay();
348
+ void this.navigateTreeEntry(entry.id);
349
+ },
350
+ onSummarize: (entry) => {
351
+ this.closeOverlay();
352
+ void this.summarizeTreeBranch(entry.id);
353
+ },
354
+ onCancel: () => this.closeOverlay(),
355
+ });
356
+ this.showOverlay(selector, true, "80%");
357
+ }
358
+ async navigateTreeEntry(entryId) {
359
+ try {
360
+ const result = await this.session.navigateTree(entryId);
361
+ if (!this.started)
362
+ return false;
363
+ this.projection.replaceTranscript(this.session.transcript);
364
+ this.editor.setValue(result.editorText ?? "");
365
+ this.projection.setStatus(result.imagesOmitted
366
+ ? this.locale === "zh-CN"
367
+ ? "已恢复文本;请重新附加图片。"
368
+ : "Text restored; reattach images before sending."
369
+ : `tree=${result.selectedEntryId}`);
370
+ return true;
371
+ }
372
+ catch (cause) {
373
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
374
+ return false;
375
+ }
376
+ finally {
377
+ this.refresh();
378
+ }
379
+ }
380
+ async summarizeTreeBranch(entryId) {
381
+ if (this.compactionInFlight || this.promptInFlight)
382
+ return;
383
+ this.compactionInFlight = true;
384
+ this.projection.setStatus(this.locale === "zh-CN" ? "正在为分支生成摘要。" : "Summarizing selected branch.");
385
+ this.refresh();
386
+ try {
387
+ if (!(await this.navigateTreeEntry(entryId)))
388
+ return;
389
+ await this.session.compact();
390
+ this.editor.setValue("");
391
+ this.projection.replaceTranscript(this.session.transcript);
392
+ this.projection.setStatus(this.locale === "zh-CN" ? "摘要分支已创建。" : "Summary branch created.");
393
+ }
394
+ catch (cause) {
395
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
396
+ }
397
+ finally {
398
+ this.compactionInFlight = false;
399
+ this.refresh();
400
+ }
401
+ }
279
402
  openSettingsSelector() {
403
+ const t = (key) => translate(this.locale, key);
280
404
  const list = new SettingsList([
281
405
  {
282
406
  id: "compaction",
283
- label: "Context compaction",
284
- currentValue: this.session.compactionEnabled ? "on" : "off",
285
- values: ["on", "off"],
286
- description: "Summarize older persisted context before the model limit is reached.",
407
+ label: t("contextCompaction"),
408
+ currentValue: this.session.compactionEnabled ? t("on") : t("off"),
409
+ values: [t("on"), t("off")],
287
410
  },
288
- ]);
411
+ {
412
+ id: "locale",
413
+ label: t("language"),
414
+ currentValue: this.locale,
415
+ values: ["en", "zh-CN"],
416
+ },
417
+ ], { title: t("settings") });
289
418
  list.onChange = (id, value) => {
419
+ if (id === "locale") {
420
+ this.locale = value === "zh-CN" ? "zh-CN" : "en";
421
+ void saveGlobalLocale(this.agentDir, this.locale).catch((cause) => {
422
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
423
+ this.refresh();
424
+ });
425
+ this.closeOverlay();
426
+ this.openSettingsSelector();
427
+ return;
428
+ }
290
429
  if (id !== "compaction")
291
430
  return;
292
431
  try {
293
- const enabled = this.session.setCompactionEnabled(value === "on");
294
- list.updateValue(id, enabled ? "on" : "off");
295
- this.projection.setStatus(enabled === (value === "on")
296
- ? `compaction=${enabled ? "on" : "off"}`
297
- : "compaction unavailable without a persisted session");
432
+ const enabled = this.session.setCompactionEnabled(value === t("on"));
433
+ list.updateValue(id, enabled ? t("on") : t("off"));
434
+ this.projection.setStatus(enabled === (value === t("on")) ? `compaction=${enabled ? t("on") : t("off")}` : t("compactionUnavailable"));
298
435
  }
299
436
  catch (cause) {
300
- list.updateValue(id, this.session.compactionEnabled ? "on" : "off");
437
+ list.updateValue(id, this.session.compactionEnabled ? t("on") : t("off"));
301
438
  this.projection.setError(cause instanceof Error ? cause.message : String(cause));
302
439
  this.closeOverlay();
303
440
  }
@@ -306,15 +443,16 @@ export class InteractiveMode {
306
443
  list.onCancel = () => this.closeOverlay();
307
444
  this.showOverlay(list);
308
445
  }
309
- showOverlay(component) {
446
+ showOverlay(component, preserveLastLine = false, maxHeight = "60%") {
310
447
  this.editor.cancelAutocomplete();
311
448
  this.closeAutocompleteOverlay();
312
449
  this.closeOverlay();
313
450
  this.overlay = this.tui.showOverlay(component, {
314
451
  width: "70%",
315
- maxHeight: "60%",
452
+ maxHeight,
316
453
  anchor: "center",
317
454
  margin: 1,
455
+ preserveLastLine,
318
456
  });
319
457
  }
320
458
  closeOverlay() {
@@ -331,11 +469,18 @@ export class InteractiveMode {
331
469
  items: this.editor.getAutocompleteItems(),
332
470
  index: this.editor.getAutocompleteIndex(),
333
471
  }));
472
+ const editorBounds = this.root.getEditorBounds(this.tui.columns);
334
473
  this.autocompleteOverlay = this.tui.showOverlay(menu, {
335
474
  width: "55%",
336
- maxHeight: 8,
475
+ maxHeight: 9,
337
476
  anchor: "bottom-left",
477
+ placement: {
478
+ anchorRow: editorBounds.end - 1,
479
+ avoidStartRow: editorBounds.start,
480
+ preferred: "below",
481
+ },
338
482
  margin: 1,
483
+ preserveLastLine: true,
339
484
  nonCapturing: true,
340
485
  });
341
486
  this.refresh();
@@ -366,13 +511,24 @@ export class InteractiveMode {
366
511
  this.unsubscribeSession = this.session.subscribeSession((event) => {
367
512
  if (!this.started)
368
513
  return;
514
+ if (event.type === "tree_navigated") {
515
+ this.projection.replaceTranscript(this.session.transcript);
516
+ this.refresh();
517
+ return;
518
+ }
519
+ if (event.type === "queue_update") {
520
+ this.steeringPrompts = [...event.steering];
521
+ this.refreshQueue();
522
+ this.refresh();
523
+ return;
524
+ }
369
525
  this.projection.apply(event);
370
526
  this.refresh();
371
527
  });
372
528
  }
373
529
  async switchSession(choice) {
374
530
  if (this.promptInFlight || this.sessionSwitching) {
375
- this.projection.setError("Cannot switch sessions while a prompt is running.");
531
+ this.projection.setError(this.locale === "zh-CN" ? "提示词运行时不能切换会话。" : "Cannot switch sessions while a prompt is running.");
376
532
  this.refresh();
377
533
  return;
378
534
  }
@@ -385,10 +541,12 @@ export class InteractiveMode {
385
541
  return;
386
542
  this.unsubscribeSession?.();
387
543
  this.session = next;
388
- this.clipboardDirectory = clipboardImageDirectory(next.allowedRoot);
389
- void cleanupStaleClipboardImages(next.allowedRoot);
544
+ this.projection.configureFilePreview(next.allowedRoot, () => this.refresh());
545
+ this.clipboardDirectory = clipboardImageDirectory(this.agentDir, next.allowedRoot);
546
+ void cleanupStaleClipboardImages(this.agentDir, next.allowedRoot);
390
547
  this.queuedPrompts = [];
391
- this.projection.setQueue([]);
548
+ this.steeringPrompts = [];
549
+ this.refreshQueue();
392
550
  this.projection.replaceTranscript(next.transcript);
393
551
  this.projection.setUsage(next.usage);
394
552
  this.projection.setStatus(`session=${choice.id}`);
@@ -406,24 +564,33 @@ export class InteractiveMode {
406
564
  const prompt = text.trim();
407
565
  if (prompt.length === 0 || !this.started)
408
566
  return;
409
- if (prompt.startsWith("/")) {
567
+ const steeringArgument = steeringCommandArgument(prompt);
568
+ if (steeringArgument !== undefined) {
569
+ await this.submitSteering(steeringArgument);
570
+ return;
571
+ }
572
+ if (prompt.startsWith("/") && !this.isLoadedSkillCommand(prompt)) {
410
573
  this.editor.setValue("");
411
574
  this.handleSlashCommand(prompt);
412
575
  return;
413
576
  }
414
577
  if (this.sessionSwitching) {
415
- this.projection.setError("A session is opening; wait before submitting a prompt.");
578
+ this.projection.setError(this.locale === "zh-CN"
579
+ ? "会话正在打开,请等待后再提交提示词。"
580
+ : "A session is opening; wait before submitting a prompt.");
416
581
  this.refresh();
417
582
  return;
418
583
  }
419
584
  if (this.compactionInFlight) {
420
- this.projection.setError("A compaction is running; wait before submitting a prompt.");
585
+ this.projection.setError(this.locale === "zh-CN"
586
+ ? "正在压缩上下文,请等待后再提交提示词。"
587
+ : "A compaction is running; wait before submitting a prompt.");
421
588
  this.refresh();
422
589
  return;
423
590
  }
424
591
  if (this.promptInFlight) {
425
592
  this.queuedPrompts.push(prompt);
426
- this.projection.setQueue(this.queuedPrompts);
593
+ this.refreshQueue();
427
594
  this.root.invalidate();
428
595
  this.tui.requestRender();
429
596
  return;
@@ -453,13 +620,57 @@ export class InteractiveMode {
453
620
  this.activeAbort = undefined;
454
621
  this.projection.setRetrying(false);
455
622
  const next = this.queuedPrompts.shift();
456
- this.projection.setQueue(this.queuedPrompts);
623
+ this.refreshQueue();
457
624
  this.root.invalidate();
458
625
  this.tui.requestRender();
459
626
  if (next && this.started)
460
627
  void this.submit(next);
461
628
  }
462
629
  }
630
+ async submitSteering(text) {
631
+ const prompt = text.trim();
632
+ if (prompt.length === 0) {
633
+ this.projection.setError(this.locale === "zh-CN" ? "引导内容不能为空。" : "Steering content must not be empty.");
634
+ this.refresh();
635
+ return;
636
+ }
637
+ if (this.sessionSwitching) {
638
+ this.projection.setError(this.locale === "zh-CN"
639
+ ? "会话正在打开,请等待后再引导 Agent。"
640
+ : "A session is opening; wait before steering the agent.");
641
+ this.refresh();
642
+ return;
643
+ }
644
+ if (this.compactionInFlight) {
645
+ this.projection.setError(this.locale === "zh-CN"
646
+ ? "正在压缩上下文,请等待后再引导 Agent。"
647
+ : "A compaction is running; wait before steering the agent.");
648
+ this.refresh();
649
+ return;
650
+ }
651
+ if (!this.promptInFlight) {
652
+ this.projection.setError(this.locale === "zh-CN"
653
+ ? "只能在提示词运行时引导 Agent。"
654
+ : "Steering is only available while a prompt is running.");
655
+ this.refresh();
656
+ return;
657
+ }
658
+ try {
659
+ const input = await extractImageAttachments(normalizeDroppedImagePrompt(prompt, this.session.allowedRoot), this.session.allowedRoot);
660
+ await this.session.steerWithImages(input.text, input.images);
661
+ this.editor.setValue("");
662
+ }
663
+ catch (cause) {
664
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
665
+ }
666
+ this.refresh();
667
+ }
668
+ refreshQueue() {
669
+ this.projection.setQueue([
670
+ ...this.steeringPrompts.map((prompt) => `steer: ${prompt}`),
671
+ ...this.queuedPrompts.map((prompt) => `queued: ${prompt}`),
672
+ ]);
673
+ }
463
674
  async cleanupUnreferencedClipboardFiles() {
464
675
  const text = this.editor.getValue();
465
676
  const files = [...this.clipboardFiles].filter((path) => !text.includes(path));
@@ -480,11 +691,13 @@ export class InteractiveMode {
480
691
  const args = argParts.join(" ");
481
692
  switch (command) {
482
693
  case "help":
483
- this.projection.setStatus("commands: /clear /model /session /theme /settings /compact /usage /retry");
694
+ this.projection.setStatus(`commands: ${this.listSlashCommands()
695
+ .map((item) => `/${item.name}`)
696
+ .join(" ")}`);
484
697
  break;
485
698
  case "clear":
486
699
  this.projection.clearVisibleMessages();
487
- this.projection.setStatus("visible messages cleared");
700
+ this.projection.setStatus(translate(this.locale, "visibleMessagesCleared"));
488
701
  break;
489
702
  case "model":
490
703
  this.openModelSelector();
@@ -492,12 +705,21 @@ export class InteractiveMode {
492
705
  case "session":
493
706
  this.openSessionSelector();
494
707
  return;
708
+ case "tree":
709
+ this.openTreeSelector();
710
+ return;
495
711
  case "theme":
496
712
  this.openThemeSelector();
497
713
  return;
498
714
  case "settings":
499
715
  this.openSettingsSelector();
500
716
  return;
717
+ case "login":
718
+ this.openProviderLogin();
719
+ return;
720
+ case "logout":
721
+ void this.logoutProvider();
722
+ return;
501
723
  case "compact":
502
724
  void this.runManualCompaction();
503
725
  return;
@@ -508,7 +730,7 @@ export class InteractiveMode {
508
730
  if (this.lastFailedPrompt && !this.promptInFlight)
509
731
  void this.submit(this.lastFailedPrompt, true);
510
732
  else
511
- this.projection.setStatus("nothing to retry");
733
+ this.projection.setStatus(translate(this.locale, "nothingToRetry"));
512
734
  break;
513
735
  default: {
514
736
  if (this.extensionHost?.listCommands().some((entry) => entry.name === command)) {
@@ -523,22 +745,99 @@ export class InteractiveMode {
523
745
  }
524
746
  this.refresh();
525
747
  }
748
+ openProviderLogin() {
749
+ if (!this.providerOnboarding) {
750
+ this.projection.setError("Provider login is unavailable in this session.");
751
+ this.refresh();
752
+ return;
753
+ }
754
+ if (this.promptInFlight || this.sessionSwitching) {
755
+ this.projection.setError("Cannot change Provider while a prompt is running.");
756
+ this.refresh();
757
+ return;
758
+ }
759
+ this.editor.cancelAutocomplete();
760
+ this.closeAutocompleteOverlay();
761
+ void showInteractiveProviderOnboarding({
762
+ ...this.providerOnboarding,
763
+ configuration: { ...this.providerOnboarding.configuration, locale: this.locale },
764
+ tui: this.tui,
765
+ })
766
+ .then((runtime) => {
767
+ if (!this.started)
768
+ return;
769
+ this.tui.setFocus(this.editor);
770
+ if (!runtime) {
771
+ this.refresh();
772
+ return;
773
+ }
774
+ this.session.setRuntime(runtime.provider, runtime.model);
775
+ this.projection.setStatus(`provider=${runtime.provider.id} model=${runtime.model.id}`);
776
+ this.refresh();
777
+ })
778
+ .catch((cause) => {
779
+ if (this.started)
780
+ this.tui.setFocus(this.editor);
781
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
782
+ this.refresh();
783
+ });
784
+ }
785
+ async logoutProvider() {
786
+ if (!this.providerOnboarding) {
787
+ this.projection.setError("Provider logout is unavailable in this session.");
788
+ this.refresh();
789
+ return;
790
+ }
791
+ if (this.promptInFlight || this.sessionSwitching) {
792
+ this.projection.setError("Cannot change Provider while a prompt is running.");
793
+ this.refresh();
794
+ return;
795
+ }
796
+ const providerId = this.session.providerId;
797
+ try {
798
+ const removed = await removeGlobalProviderApiKey(this.providerOnboarding.agentDir, providerId);
799
+ if (!removed) {
800
+ this.projection.setStatus(`no global API key stored for provider=${providerId}`);
801
+ this.refresh();
802
+ return;
803
+ }
804
+ this.projection.setStatus(`global API key removed for provider=${providerId}; it remains active until this session ends`);
805
+ }
806
+ catch (cause) {
807
+ this.projection.setError(cause instanceof Error ? cause.message : String(cause));
808
+ }
809
+ this.refresh();
810
+ }
811
+ listSlashCommands() {
812
+ const skills = this.session.availableSkills.map((skill) => ({
813
+ name: `skill:${skill.name}`,
814
+ description: skill.description,
815
+ }));
816
+ return [...builtinSlashCommands(this.locale), ...(this.extensionHost?.listCommands() ?? []), ...skills];
817
+ }
818
+ isLoadedSkillCommand(input) {
819
+ const name = /^\/skill:([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s|$)/.exec(input)?.[1];
820
+ return name !== undefined && this.session.availableSkills.some((skill) => skill.name === name);
821
+ }
526
822
  formatUsage() {
527
823
  const usage = this.session.usage;
824
+ if (this.locale === "zh-CN") {
825
+ return `用量:请求=${usage.requestCount} 输入=${usage.inputTokens} 输出=${usage.outputTokens} 总计=${usage.totalTokens} 费用=$${usage.cost.total.toFixed(4)} 上下文=${usage.estimatedContextTokens}/${usage.contextWindow}`;
826
+ }
528
827
  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}`;
529
828
  }
530
829
  async runManualCompaction() {
531
830
  if (this.promptInFlight || this.compactionInFlight) {
532
- this.projection.setError("Cannot compact while a prompt is running.");
831
+ this.projection.setError(this.locale === "zh-CN" ? "提示词运行时不能压缩上下文。" : "Cannot compact while a prompt is running.");
533
832
  this.refresh();
534
833
  return;
535
834
  }
536
835
  this.compactionInFlight = true;
537
- this.projection.setStatus("starting manual compaction");
836
+ this.projection.setStatus(translate(this.locale, "manualCompactionStarting"));
538
837
  this.refresh();
539
838
  try {
540
839
  await this.session.compact();
541
- this.projection.setStatus("manual compaction complete");
840
+ this.projection.setStatus(translate(this.locale, "manualCompactionComplete"));
542
841
  }
543
842
  catch (cause) {
544
843
  this.projection.setError(cause instanceof Error ? cause.message : String(cause));