@gajae-code/coding-agent 0.8.1 → 0.9.0

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 (253) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/completion-cli.d.ts +41 -0
  5. package/dist/types/cli/config-cli.d.ts +1 -0
  6. package/dist/types/cli/fixture-report.d.ts +20 -0
  7. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  8. package/dist/types/cli/nofile-limit.d.ts +11 -0
  9. package/dist/types/cli/notify-cli.d.ts +7 -0
  10. package/dist/types/cli/update-cli.d.ts +22 -0
  11. package/dist/types/cli.d.ts +124 -1
  12. package/dist/types/commands/completion.d.ts +29 -0
  13. package/dist/types/commands/config.d.ts +3 -0
  14. package/dist/types/commands/deep-interview.d.ts +3 -0
  15. package/dist/types/config/file-lock.d.ts +7 -6
  16. package/dist/types/config/keybindings.d.ts +2 -2
  17. package/dist/types/config/model-profile-activation.d.ts +6 -0
  18. package/dist/types/config/settings-schema.d.ts +105 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
  26. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  27. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/deep-interview-state.d.ts +17 -2
  29. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  30. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  31. package/dist/types/gjc-runtime/team-runtime.d.ts +28 -1
  32. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +120 -13
  34. package/dist/types/goals/runtime.d.ts +1 -0
  35. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  36. package/dist/types/main.d.ts +4 -1
  37. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  38. package/dist/types/modes/components/model-selector.d.ts +4 -0
  39. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  40. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  41. package/dist/types/modes/components/status-line.d.ts +9 -0
  42. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  43. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  45. package/dist/types/modes/interactive-mode.d.ts +2 -0
  46. package/dist/types/modes/print-mode.d.ts +13 -1
  47. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  48. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  49. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  50. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  51. package/dist/types/modes/types.d.ts +1 -0
  52. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  53. package/dist/types/notifications/config.d.ts +14 -1
  54. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  55. package/dist/types/notifications/index.d.ts +6 -3
  56. package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
  57. package/dist/types/notifications/telegram-daemon.d.ts +23 -3
  58. package/dist/types/notifications/telegram-reference.d.ts +0 -5
  59. package/dist/types/notifications/threaded-render.d.ts +6 -0
  60. package/dist/types/runtime-api-key.d.ts +7 -0
  61. package/dist/types/sdk.d.ts +3 -0
  62. package/dist/types/session/agent-session.d.ts +53 -1
  63. package/dist/types/session/agent-storage.d.ts +1 -0
  64. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  65. package/dist/types/session/messages.d.ts +4 -0
  66. package/dist/types/session/session-manager.d.ts +17 -1
  67. package/dist/types/skill-state/active-state.d.ts +18 -1
  68. package/dist/types/system-prompt.d.ts +18 -2
  69. package/dist/types/task/executor.d.ts +3 -0
  70. package/dist/types/task/fork-context-budget.d.ts +7 -0
  71. package/dist/types/task/token-log.d.ts +30 -0
  72. package/dist/types/task/token-log.test.d.ts +1 -0
  73. package/dist/types/tools/bisect.d.ts +81 -0
  74. package/dist/types/tools/computer.d.ts +2 -2
  75. package/dist/types/tools/index.d.ts +1 -0
  76. package/dist/types/tools/search.d.ts +2 -2
  77. package/dist/types/utils/file-mentions.d.ts +14 -0
  78. package/dist/types/utils/git.d.ts +32 -0
  79. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  80. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  81. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  82. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  83. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  84. package/package.json +7 -7
  85. package/src/append-only-mode.ts +58 -0
  86. package/src/autoresearch/prompt.md +1 -1
  87. package/src/autoresearch/tools/log-experiment.ts +1 -1
  88. package/src/capability/rule.ts +9 -0
  89. package/src/cli/completion-cli.ts +234 -0
  90. package/src/cli/config-cli.ts +61 -9
  91. package/src/cli/fast-help.ts +1 -0
  92. package/src/cli/fixture-report.test.ts +230 -0
  93. package/src/cli/fixture-report.ts +171 -0
  94. package/src/cli/nofile-limit.ts +48 -0
  95. package/src/cli/notify-cli.ts +91 -7
  96. package/src/cli/update-cli.ts +85 -9
  97. package/src/cli.ts +29 -1
  98. package/src/commands/completion.ts +84 -0
  99. package/src/commands/config.ts +2 -0
  100. package/src/commands/contribution-prep.ts +1 -1
  101. package/src/commands/deep-interview.ts +2 -1
  102. package/src/config/file-lock.ts +73 -34
  103. package/src/config/keybindings.ts +2 -2
  104. package/src/config/model-profile-activation.ts +65 -0
  105. package/src/config/model-registry.ts +14 -2
  106. package/src/config/settings-schema.ts +68 -1
  107. package/src/config/settings.ts +22 -0
  108. package/src/coordinator-mcp/server.ts +34 -8
  109. package/src/defaults/gjc/skills/deep-interview/SKILL.md +50 -18
  110. package/src/defaults/gjc/skills/ralplan/SKILL.md +40 -99
  111. package/src/defaults/gjc/skills/team/SKILL.md +17 -14
  112. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  113. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  114. package/src/defaults/gjc-defaults.ts +9 -0
  115. package/src/discovery/helpers.ts +10 -0
  116. package/src/discovery/index.ts +1 -0
  117. package/src/export/html/template.generated.ts +1 -1
  118. package/src/export/html/template.js +60 -2
  119. package/src/export/ttsr.ts +96 -18
  120. package/src/extensibility/extensions/runner.ts +183 -219
  121. package/src/extensibility/extensions/types.ts +9 -0
  122. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  123. package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
  124. package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
  125. package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
  126. package/src/gjc-runtime/deep-interview-state.ts +44 -2
  127. package/src/gjc-runtime/session-state-sidecar.ts +263 -20
  128. package/src/gjc-runtime/state-runtime.ts +12 -2
  129. package/src/gjc-runtime/state-writer.ts +15 -0
  130. package/src/gjc-runtime/team-runtime.ts +187 -15
  131. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  132. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  133. package/src/gjc-runtime/ultragoal-runtime.ts +1663 -159
  134. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  135. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  136. package/src/goals/runtime.ts +10 -2
  137. package/src/hindsight/backend.ts +1 -3
  138. package/src/hooks/native-skill-hook.ts +124 -4
  139. package/src/internal-urls/docs-index.generated.ts +16 -7
  140. package/src/main.ts +95 -43
  141. package/src/memories/index.ts +12 -1
  142. package/src/modes/components/assistant-message.ts +241 -82
  143. package/src/modes/components/custom-editor.ts +1 -0
  144. package/src/modes/components/model-selector.ts +54 -11
  145. package/src/modes/components/queued-message-selector.ts +92 -0
  146. package/src/modes/components/settings-selector.ts +4 -0
  147. package/src/modes/components/status-line.ts +167 -54
  148. package/src/modes/components/tool-execution.ts +44 -19
  149. package/src/modes/components/welcome.ts +160 -44
  150. package/src/modes/controllers/command-controller.ts +40 -4
  151. package/src/modes/controllers/event-controller.ts +66 -47
  152. package/src/modes/controllers/extension-ui-controller.ts +4 -10
  153. package/src/modes/controllers/input-controller.ts +180 -18
  154. package/src/modes/controllers/selector-controller.ts +122 -27
  155. package/src/modes/interactive-mode.ts +51 -16
  156. package/src/modes/print-mode.ts +43 -4
  157. package/src/modes/prompt-action-autocomplete.ts +21 -20
  158. package/src/modes/rpc/rpc-mode.ts +29 -3
  159. package/src/modes/rpc/rpc-types.ts +9 -0
  160. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  161. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  162. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  163. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  164. package/src/modes/types.ts +1 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  166. package/src/modes/utils/ui-helpers.ts +19 -6
  167. package/src/notifications/config.ts +25 -5
  168. package/src/notifications/daemon-paths.ts +2 -0
  169. package/src/notifications/index.ts +117 -14
  170. package/src/notifications/lifecycle-commands.ts +37 -7
  171. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  172. package/src/notifications/telegram-daemon-cli.ts +2 -2
  173. package/src/notifications/telegram-daemon-control.ts +5 -3
  174. package/src/notifications/telegram-daemon.ts +302 -51
  175. package/src/notifications/telegram-reference.ts +0 -16
  176. package/src/notifications/threaded-render.ts +17 -1
  177. package/src/prompts/agents/architect.md +19 -10
  178. package/src/prompts/agents/critic.md +16 -18
  179. package/src/prompts/agents/executor.md +14 -11
  180. package/src/prompts/agents/planner.md +18 -13
  181. package/src/prompts/ci-green-request.md +3 -8
  182. package/src/prompts/goals/goal-continuation.md +0 -5
  183. package/src/prompts/goals/goal-mode-active.md +0 -5
  184. package/src/prompts/system/agent-creation-architect.md +2 -2
  185. package/src/prompts/system/plan-mode-active.md +1 -1
  186. package/src/prompts/system/plan-mode-subagent.md +3 -3
  187. package/src/prompts/system/project-prompt.md +0 -13
  188. package/src/prompts/system/subagent-system-prompt.md +3 -2
  189. package/src/prompts/system/system-prompt.md +29 -19
  190. package/src/prompts/system/volatile-project-context.md +10 -0
  191. package/src/prompts/system/web-search.md +4 -7
  192. package/src/prompts/tools/apply-patch.md +6 -0
  193. package/src/prompts/tools/ast-edit.md +1 -0
  194. package/src/prompts/tools/bash.md +1 -1
  195. package/src/prompts/tools/bisect.md +22 -0
  196. package/src/prompts/tools/browser.md +1 -1
  197. package/src/prompts/tools/computer.md +2 -2
  198. package/src/prompts/tools/cron.md +0 -2
  199. package/src/prompts/tools/eval.md +2 -2
  200. package/src/prompts/tools/find.md +1 -1
  201. package/src/prompts/tools/github.md +6 -6
  202. package/src/prompts/tools/goal.md +1 -3
  203. package/src/prompts/tools/image-gen.md +12 -5
  204. package/src/prompts/tools/irc.md +5 -4
  205. package/src/prompts/tools/job.md +2 -2
  206. package/src/prompts/tools/lsp.md +1 -1
  207. package/src/prompts/tools/monitor.md +2 -2
  208. package/src/prompts/tools/patch.md +2 -2
  209. package/src/prompts/tools/replace.md +2 -21
  210. package/src/prompts/tools/rewind.md +1 -1
  211. package/src/prompts/tools/search-tool-bm25.md +1 -11
  212. package/src/prompts/tools/search.md +1 -1
  213. package/src/prompts/tools/skill.md +3 -2
  214. package/src/prompts/tools/ssh.md +2 -1
  215. package/src/prompts/tools/subagent.md +2 -2
  216. package/src/prompts/tools/task.md +4 -5
  217. package/src/prompts/tools/todo-write.md +1 -1
  218. package/src/prompts/tools/vim.md +1 -0
  219. package/src/prompts/tools/web-search.md +2 -0
  220. package/src/prompts/tools/write.md +8 -2
  221. package/src/runtime-api-key.ts +16 -0
  222. package/src/sdk.ts +52 -28
  223. package/src/session/agent-session.ts +951 -132
  224. package/src/session/agent-storage.ts +4 -0
  225. package/src/session/file-mention-pruning.ts +64 -0
  226. package/src/session/messages.ts +4 -0
  227. package/src/session/session-manager.ts +103 -4
  228. package/src/skill-state/active-state.ts +141 -5
  229. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  230. package/src/slash-commands/builtin-registry.ts +218 -71
  231. package/src/system-prompt.ts +57 -16
  232. package/src/task/executor.ts +132 -24
  233. package/src/task/fork-context-advisory.ts +2 -7
  234. package/src/task/fork-context-budget.ts +9 -0
  235. package/src/task/index.ts +13 -6
  236. package/src/task/output-manager.ts +20 -8
  237. package/src/task/receipt.ts +26 -7
  238. package/src/task/token-log.test.ts +188 -0
  239. package/src/task/token-log.ts +179 -0
  240. package/src/tools/bisect.ts +367 -0
  241. package/src/tools/computer.ts +246 -43
  242. package/src/tools/index.ts +3 -0
  243. package/src/tools/output-meta.ts +116 -4
  244. package/src/tools/search.ts +8 -2
  245. package/src/tools/skill.ts +15 -0
  246. package/src/tools/telegram-send.ts +6 -0
  247. package/src/utils/file-mentions.ts +39 -7
  248. package/src/utils/git.ts +73 -0
  249. package/src/web/scrapers/youtube.ts +24 -12
  250. package/src/workflow/workflow-intent-diff.ts +29 -6
  251. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  252. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  253. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -55,6 +55,15 @@ function elideRunawayThinkingRepetition(text: string): string {
55
55
  return text;
56
56
  }
57
57
 
58
+ interface AssistantMessageUpdateOptions {
59
+ streaming?: boolean;
60
+ }
61
+
62
+ type AssistantChildDescriptor = {
63
+ key: string;
64
+ component: Component;
65
+ };
66
+
58
67
  /**
59
68
  * Component that renders a complete assistant message
60
69
  */
@@ -67,6 +76,11 @@ export class AssistantMessageComponent extends Container {
67
76
  #kittyConversionsInFlight = new Set<string>();
68
77
  #responseHeader = new Text(theme.bold(theme.fg("statusLineModel", "gajae")), 1, 0);
69
78
  #contentBlocksCache = new WeakMap<object, { source: string; component: Component }>();
79
+ #lastStreaming = false;
80
+ #childComponents = new Map<string, Component>();
81
+ #contentBlockKeys = new WeakMap<object, string>();
82
+ #nextContentBlockKey = 0;
83
+ #reusableChildren = new WeakSet<Component>();
70
84
 
71
85
  constructor(
72
86
  message?: AssistantMessage,
@@ -87,7 +101,7 @@ export class AssistantMessageComponent extends Container {
87
101
  override invalidate(): void {
88
102
  super.invalidate();
89
103
  if (this.#lastMessage) {
90
- this.updateContent(this.#lastMessage);
104
+ this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
91
105
  }
92
106
  }
93
107
 
@@ -115,7 +129,7 @@ export class AssistantMessageComponent extends Container {
115
129
  this.#convertToolImagesForKitty(toolCallId, validImages);
116
130
  }
117
131
  if (this.#lastMessage) {
118
- this.updateContent(this.#lastMessage);
132
+ this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
119
133
  }
120
134
  }
121
135
 
@@ -138,7 +152,7 @@ export class AssistantMessageComponent extends Container {
138
152
  mimeType: "image/png",
139
153
  });
140
154
  if (this.#lastMessage) {
141
- this.updateContent(this.#lastMessage);
155
+ this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
142
156
  }
143
157
  this.onImageUpdate?.();
144
158
  })
@@ -151,153 +165,298 @@ export class AssistantMessageComponent extends Container {
151
165
  setUsageInfo(usage: Usage): void {
152
166
  this.#usageInfo = usage;
153
167
  if (this.#lastMessage) {
154
- this.updateContent(this.#lastMessage);
168
+ this.updateContent(this.#lastMessage, { streaming: this.#lastStreaming });
169
+ }
170
+ }
171
+
172
+ #contentBlockKey(content: object): string {
173
+ let key = this.#contentBlockKeys.get(content);
174
+ if (!key) {
175
+ key = `block:${this.#nextContentBlockKey++}`;
176
+ this.#contentBlockKeys.set(content, key);
155
177
  }
178
+ return key;
156
179
  }
157
180
 
158
- #renderTextBlock(content: { text: string }): Component {
181
+ #cachedChild<T extends Component>(key: string, create: () => T): T {
182
+ const cached = this.#childComponents.get(key);
183
+ if (cached) return cached as T;
184
+ const component = create();
185
+ this.#childComponents.set(key, component);
186
+ this.#reusableChildren.add(component);
187
+ return component;
188
+ }
189
+
190
+ #renderTextBlock(content: { text: string }, streaming: boolean): Component {
159
191
  const cached = this.#contentBlocksCache.get(content);
160
- if (cached?.source === content.text) return cached.component;
192
+ if (cached?.source === content.text) {
193
+ if (cached.component instanceof Markdown) {
194
+ cached.component.setOnStaleThrottle(this.onImageUpdate);
195
+ cached.component.setStreaming(streaming);
196
+ }
197
+ return cached.component;
198
+ }
161
199
  const trimmed = content.text.trim();
162
200
  const deepInterview = renderDeepInterviewAssistantText(trimmed, theme);
163
201
  // Reuse the same Markdown instance across streaming chunks (update text in place)
164
202
  // instead of constructing a new one each chunk; combined with the markdown
165
203
  // per-code-block highlight cache, appends no longer re-highlight the whole prefix.
166
204
  if (!deepInterview && cached && cached.component instanceof Markdown) {
167
- cached.component.setText(trimmed);
205
+ cached.component.setOnStaleThrottle(this.onImageUpdate);
206
+ cached.component.setText(trimmed, { streaming });
168
207
  cached.source = content.text;
169
208
  return cached.component;
170
209
  }
171
210
  const component = deepInterview ?? new Markdown(trimmed, 1, 0, getMarkdownTheme());
211
+ if (component instanceof Markdown) {
212
+ component.setOnStaleThrottle(this.onImageUpdate);
213
+ component.setStreaming(streaming);
214
+ }
172
215
  this.#contentBlocksCache.set(content, { source: content.text, component });
216
+ this.#reusableChildren.add(component);
173
217
  return component;
174
218
  }
175
219
 
176
- #renderThinkingBlock(content: { thinking: string }): Markdown {
220
+ #renderThinkingBlock(content: { thinking: string }, streaming: boolean): Markdown {
177
221
  const cached = this.#contentBlocksCache.get(content);
178
- if (cached?.source === content.thinking) return cached.component as Markdown;
222
+ if (cached?.source === content.thinking) {
223
+ if (cached.component instanceof Markdown) {
224
+ cached.component.setOnStaleThrottle(this.onImageUpdate);
225
+ cached.component.setStreaming(streaming);
226
+ }
227
+ return cached.component as Markdown;
228
+ }
179
229
  const trimmed = elideRunawayThinkingRepetition(content.thinking.trim());
230
+ if (cached?.component instanceof Markdown) {
231
+ cached.component.setOnStaleThrottle(this.onImageUpdate);
232
+ cached.component.setText(trimmed, { streaming });
233
+ cached.source = content.thinking;
234
+ return cached.component;
235
+ }
180
236
  const component = new Markdown(trimmed, 1, 0, getMarkdownTheme(), {
181
237
  color: (text: string) => theme.fg("thinkingText", text),
182
238
  italic: true,
183
239
  });
240
+ component.setOnStaleThrottle(this.onImageUpdate);
241
+ component.setStreaming(streaming);
184
242
  this.#contentBlocksCache.set(content, { source: content.thinking, component });
243
+ this.#reusableChildren.add(component);
185
244
  return component;
186
245
  }
187
246
 
188
- #renderToolImages(): void {
247
+ #toolImageDescriptors(): AssistantChildDescriptor[] {
189
248
  const imageEntries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
190
249
  images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
191
250
  );
192
- if (imageEntries.length === 0) return;
251
+ if (imageEntries.length === 0) return [];
193
252
 
194
- this.#contentContainer.addChild(new Spacer(1));
253
+ const descriptors: AssistantChildDescriptor[] = [
254
+ { key: "tool-images:spacer", component: this.#cachedChild("tool-images:spacer", () => new Spacer(1)) },
255
+ ];
195
256
  for (const { image, key } of imageEntries) {
196
257
  const displayImage =
197
258
  TERMINAL.imageProtocol === ImageProtocol.Kitty && image.mimeType !== "image/png"
198
259
  ? this.#convertedKittyImages.get(key)
199
260
  : image;
200
261
  if (TERMINAL.imageProtocol && displayImage) {
201
- this.#contentContainer.addChild(
202
- new Image(
203
- displayImage.data,
204
- displayImage.mimeType,
205
- { fallbackColor: (text: string) => theme.fg("toolOutput", text) },
206
- resolveImageOptions(),
262
+ const imageKey = `tool-image:${key}:${displayImage.mimeType}:${displayImage.data}`;
263
+ descriptors.push({
264
+ key: imageKey,
265
+ component: this.#cachedChild(
266
+ imageKey,
267
+ () =>
268
+ new Image(
269
+ displayImage.data,
270
+ displayImage.mimeType,
271
+ { fallbackColor: (text: string) => theme.fg("toolOutput", text) },
272
+ resolveImageOptions(),
273
+ ),
207
274
  ),
208
- );
275
+ });
209
276
  continue;
210
277
  }
211
- this.#contentContainer.addChild(new Text(theme.fg("toolOutput", `[Image: ${image.mimeType}]`), 1, 0));
278
+ const fallbackKey = `tool-image-fallback:${key}:${image.mimeType}`;
279
+ descriptors.push({
280
+ key: fallbackKey,
281
+ component: this.#cachedChild(
282
+ fallbackKey,
283
+ () => new Text(theme.fg("toolOutput", `[Image: ${image.mimeType}]`), 1, 0),
284
+ ),
285
+ });
212
286
  }
287
+ return descriptors;
213
288
  }
214
289
 
215
- updateContent(message: AssistantMessage): void {
216
- this.#lastMessage = message;
290
+ #reconcileChildren(descriptors: AssistantChildDescriptor[]): void {
291
+ const nextChildren = descriptors.map(descriptor => descriptor.component);
292
+ if (
293
+ nextChildren.length === this.#contentContainer.children.length &&
294
+ nextChildren.every((child, index) => child === this.#contentContainer.children[index])
295
+ ) {
296
+ return;
297
+ }
217
298
 
218
- // Clear content container
219
- this.#contentContainer.clear();
299
+ const nextSet = new Set(nextChildren);
300
+ for (const child of this.#contentContainer.children) {
301
+ if (!nextSet.has(child) && !this.#reusableChildren.has(child)) {
302
+ child.dispose?.();
303
+ }
304
+ }
305
+ this.#contentContainer.children = nextChildren;
306
+ }
220
307
 
221
- const hasVisibleContent = message.content.some(
222
- c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
223
- );
308
+ updateContent(message: AssistantMessage, options?: AssistantMessageUpdateOptions): void {
309
+ this.#lastMessage = message;
310
+ this.#lastStreaming = options?.streaming ?? false;
224
311
 
312
+ const visibleContentAfter = new Array<boolean>(message.content.length).fill(false);
313
+ let hasVisibleContentAfter = false;
314
+ let hasVisibleContent = false;
315
+ let activeContentIndex = -1;
316
+ for (let i = message.content.length - 1; i >= 0; i--) {
317
+ visibleContentAfter[i] = hasVisibleContentAfter;
318
+ const content = message.content[i];
319
+ const isVisible =
320
+ (content.type === "text" && Boolean(content.text.trim())) ||
321
+ (content.type === "thinking" && Boolean(content.thinking.trim()));
322
+ if (isVisible) {
323
+ hasVisibleContent = true;
324
+ hasVisibleContentAfter = true;
325
+ if (this.#lastStreaming && activeContentIndex === -1) {
326
+ activeContentIndex = i;
327
+ }
328
+ }
329
+ }
330
+
331
+ const streaming = this.#lastStreaming;
332
+ const descriptors: AssistantChildDescriptor[] = [];
225
333
  if (hasVisibleContent) {
226
- this.#contentContainer.addChild(new Spacer(1));
227
- this.#contentContainer.addChild(this.#responseHeader);
334
+ descriptors.push({
335
+ key: "response-header:spacer",
336
+ component: this.#cachedChild("response-header:spacer", () => new Spacer(1)),
337
+ });
338
+ descriptors.push({ key: "response-header", component: this.#responseHeader });
228
339
  }
229
340
 
230
341
  // Render content in order
231
342
  for (let i = 0; i < message.content.length; i++) {
232
343
  const content = message.content[i];
344
+ const blockKey = this.#contentBlockKey(content);
233
345
  if (content.type === "text" && content.text.trim()) {
234
- this.#contentContainer.addChild(this.#renderTextBlock(content));
346
+ descriptors.push({
347
+ key: `${blockKey}:text`,
348
+ component: this.#renderTextBlock(content, streaming && i === activeContentIndex),
349
+ });
235
350
  } else if (content.type === "thinking" && content.thinking.trim()) {
236
351
  // Add spacing only when another visible assistant content block follows.
237
352
  // This avoids a superfluous blank line before separately-rendered tool execution blocks.
238
- const hasVisibleContentAfter = message.content
239
- .slice(i + 1)
240
- .some(c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
241
-
242
353
  if (this.hideThinkingBlock) {
243
354
  // Show static "Thinking..." label when hidden
244
- this.#contentContainer.addChild(new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0));
245
- if (hasVisibleContentAfter) {
246
- this.#contentContainer.addChild(new Spacer(1));
247
- }
355
+ descriptors.push({
356
+ key: `${blockKey}:thinking-hidden`,
357
+ component: this.#cachedChild(
358
+ `${blockKey}:thinking-hidden`,
359
+ () => new Text(theme.italic(theme.fg("thinkingText", "Thinking...")), 1, 0),
360
+ ),
361
+ });
248
362
  } else {
249
- this.#contentContainer.addChild(this.#renderThinkingBlock(content));
250
- if (hasVisibleContentAfter) {
251
- this.#contentContainer.addChild(new Spacer(1));
252
- }
363
+ descriptors.push({
364
+ key: `${blockKey}:thinking`,
365
+ component: this.#renderThinkingBlock(content, streaming && i === activeContentIndex),
366
+ });
367
+ }
368
+ if (visibleContentAfter[i]) {
369
+ descriptors.push({
370
+ key: `${blockKey}:thinking-spacer`,
371
+ component: this.#cachedChild(`${blockKey}:thinking-spacer`, () => new Spacer(1)),
372
+ });
253
373
  }
254
374
  }
255
375
  }
256
376
 
257
- this.#renderToolImages();
258
- // Check if aborted - show after partial content
259
- // But only if there are no tool calls (tool execution components will show the error)
260
- const hasToolCalls = message.content.some(c => c.type === "toolCall");
261
- if (!hasToolCalls) {
262
- if (message.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
263
- const abortMessage =
264
- message.errorMessage && message.errorMessage !== "Request was aborted"
265
- ? message.errorMessage
266
- : "Operation aborted";
267
- if (hasVisibleContent) {
268
- this.#contentContainer.addChild(new Spacer(1));
269
- } else {
270
- this.#contentContainer.addChild(new Spacer(1));
377
+ descriptors.push(...this.#toolImageDescriptors());
378
+
379
+ const isTerminal = !streaming || Boolean(message.stopReason);
380
+ if (isTerminal) {
381
+ // Check if aborted - show after partial content
382
+ // But only if there are no tool calls (tool execution components will show the error)
383
+ const hasToolCalls = message.content.some(c => c.type === "toolCall");
384
+ if (!hasToolCalls) {
385
+ if (message.stopReason === "aborted" && !isSilentAbort(message.errorMessage)) {
386
+ const abortMessage =
387
+ message.errorMessage && message.errorMessage !== "Request was aborted"
388
+ ? message.errorMessage
389
+ : "Operation aborted";
390
+ descriptors.push({
391
+ key: "abort:spacer",
392
+ component: this.#cachedChild("abort:spacer", () => new Spacer(1)),
393
+ });
394
+ descriptors.push({
395
+ key: `abort:text:${abortMessage}`,
396
+ component: this.#cachedChild(
397
+ `abort:text:${abortMessage}`,
398
+ () => new Text(theme.fg("error", abortMessage), 1, 0),
399
+ ),
400
+ });
401
+ } else if (message.stopReason === "error") {
402
+ const errorMsg = message.errorMessage || "Unknown error";
403
+ descriptors.push({
404
+ key: "error:spacer",
405
+ component: this.#cachedChild("error:spacer", () => new Spacer(1)),
406
+ });
407
+ descriptors.push({
408
+ key: `error:text:${errorMsg}`,
409
+ component: this.#cachedChild(
410
+ `error:text:${errorMsg}`,
411
+ () => new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0),
412
+ ),
413
+ });
271
414
  }
272
- this.#contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
273
- } else if (message.stopReason === "error") {
274
- const errorMsg = message.errorMessage || "Unknown error";
275
- this.#contentContainer.addChild(new Spacer(1));
276
- this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
277
415
  }
278
- }
279
- if (
280
- message.errorMessage &&
281
- !isSilentAbort(message.errorMessage) &&
282
- message.stopReason !== "aborted" &&
283
- message.stopReason !== "error"
284
- ) {
285
- this.#contentContainer.addChild(new Spacer(1));
286
- this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${message.errorMessage}`), 1, 0));
287
- }
416
+ if (
417
+ message.errorMessage &&
418
+ !isSilentAbort(message.errorMessage) &&
419
+ message.stopReason !== "aborted" &&
420
+ message.stopReason !== "error"
421
+ ) {
422
+ descriptors.push({
423
+ key: "message-error:spacer",
424
+ component: this.#cachedChild("message-error:spacer", () => new Spacer(1)),
425
+ });
426
+ descriptors.push({
427
+ key: `message-error:text:${message.errorMessage}`,
428
+ component: this.#cachedChild(
429
+ `message-error:text:${message.errorMessage}`,
430
+ () => new Text(theme.fg("error", `Error: ${message.errorMessage}`), 1, 0),
431
+ ),
432
+ });
433
+ }
288
434
 
289
- // Token usage metadata
290
- if (settings.get("display.showTokenUsage") && this.#usageInfo) {
291
- const usage = this.#usageInfo;
292
- const totalInput = usage.input + usage.cacheWrite;
293
- const parts: string[] = [];
294
- parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
295
- parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
296
- if (usage.cacheRead > 0) {
297
- parts.push(`cache: ${formatNumber(usage.cacheRead)}`);
435
+ // Token usage metadata
436
+ if (settings.get("display.showTokenUsage") && this.#usageInfo) {
437
+ const usage = this.#usageInfo;
438
+ const totalInput = usage.input + usage.cacheWrite;
439
+ const parts: string[] = [];
440
+ parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
441
+ parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
442
+ if (usage.cacheRead > 0) {
443
+ parts.push(`cache: ${formatNumber(usage.cacheRead)}`);
444
+ }
445
+ const usageText = parts.join(" ");
446
+ descriptors.push({
447
+ key: "usage:spacer",
448
+ component: this.#cachedChild("usage:spacer", () => new Spacer(1)),
449
+ });
450
+ descriptors.push({
451
+ key: `usage:text:${usageText}`,
452
+ component: this.#cachedChild(
453
+ `usage:text:${usageText}`,
454
+ () => new Text(theme.fg("dim", usageText), 1, 0),
455
+ ),
456
+ });
298
457
  }
299
- this.#contentContainer.addChild(new Spacer(1));
300
- this.#contentContainer.addChild(new Text(theme.fg("dim", parts.join(" ")), 1, 0));
301
458
  }
459
+
460
+ this.#reconcileChildren(descriptors);
302
461
  }
303
462
  }
@@ -182,6 +182,7 @@ export class CustomEditor extends Editor {
182
182
  dispose(): void {
183
183
  this.#clearPendingPasteState();
184
184
  this.#pasteHandler = new BracketedPasteHandler();
185
+ super.dispose();
185
186
  }
186
187
 
187
188
  #drainPendingPasteInput(initialInput?: string): void {
@@ -96,6 +96,7 @@ export type ModelSelectorSelection =
96
96
  kind: "assignment";
97
97
  model: Model;
98
98
  role: GjcModelAssignmentTargetId | null;
99
+ roles?: readonly GjcModelAssignmentTargetId[];
99
100
  thinkingLevel?: ThinkingLevel;
100
101
  selector?: string;
101
102
  }
@@ -1360,7 +1361,11 @@ export class ModelSelectorComponent extends Container {
1360
1361
  for (let i = 0; i < actionCount; i++) {
1361
1362
  const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
1362
1363
  const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[i];
1363
- const label = `Set as ${GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase()} (${GJC_MODEL_ASSIGNMENT_TARGETS[role].name})`;
1364
+ const label = role
1365
+ ? `Set as ${GJC_MODEL_ASSIGNMENT_TARGETS[role].tag ?? role.toUpperCase()} (${GJC_MODEL_ASSIGNMENT_TARGETS[role].name})`
1366
+ : i === GJC_MODEL_ASSIGNMENT_TARGET_IDS.length
1367
+ ? "Set for all role agents"
1368
+ : "Set for all targets";
1364
1369
  this.#listContainer.addChild(
1365
1370
  new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
1366
1371
  );
@@ -1389,7 +1394,7 @@ export class ModelSelectorComponent extends Container {
1389
1394
  return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
1390
1395
  }
1391
1396
  #getActionCount(_model: Model): number {
1392
- return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length;
1397
+ return GJC_MODEL_ASSIGNMENT_TARGET_IDS.length + 2;
1393
1398
  }
1394
1399
 
1395
1400
  #getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
@@ -1660,7 +1665,15 @@ export class ModelSelectorComponent extends Container {
1660
1665
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
1661
1666
  this.#pendingActionItem = undefined;
1662
1667
  const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
1663
- if (role) this.#handleSelect(item, role);
1668
+ if (role) {
1669
+ this.#handleSelect(item, role);
1670
+ return;
1671
+ }
1672
+ const roles =
1673
+ this.#selectedActionIndex === GJC_MODEL_ASSIGNMENT_TARGET_IDS.length
1674
+ ? (["executor", "architect", "planner", "critic"] as const)
1675
+ : GJC_MODEL_ASSIGNMENT_TARGET_IDS;
1676
+ this.#handleSelect(item, "default", undefined, roles);
1664
1677
  return;
1665
1678
  }
1666
1679
  if (getKeybindings().matches(keyData, "tui.select.cancel")) {
@@ -1699,17 +1712,34 @@ export class ModelSelectorComponent extends Container {
1699
1712
  this.#updateList();
1700
1713
  }
1701
1714
  }
1715
+ #getInitialThinkingChoiceIndex(item: ModelItem | CanonicalModelItem, levels: ThinkingLevel[]): number {
1716
+ const preferred = this.#getPreferredThinkingLevel(item);
1717
+ if (preferred && preferred !== ThinkingLevel.Inherit) {
1718
+ const index = levels.indexOf(preferred);
1719
+ if (index !== -1) return index;
1720
+ }
1721
+ return 0;
1722
+ }
1723
+
1724
+ #getPreferredThinkingLevel(item: ModelItem | CanonicalModelItem): ThinkingLevel | undefined {
1725
+ if (item.thinkingLevel && item.thinkingLevel !== ThinkingLevel.Inherit) {
1726
+ return item.thinkingLevel;
1727
+ }
1728
+ return undefined;
1729
+ }
1702
1730
 
1703
1731
  #handleSelect(
1704
1732
  item: ModelItem | CanonicalModelItem,
1705
1733
  role: GjcModelAssignmentTargetId | null,
1706
1734
  thinkingLevel?: ThinkingLevel,
1735
+ roles?: readonly GjcModelAssignmentTargetId[],
1707
1736
  ): void {
1708
1737
  const itemThinkingLevel = thinkingLevel ?? item.thinkingLevel;
1709
1738
  const hasExplicitThinkingChoice = thinkingLevel !== undefined || item.explicitThinkingLevel === true;
1710
- if (!hasExplicitThinkingChoice && requiresExplicitThinkingChoice(item.model)) {
1711
- this.#pendingThinkingChoice = { item, role, levels: getSelectableThinkingLevels(item.model) };
1712
- this.#selectedThinkingIndex = 0;
1739
+ if (!hasExplicitThinkingChoice && requiresExplicitThinkingChoice(item.model, role)) {
1740
+ const levels = getSelectableThinkingLevels(item.model);
1741
+ this.#pendingThinkingChoice = { item, role, levels };
1742
+ this.#selectedThinkingIndex = this.#getInitialThinkingChoiceIndex(item, levels);
1713
1743
  this.#updateList();
1714
1744
  return;
1715
1745
  }
@@ -1727,17 +1757,23 @@ export class ModelSelectorComponent extends Container {
1727
1757
  }
1728
1758
 
1729
1759
  const selectedThinkingLevel = itemThinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
1730
- const selectorValue =
1731
- role === "default" ? item.selector : formatModelSelectorValue(item.selector, selectedThinkingLevel);
1760
+ const selectorValue = roles
1761
+ ? formatModelSelectorValue(item.selector, selectedThinkingLevel)
1762
+ : role === "default"
1763
+ ? item.selector
1764
+ : formatModelSelectorValue(item.selector, selectedThinkingLevel);
1732
1765
 
1733
1766
  // Update local state for UI
1734
- this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel };
1767
+ for (const targetRole of roles ?? [role]) {
1768
+ this.#roles[targetRole] = { model: item.model, thinkingLevel: selectedThinkingLevel };
1769
+ }
1735
1770
 
1736
1771
  // Notify caller (for updating agent state if needed)
1737
1772
  this.#onSelectCallback({
1738
1773
  kind: "assignment",
1739
1774
  model: item.model,
1740
1775
  role,
1776
+ roles,
1741
1777
  thinkingLevel: selectedThinkingLevel,
1742
1778
  selector: selectorValue,
1743
1779
  });
@@ -1752,6 +1788,11 @@ export class ModelSelectorComponent extends Container {
1752
1788
  async __testSelectProfile(profileName: string, setDefault: boolean): Promise<void> {
1753
1789
  await this.#onSelectCallback({ kind: "profile", profileName, setDefault });
1754
1790
  }
1791
+ async __testSelectAssignment(
1792
+ selection: Omit<Extract<ModelSelectorSelection, { kind: "assignment" }>, "kind">,
1793
+ ): Promise<void> {
1794
+ await this.#onSelectCallback({ kind: "assignment", ...selection });
1795
+ }
1755
1796
  async __testSelectPresetAction(profileName: string, action: "rename" | "delete"): Promise<void> {
1756
1797
  await this.#onSelectCallback({
1757
1798
  kind: action === "rename" ? "renameProfile" : "deleteProfile",
@@ -1764,8 +1805,10 @@ export class ModelSelectorComponent extends Container {
1764
1805
  }
1765
1806
  }
1766
1807
 
1767
- function requiresExplicitThinkingChoice(model: Model): boolean {
1768
- return model.reasoning === true && (model.provider === "openai" || model.provider === "openai-codex");
1808
+ function requiresExplicitThinkingChoice(model: Model, role: GjcModelAssignmentTargetId | null): boolean {
1809
+ if (model.reasoning !== true) return false;
1810
+ if (model.provider === "openai" || model.provider === "openai-codex") return true;
1811
+ return role !== null && GJC_MODEL_ASSIGNMENT_TARGETS[role].settingsPath === "task.agentModelOverrides";
1769
1812
  }
1770
1813
 
1771
1814
  function getSelectableThinkingLevels(model: Model): ThinkingLevel[] {
@@ -0,0 +1,92 @@
1
+ import { Container, matchesKey, type SelectItem, SelectList, Spacer, Text } from "@gajae-code/tui";
2
+ import type { QueuedMessageEditEntry } from "../../session/agent-session";
3
+ import { getSelectListTheme, theme } from "../theme/theme";
4
+ import { DynamicBorder } from "./dynamic-border";
5
+
6
+ const MAX_VISIBLE_QUEUED_MESSAGES = 8;
7
+ const RAW_UP = "\x1b[A";
8
+ const RAW_DOWN = "\x1b[B";
9
+ type QueuedMessageMoveDirection = "up" | "down";
10
+
11
+ export type { QueuedMessageMoveDirection };
12
+
13
+ export class QueuedMessageSelectorComponent extends Container {
14
+ #selectList: SelectList;
15
+ #selectedEntry: QueuedMessageEditEntry | undefined;
16
+ #selectedIndex = 0;
17
+ #onDelete: (entry: QueuedMessageEditEntry, selectedIndex: number) => void;
18
+ #onMove: (entry: QueuedMessageEditEntry, selectedIndex: number, direction: QueuedMessageMoveDirection) => void;
19
+
20
+ constructor(
21
+ entries: QueuedMessageEditEntry[],
22
+ onSelect: (entry: QueuedMessageEditEntry) => void,
23
+ onDelete: (entry: QueuedMessageEditEntry, selectedIndex: number) => void,
24
+ onMove: (entry: QueuedMessageEditEntry, selectedIndex: number, direction: QueuedMessageMoveDirection) => void,
25
+ onCancel: () => void,
26
+ options?: { selectedIndex?: number },
27
+ ) {
28
+ super();
29
+
30
+ this.#onDelete = onDelete;
31
+ this.#onMove = onMove;
32
+ const byId = new Map(entries.map(entry => [entry.id, entry]));
33
+ this.#selectedIndex = Math.max(0, Math.min(options?.selectedIndex ?? 0, entries.length - 1));
34
+ this.#selectedEntry = entries[this.#selectedIndex];
35
+ const items: SelectItem[] = entries.map((entry, index) => ({
36
+ value: entry.id,
37
+ label: `${entry.label} ${index + 1}`,
38
+ description: entry.text,
39
+ hint: "Enter edit · Del remove · Ctrl+↑/↓ move",
40
+ }));
41
+
42
+ this.addChild(new Spacer(1));
43
+ this.addChild(new Text(theme.bold("Queued messages"), 1, 0));
44
+ this.addChild(new Text(theme.fg("muted", "Enter edit · Del remove · Ctrl+↑/↓ move · Esc cancel"), 1, 0));
45
+ this.addChild(new Spacer(1));
46
+ this.addChild(new DynamicBorder());
47
+
48
+ this.#selectList = new SelectList(items, MAX_VISIBLE_QUEUED_MESSAGES, getSelectListTheme());
49
+ this.#selectList.onSelect = item => {
50
+ const entry = byId.get(item.value);
51
+ if (entry) onSelect(entry);
52
+ };
53
+ this.#selectList.onSelectionChange = item => {
54
+ const index = entries.findIndex(entry => entry.id === item.value);
55
+ this.#selectedIndex = index === -1 ? this.#selectedIndex : index;
56
+ this.#selectedEntry = byId.get(item.value);
57
+ };
58
+ this.#selectList.setSelectedIndex(this.#selectedIndex);
59
+ this.#selectList.onCancel = onCancel;
60
+
61
+ this.addChild(this.#selectList);
62
+ this.addChild(new DynamicBorder());
63
+ }
64
+
65
+ handleInput(keyData: string): void {
66
+ if (matchesKey(keyData, "alt+up")) {
67
+ this.#selectList.handleInput(RAW_UP);
68
+ return;
69
+ }
70
+ if (matchesKey(keyData, "alt+down")) {
71
+ this.#selectList.handleInput(RAW_DOWN);
72
+ return;
73
+ }
74
+ if (matchesKey(keyData, "ctrl+up") || matchesKey(keyData, "ctrl+shift+up")) {
75
+ if (this.#selectedEntry) this.#onMove(this.#selectedEntry, this.#selectedIndex, "up");
76
+ return;
77
+ }
78
+ if (matchesKey(keyData, "ctrl+down") || matchesKey(keyData, "ctrl+shift+down")) {
79
+ if (this.#selectedEntry) this.#onMove(this.#selectedEntry, this.#selectedIndex, "down");
80
+ return;
81
+ }
82
+ if (matchesKey(keyData, "delete")) {
83
+ if (this.#selectedEntry) this.#onDelete(this.#selectedEntry, this.#selectedIndex);
84
+ return;
85
+ }
86
+ this.#selectList.handleInput(keyData);
87
+ }
88
+
89
+ getSelectList(): SelectList {
90
+ return this.#selectList;
91
+ }
92
+ }