@oh-my-pi/pi-coding-agent 4.1.0 → 4.2.1
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.
- package/CHANGELOG.md +66 -0
- package/README.md +2 -1
- package/docs/sdk.md +0 -3
- package/package.json +6 -5
- package/src/config.ts +9 -0
- package/src/core/agent-session.ts +3 -3
- package/src/core/agent-storage.ts +450 -0
- package/src/core/auth-storage.ts +102 -183
- package/src/core/compaction/branch-summarization.ts +5 -4
- package/src/core/compaction/compaction.ts +7 -6
- package/src/core/compaction/utils.ts +6 -11
- package/src/core/custom-commands/bundled/review/index.ts +22 -94
- package/src/core/custom-share.ts +66 -0
- package/src/core/export-html/index.ts +1 -33
- package/src/core/history-storage.ts +15 -7
- package/src/core/prompt-templates.ts +271 -1
- package/src/core/sdk.ts +14 -3
- package/src/core/settings-manager.ts +100 -34
- package/src/core/slash-commands.ts +4 -1
- package/src/core/storage-migration.ts +215 -0
- package/src/core/system-prompt.ts +130 -290
- package/src/core/title-generator.ts +3 -2
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +2 -1
- package/src/core/tools/calculator.ts +2 -1
- package/src/core/tools/complete.ts +5 -2
- package/src/core/tools/edit.ts +2 -1
- package/src/core/tools/find.ts +2 -1
- package/src/core/tools/gemini-image.ts +2 -1
- package/src/core/tools/git.ts +2 -2
- package/src/core/tools/grep.ts +2 -1
- package/src/core/tools/index.test.ts +0 -28
- package/src/core/tools/index.ts +0 -6
- package/src/core/tools/lsp/index.ts +2 -1
- package/src/core/tools/output.ts +2 -1
- package/src/core/tools/read.ts +4 -1
- package/src/core/tools/ssh.ts +4 -2
- package/src/core/tools/task/agents.ts +56 -30
- package/src/core/tools/task/commands.ts +5 -8
- package/src/core/tools/task/index.ts +7 -15
- package/src/core/tools/web-fetch.ts +2 -1
- package/src/core/tools/web-search/auth.ts +106 -16
- package/src/core/tools/web-search/index.ts +3 -2
- package/src/core/tools/web-search/providers/anthropic.ts +44 -6
- package/src/core/tools/write.ts +2 -1
- package/src/core/voice.ts +3 -1
- package/src/discovery/builtin.ts +9 -54
- package/src/discovery/claude.ts +16 -69
- package/src/discovery/codex.ts +11 -36
- package/src/discovery/helpers.ts +52 -1
- package/src/main.ts +1 -1
- package/src/migrations.ts +20 -20
- package/src/modes/interactive/controllers/command-controller.ts +527 -0
- package/src/modes/interactive/controllers/event-controller.ts +340 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
- package/src/modes/interactive/controllers/input-controller.ts +585 -0
- package/src/modes/interactive/controllers/selector-controller.ts +585 -0
- package/src/modes/interactive/interactive-mode.ts +363 -3139
- package/src/modes/interactive/theme/theme.ts +5 -5
- package/src/modes/interactive/types.ts +189 -0
- package/src/modes/interactive/utils/ui-helpers.ts +449 -0
- package/src/modes/interactive/utils/voice-manager.ts +96 -0
- package/src/prompts/{explore.md → agents/explore.md} +7 -5
- package/src/prompts/agents/frontmatter.md +7 -0
- package/src/prompts/{plan.md → agents/plan.md} +3 -3
- package/src/prompts/agents/planner.md +112 -0
- package/src/prompts/agents/task.md +15 -0
- package/src/prompts/review-request.md +44 -8
- package/src/prompts/system/custom-system-prompt.md +80 -0
- package/src/prompts/system/file-operations.md +12 -0
- package/src/prompts/system/system-prompt.md +237 -0
- package/src/prompts/system/title-system.md +2 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +34 -22
- package/src/core/tools/rulebook.ts +0 -132
- package/src/prompts/architect-plan.md +0 -10
- package/src/prompts/implement-with-critic.md +0 -11
- package/src/prompts/implement.md +0 -11
- package/src/prompts/system-prompt.md +0 -43
- package/src/prompts/task.md +0 -14
- package/src/prompts/title-system.md +0 -8
- /package/src/prompts/{init.md → agents/init.md} +0 -0
- /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
- /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
- /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
- /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
- /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
- /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
- /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
|
+
}
|