@oh-my-pi/pi-coding-agent 4.1.0 → 4.2.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 (81) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -1
  3. package/docs/sdk.md +0 -3
  4. package/package.json +6 -5
  5. package/src/config.ts +9 -0
  6. package/src/core/agent-storage.ts +450 -0
  7. package/src/core/auth-storage.ts +102 -183
  8. package/src/core/compaction/branch-summarization.ts +5 -4
  9. package/src/core/compaction/compaction.ts +7 -6
  10. package/src/core/compaction/utils.ts +6 -11
  11. package/src/core/custom-commands/bundled/review/index.ts +22 -94
  12. package/src/core/custom-share.ts +66 -0
  13. package/src/core/history-storage.ts +15 -7
  14. package/src/core/prompt-templates.ts +271 -1
  15. package/src/core/sdk.ts +14 -3
  16. package/src/core/settings-manager.ts +100 -34
  17. package/src/core/slash-commands.ts +4 -1
  18. package/src/core/storage-migration.ts +215 -0
  19. package/src/core/system-prompt.ts +87 -289
  20. package/src/core/title-generator.ts +3 -2
  21. package/src/core/tools/ask.ts +2 -2
  22. package/src/core/tools/bash.ts +2 -1
  23. package/src/core/tools/calculator.ts +2 -1
  24. package/src/core/tools/edit.ts +2 -1
  25. package/src/core/tools/find.ts +2 -1
  26. package/src/core/tools/gemini-image.ts +2 -1
  27. package/src/core/tools/git.ts +2 -2
  28. package/src/core/tools/grep.ts +2 -1
  29. package/src/core/tools/index.test.ts +0 -28
  30. package/src/core/tools/index.ts +0 -6
  31. package/src/core/tools/lsp/index.ts +2 -1
  32. package/src/core/tools/output.ts +2 -1
  33. package/src/core/tools/read.ts +4 -1
  34. package/src/core/tools/ssh.ts +4 -2
  35. package/src/core/tools/task/agents.ts +56 -30
  36. package/src/core/tools/task/commands.ts +9 -8
  37. package/src/core/tools/task/index.ts +7 -15
  38. package/src/core/tools/web-fetch.ts +2 -1
  39. package/src/core/tools/web-search/auth.ts +106 -16
  40. package/src/core/tools/web-search/index.ts +3 -2
  41. package/src/core/tools/web-search/providers/anthropic.ts +44 -6
  42. package/src/core/tools/write.ts +2 -1
  43. package/src/core/voice.ts +3 -1
  44. package/src/main.ts +1 -1
  45. package/src/migrations.ts +20 -20
  46. package/src/modes/interactive/controllers/command-controller.ts +527 -0
  47. package/src/modes/interactive/controllers/event-controller.ts +340 -0
  48. package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
  49. package/src/modes/interactive/controllers/input-controller.ts +585 -0
  50. package/src/modes/interactive/controllers/selector-controller.ts +585 -0
  51. package/src/modes/interactive/interactive-mode.ts +364 -3143
  52. package/src/modes/interactive/theme/theme.ts +5 -5
  53. package/src/modes/interactive/types.ts +189 -0
  54. package/src/modes/interactive/utils/ui-helpers.ts +449 -0
  55. package/src/modes/interactive/utils/voice-manager.ts +96 -0
  56. package/src/prompts/{explore.md → agents/explore.md} +7 -5
  57. package/src/prompts/agents/frontmatter.md +7 -0
  58. package/src/prompts/{plan.md → agents/plan.md} +3 -3
  59. package/src/prompts/{task.md → agents/task.md} +1 -1
  60. package/src/prompts/review-request.md +44 -8
  61. package/src/prompts/system/custom-system-prompt.md +80 -0
  62. package/src/prompts/system/file-operations.md +12 -0
  63. package/src/prompts/system/system-prompt.md +232 -0
  64. package/src/prompts/system/title-system.md +2 -0
  65. package/src/prompts/tools/bash.md +1 -1
  66. package/src/prompts/tools/read.md +1 -1
  67. package/src/prompts/tools/task.md +9 -3
  68. package/src/core/tools/rulebook.ts +0 -132
  69. package/src/prompts/system-prompt.md +0 -43
  70. package/src/prompts/title-system.md +0 -8
  71. /package/src/prompts/{architect-plan.md → agents/architect-plan.md} +0 -0
  72. /package/src/prompts/{implement-with-critic.md → agents/implement-with-critic.md} +0 -0
  73. /package/src/prompts/{implement.md → agents/implement.md} +0 -0
  74. /package/src/prompts/{init.md → agents/init.md} +0 -0
  75. /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
  76. /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
  77. /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
  78. /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
  79. /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
  80. /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
  81. /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
@@ -0,0 +1,600 @@
1
+ import type { Component, TUI } from "@oh-my-pi/pi-tui";
2
+ import { Spacer, Text } from "@oh-my-pi/pi-tui";
3
+ import type {
4
+ ExtensionActions,
5
+ ExtensionCommandContextActions,
6
+ ExtensionContextActions,
7
+ ExtensionError,
8
+ ExtensionUIContext,
9
+ } from "../../../core/extensions/index";
10
+ import { KeybindingsManager } from "../../../core/keybindings";
11
+ import { logger } from "../../../core/logger";
12
+ import { setTerminalTitle } from "../../../core/title-generator";
13
+ import { HookEditorComponent } from "../components/hook-editor";
14
+ import { HookInputComponent } from "../components/hook-input";
15
+ import { HookSelectorComponent } from "../components/hook-selector";
16
+ import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../theme/theme";
17
+ import type { InteractiveModeContext } from "../types";
18
+
19
+ export class ExtensionUiController {
20
+ constructor(private ctx: InteractiveModeContext) {}
21
+
22
+ /**
23
+ * Initialize the hook system with TUI-based UI context.
24
+ */
25
+ async initHooksAndCustomTools(): Promise<void> {
26
+ // Create and set hook & tool UI context
27
+ const uiContext: ExtensionUIContext = {
28
+ select: (title, options, _dialogOptions) => this.showHookSelector(title, options),
29
+ confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
30
+ input: (title, placeholder, _dialogOptions) => this.showHookInput(title, placeholder),
31
+ notify: (message, type) => this.showHookNotify(message, type),
32
+ setStatus: (key, text) => this.setHookStatus(key, text),
33
+ setWidget: (key, content) => this.setHookWidget(key, content),
34
+ setTitle: (title) => setTerminalTitle(title),
35
+ custom: (factory, _options) => this.showHookCustom(factory),
36
+ setEditorText: (text) => this.ctx.editor.setText(text),
37
+ getEditorText: () => this.ctx.editor.getText(),
38
+ editor: (title, prefill) => this.showHookEditor(title, prefill),
39
+ get theme() {
40
+ return theme;
41
+ },
42
+ getAllThemes: () => getAvailableThemesWithPaths().map((t) => ({ name: t.name, path: t.path })),
43
+ getTheme: (name) => getThemeByName(name),
44
+ setTheme: (themeArg) => {
45
+ if (typeof themeArg === "string") {
46
+ return setTheme(themeArg, true);
47
+ }
48
+ // Theme object passed directly - not supported in current implementation
49
+ return { success: false, error: "Direct theme object not supported" };
50
+ },
51
+ setFooter: () => {},
52
+ setHeader: () => {},
53
+ setEditorComponent: () => {},
54
+ };
55
+ this.ctx.setToolUIContext(uiContext, true);
56
+
57
+ const extensionRunner = this.ctx.session.extensionRunner;
58
+ if (!extensionRunner) {
59
+ return; // No hooks loaded
60
+ }
61
+
62
+ const actions: ExtensionActions = {
63
+ sendMessage: (message, options) => {
64
+ const wasStreaming = this.ctx.session.isStreaming;
65
+ this.ctx.session
66
+ .sendCustomMessage(message, options)
67
+ .then(() => {
68
+ // For non-streaming cases with display=true, update UI
69
+ // (streaming cases update via message_end event)
70
+ if (!this.ctx.isBackgrounded && !wasStreaming && message.display) {
71
+ this.ctx.rebuildChatFromMessages();
72
+ }
73
+ })
74
+ .catch((err: unknown) => {
75
+ this.ctx.showError(
76
+ `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`,
77
+ );
78
+ });
79
+ },
80
+ sendUserMessage: (content, options) => {
81
+ this.ctx.session.sendUserMessage(content, options).catch((err: unknown) => {
82
+ this.ctx.showError(
83
+ `Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`,
84
+ );
85
+ });
86
+ },
87
+ appendEntry: (customType, data) => {
88
+ this.ctx.sessionManager.appendCustomEntry(customType, data);
89
+ },
90
+ getActiveTools: () => this.ctx.session.getActiveToolNames(),
91
+ getAllTools: () => this.ctx.session.getAllToolNames(),
92
+ setActiveTools: (toolNames) => this.ctx.session.setActiveToolsByName(toolNames),
93
+ setModel: async (model) => {
94
+ const key = await this.ctx.session.modelRegistry.getApiKey(model);
95
+ if (!key) return false;
96
+ await this.ctx.session.setModel(model);
97
+ return true;
98
+ },
99
+ getThinkingLevel: () => this.ctx.session.thinkingLevel,
100
+ setThinkingLevel: (level) => this.ctx.session.setThinkingLevel(level),
101
+ };
102
+ const contextActions: ExtensionContextActions = {
103
+ getModel: () => this.ctx.session.model,
104
+ isIdle: () => !this.ctx.session.isStreaming,
105
+ abort: () => this.ctx.session.abort(),
106
+ hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
107
+ shutdown: () => {
108
+ // Signal shutdown request (will be handled by main loop)
109
+ },
110
+ };
111
+ const commandActions: ExtensionCommandContextActions = {
112
+ waitForIdle: () => this.ctx.session.agent.waitForIdle(),
113
+ newSession: async (options) => {
114
+ // Stop any loading animation
115
+ if (this.ctx.loadingAnimation) {
116
+ this.ctx.loadingAnimation.stop();
117
+ this.ctx.loadingAnimation = undefined;
118
+ }
119
+ this.ctx.statusContainer.clear();
120
+
121
+ // Create new session
122
+ const success = await this.ctx.session.newSession({ parentSession: options?.parentSession });
123
+ if (!success) {
124
+ return { cancelled: true };
125
+ }
126
+
127
+ // Call setup callback if provided
128
+ if (options?.setup) {
129
+ await options.setup(this.ctx.sessionManager);
130
+ }
131
+
132
+ // Clear UI state
133
+ this.ctx.chatContainer.clear();
134
+ this.ctx.pendingMessagesContainer.clear();
135
+ this.ctx.compactionQueuedMessages = [];
136
+ this.ctx.streamingComponent = undefined;
137
+ this.ctx.streamingMessage = undefined;
138
+ this.ctx.pendingTools.clear();
139
+
140
+ this.ctx.chatContainer.addChild(new Spacer(1));
141
+ this.ctx.chatContainer.addChild(
142
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
143
+ );
144
+ this.ctx.ui.requestRender();
145
+
146
+ return { cancelled: false };
147
+ },
148
+ branch: async (entryId) => {
149
+ const result = await this.ctx.session.branch(entryId);
150
+ if (result.cancelled) {
151
+ return { cancelled: true };
152
+ }
153
+
154
+ // Update UI
155
+ this.ctx.chatContainer.clear();
156
+ this.ctx.renderInitialMessages();
157
+ this.ctx.editor.setText(result.selectedText);
158
+ this.ctx.showStatus("Branched to new session");
159
+
160
+ return { cancelled: false };
161
+ },
162
+ navigateTree: async (targetId, options) => {
163
+ const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize });
164
+ if (result.cancelled) {
165
+ return { cancelled: true };
166
+ }
167
+
168
+ // Update UI
169
+ this.ctx.chatContainer.clear();
170
+ this.ctx.renderInitialMessages();
171
+ if (result.editorText) {
172
+ this.ctx.editor.setText(result.editorText);
173
+ }
174
+ this.ctx.showStatus("Navigated to selected point");
175
+
176
+ return { cancelled: false };
177
+ },
178
+ };
179
+
180
+ extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
181
+
182
+ // Subscribe to extension errors
183
+ extensionRunner.onError((error: ExtensionError) => {
184
+ this.showExtensionError(error.extensionPath, error.error);
185
+ });
186
+
187
+ // Emit session_start event
188
+ await extensionRunner.emit({
189
+ type: "session_start",
190
+ });
191
+ }
192
+
193
+ setHookWidget(key: string, content: unknown): void {
194
+ this.ctx.statusLine.setHookStatus(key, String(content));
195
+ this.ctx.ui.requestRender();
196
+ }
197
+
198
+ initializeHookRunner(uiContext: ExtensionUIContext, _hasUI: boolean): void {
199
+ const extensionRunner = this.ctx.session.extensionRunner;
200
+ if (!extensionRunner) {
201
+ return;
202
+ }
203
+
204
+ const actions: ExtensionActions = {
205
+ sendMessage: (message, options) => {
206
+ const wasStreaming = this.ctx.session.isStreaming;
207
+ this.ctx.session
208
+ .sendCustomMessage(message, options)
209
+ .then(() => {
210
+ // For non-streaming cases with display=true, update UI
211
+ // (streaming cases update via message_end event)
212
+ if (!this.ctx.isBackgrounded && !wasStreaming && message.display) {
213
+ this.ctx.rebuildChatFromMessages();
214
+ }
215
+ })
216
+ .catch((err: unknown) => {
217
+ const errorText = `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`;
218
+ if (this.ctx.isBackgrounded) {
219
+ logger.error(errorText);
220
+ return;
221
+ }
222
+ this.ctx.showError(errorText);
223
+ });
224
+ },
225
+ sendUserMessage: (content, options) => {
226
+ this.ctx.session.sendUserMessage(content, options).catch((err: unknown) => {
227
+ this.ctx.showError(
228
+ `Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`,
229
+ );
230
+ });
231
+ },
232
+ appendEntry: (customType, data) => {
233
+ this.ctx.sessionManager.appendCustomEntry(customType, data);
234
+ },
235
+ getActiveTools: () => this.ctx.session.getActiveToolNames(),
236
+ getAllTools: () => this.ctx.session.getAllToolNames(),
237
+ setActiveTools: (toolNames) => this.ctx.session.setActiveToolsByName(toolNames),
238
+ setModel: async (model) => {
239
+ const key = await this.ctx.session.modelRegistry.getApiKey(model);
240
+ if (!key) return false;
241
+ await this.ctx.session.setModel(model);
242
+ return true;
243
+ },
244
+ getThinkingLevel: () => this.ctx.session.thinkingLevel,
245
+ setThinkingLevel: (level) => this.ctx.session.setThinkingLevel(level),
246
+ };
247
+ const contextActions: ExtensionContextActions = {
248
+ getModel: () => this.ctx.session.model,
249
+ isIdle: () => !this.ctx.session.isStreaming,
250
+ abort: () => this.ctx.session.abort(),
251
+ hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
252
+ shutdown: () => {
253
+ // Signal shutdown request (will be handled by main loop)
254
+ },
255
+ };
256
+ const commandActions: ExtensionCommandContextActions = {
257
+ waitForIdle: () => this.ctx.session.agent.waitForIdle(),
258
+ newSession: async (options) => {
259
+ if (this.ctx.isBackgrounded) {
260
+ return { cancelled: true };
261
+ }
262
+ // Stop any loading animation
263
+ if (this.ctx.loadingAnimation) {
264
+ this.ctx.loadingAnimation.stop();
265
+ this.ctx.loadingAnimation = undefined;
266
+ }
267
+ this.ctx.statusContainer.clear();
268
+
269
+ // Create new session
270
+ const success = await this.ctx.session.newSession({ parentSession: options?.parentSession });
271
+ if (!success) {
272
+ return { cancelled: true };
273
+ }
274
+
275
+ // Call setup callback if provided
276
+ if (options?.setup) {
277
+ await options.setup(this.ctx.sessionManager);
278
+ }
279
+
280
+ // Clear UI state
281
+ this.ctx.chatContainer.clear();
282
+ this.ctx.pendingMessagesContainer.clear();
283
+ this.ctx.compactionQueuedMessages = [];
284
+ this.ctx.streamingComponent = undefined;
285
+ this.ctx.streamingMessage = undefined;
286
+ this.ctx.pendingTools.clear();
287
+
288
+ this.ctx.chatContainer.addChild(new Spacer(1));
289
+ this.ctx.chatContainer.addChild(
290
+ new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
291
+ );
292
+ this.ctx.ui.requestRender();
293
+
294
+ return { cancelled: false };
295
+ },
296
+ branch: async (entryId) => {
297
+ if (this.ctx.isBackgrounded) {
298
+ return { cancelled: true };
299
+ }
300
+ const result = await this.ctx.session.branch(entryId);
301
+ if (result.cancelled) {
302
+ return { cancelled: true };
303
+ }
304
+
305
+ // Update UI
306
+ this.ctx.chatContainer.clear();
307
+ this.ctx.renderInitialMessages();
308
+ this.ctx.editor.setText(result.selectedText);
309
+ this.ctx.showStatus("Branched to new session");
310
+
311
+ return { cancelled: false };
312
+ },
313
+ navigateTree: async (targetId, options) => {
314
+ if (this.ctx.isBackgrounded) {
315
+ return { cancelled: true };
316
+ }
317
+ const result = await this.ctx.session.navigateTree(targetId, { summarize: options?.summarize });
318
+ if (result.cancelled) {
319
+ return { cancelled: true };
320
+ }
321
+
322
+ // Update UI
323
+ this.ctx.chatContainer.clear();
324
+ this.ctx.renderInitialMessages();
325
+ if (result.editorText) {
326
+ this.ctx.editor.setText(result.editorText);
327
+ }
328
+ this.ctx.showStatus("Navigated to selected point");
329
+
330
+ return { cancelled: false };
331
+ },
332
+ };
333
+
334
+ extensionRunner.initialize(actions, contextActions, commandActions, uiContext);
335
+ }
336
+
337
+ createBackgroundUiContext(): ExtensionUIContext {
338
+ return {
339
+ select: async (_title: string, _options: string[], _dialogOptions) => undefined,
340
+ confirm: async (_title: string, _message: string, _dialogOptions) => false,
341
+ input: async (_title: string, _placeholder?: string, _dialogOptions?: unknown) => undefined,
342
+ notify: () => {},
343
+ setStatus: () => {},
344
+ setWidget: () => {},
345
+ setTitle: () => {},
346
+ custom: async () => undefined as never,
347
+ setEditorText: () => {},
348
+ getEditorText: () => "",
349
+ editor: async () => undefined,
350
+ get theme() {
351
+ return theme;
352
+ },
353
+ getAllThemes: () => [],
354
+ getTheme: () => undefined,
355
+ setTheme: () => ({ success: false, error: "Background mode" }),
356
+ setFooter: () => {},
357
+ setHeader: () => {},
358
+ setEditorComponent: () => {},
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Emit session event to all extension tools.
364
+ */
365
+ async emitCustomToolSessionEvent(
366
+ reason: "start" | "switch" | "branch" | "tree" | "shutdown",
367
+ previousSessionFile?: string,
368
+ ): Promise<void> {
369
+ const event = { reason, previousSessionFile };
370
+ const uiContext = this.ctx.session.extensionRunner?.getUIContext();
371
+ if (!uiContext) {
372
+ return;
373
+ }
374
+ for (const registeredTool of this.ctx.session.extensionRunner?.getAllRegisteredTools() ?? []) {
375
+ if (registeredTool.definition.onSession) {
376
+ try {
377
+ await registeredTool.definition.onSession(event, {
378
+ ui: uiContext,
379
+ hasUI: !this.ctx.isBackgrounded,
380
+ cwd: this.ctx.sessionManager.getCwd(),
381
+ sessionManager: this.ctx.session.sessionManager,
382
+ modelRegistry: this.ctx.session.modelRegistry,
383
+ model: this.ctx.session.model,
384
+ isIdle: () => !this.ctx.session.isStreaming,
385
+ hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
386
+ hasQueuedMessages: () => this.ctx.session.queuedMessageCount > 0,
387
+ abort: () => {
388
+ this.ctx.session.abort();
389
+ },
390
+ shutdown: () => {
391
+ // Signal shutdown request
392
+ },
393
+ });
394
+ } catch (err) {
395
+ this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
396
+ }
397
+ }
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Show a tool error in the chat.
403
+ */
404
+ showToolError(toolName: string, error: string): void {
405
+ if (this.ctx.isBackgrounded) {
406
+ logger.error(`Tool "${toolName}" error: ${error}`);
407
+ return;
408
+ }
409
+ const errorText = new Text(theme.fg("error", `Tool "${toolName}" error: ${error}`), 1, 0);
410
+ this.ctx.chatContainer.addChild(errorText);
411
+ this.ctx.ui.requestRender();
412
+ }
413
+
414
+ /**
415
+ * Set hook status text in the footer.
416
+ */
417
+ setHookStatus(key: string, text: string | undefined): void {
418
+ if (this.ctx.isBackgrounded) {
419
+ return;
420
+ }
421
+ this.ctx.statusLine.setHookStatus(key, text);
422
+ this.ctx.ui.requestRender();
423
+ }
424
+
425
+ /**
426
+ * Show a selector for hooks.
427
+ */
428
+ showHookSelector(title: string, options: string[]): Promise<string | undefined> {
429
+ return new Promise((resolve) => {
430
+ this.ctx.hookSelector = new HookSelectorComponent(
431
+ title,
432
+ options,
433
+ (option) => {
434
+ this.hideHookSelector();
435
+ resolve(option);
436
+ },
437
+ () => {
438
+ this.hideHookSelector();
439
+ resolve(undefined);
440
+ },
441
+ );
442
+
443
+ this.ctx.editorContainer.clear();
444
+ this.ctx.editorContainer.addChild(this.ctx.hookSelector);
445
+ this.ctx.ui.setFocus(this.ctx.hookSelector);
446
+ this.ctx.ui.requestRender();
447
+ });
448
+ }
449
+
450
+ /**
451
+ * Hide the hook selector.
452
+ */
453
+ hideHookSelector(): void {
454
+ this.ctx.editorContainer.clear();
455
+ this.ctx.editorContainer.addChild(this.ctx.editor);
456
+ this.ctx.hookSelector = undefined;
457
+ this.ctx.ui.setFocus(this.ctx.editor);
458
+ this.ctx.ui.requestRender();
459
+ }
460
+
461
+ /**
462
+ * Show a confirmation dialog for hooks.
463
+ */
464
+ async showHookConfirm(title: string, message: string): Promise<boolean> {
465
+ const result = await this.showHookSelector(`${title}\n${message}`, ["Yes", "No"]);
466
+ return result === "Yes";
467
+ }
468
+
469
+ /**
470
+ * Show a text input for hooks.
471
+ */
472
+ showHookInput(title: string, placeholder?: string): Promise<string | undefined> {
473
+ return new Promise((resolve) => {
474
+ this.ctx.hookInput = new HookInputComponent(
475
+ title,
476
+ placeholder,
477
+ (value) => {
478
+ this.hideHookInput();
479
+ resolve(value);
480
+ },
481
+ () => {
482
+ this.hideHookInput();
483
+ resolve(undefined);
484
+ },
485
+ );
486
+
487
+ this.ctx.editorContainer.clear();
488
+ this.ctx.editorContainer.addChild(this.ctx.hookInput);
489
+ this.ctx.ui.setFocus(this.ctx.hookInput);
490
+ this.ctx.ui.requestRender();
491
+ });
492
+ }
493
+
494
+ /**
495
+ * Hide the hook input.
496
+ */
497
+ hideHookInput(): void {
498
+ this.ctx.editorContainer.clear();
499
+ this.ctx.editorContainer.addChild(this.ctx.editor);
500
+ this.ctx.hookInput = undefined;
501
+ this.ctx.ui.setFocus(this.ctx.editor);
502
+ this.ctx.ui.requestRender();
503
+ }
504
+
505
+ /**
506
+ * Show a multi-line editor for hooks (with Ctrl+G support).
507
+ */
508
+ showHookEditor(title: string, prefill?: string): Promise<string | undefined> {
509
+ return new Promise((resolve) => {
510
+ this.ctx.hookEditor = new HookEditorComponent(
511
+ this.ctx.ui,
512
+ title,
513
+ prefill,
514
+ (value) => {
515
+ this.hideHookEditor();
516
+ resolve(value);
517
+ },
518
+ () => {
519
+ this.hideHookEditor();
520
+ resolve(undefined);
521
+ },
522
+ );
523
+
524
+ this.ctx.editorContainer.clear();
525
+ this.ctx.editorContainer.addChild(this.ctx.hookEditor);
526
+ this.ctx.ui.setFocus(this.ctx.hookEditor);
527
+ this.ctx.ui.requestRender();
528
+ });
529
+ }
530
+
531
+ /**
532
+ * Hide the hook editor.
533
+ */
534
+ hideHookEditor(): void {
535
+ this.ctx.editorContainer.clear();
536
+ this.ctx.editorContainer.addChild(this.ctx.editor);
537
+ this.ctx.hookEditor = undefined;
538
+ this.ctx.ui.setFocus(this.ctx.editor);
539
+ this.ctx.ui.requestRender();
540
+ }
541
+
542
+ /**
543
+ * Show a notification for hooks.
544
+ */
545
+ showHookNotify(message: string, type?: "info" | "warning" | "error"): void {
546
+ if (type === "error") {
547
+ this.ctx.showError(message);
548
+ } else if (type === "warning") {
549
+ this.ctx.showWarning(message);
550
+ } else {
551
+ this.ctx.showStatus(message);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Show a custom component with keyboard focus.
557
+ */
558
+ async showHookCustom<T>(
559
+ factory: (
560
+ tui: TUI,
561
+ theme: Theme,
562
+ keybindings: KeybindingsManager,
563
+ done: (result: T) => void,
564
+ ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
565
+ ): Promise<T> {
566
+ const savedText = this.ctx.editor.getText();
567
+ const keybindings = KeybindingsManager.inMemory();
568
+
569
+ return new Promise((resolve) => {
570
+ let component: Component & { dispose?(): void };
571
+
572
+ const close = (result: T) => {
573
+ component.dispose?.();
574
+ this.ctx.editorContainer.clear();
575
+ this.ctx.editorContainer.addChild(this.ctx.editor);
576
+ this.ctx.editor.setText(savedText);
577
+ this.ctx.ui.setFocus(this.ctx.editor);
578
+ this.ctx.ui.requestRender();
579
+ resolve(result);
580
+ };
581
+
582
+ Promise.resolve(factory(this.ctx.ui, theme, keybindings, close)).then((c) => {
583
+ component = c;
584
+ this.ctx.editorContainer.clear();
585
+ this.ctx.editorContainer.addChild(component);
586
+ this.ctx.ui.setFocus(component);
587
+ this.ctx.ui.requestRender();
588
+ });
589
+ });
590
+ }
591
+
592
+ /**
593
+ * Show an extension error in the UI.
594
+ */
595
+ showExtensionError(extensionPath: string, error: string): void {
596
+ const errorText = new Text(theme.fg("error", `Extension "${extensionPath}" error: ${error}`), 1, 0);
597
+ this.ctx.chatContainer.addChild(errorText);
598
+ this.ctx.ui.requestRender();
599
+ }
600
+ }