@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,585 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { OAuthProvider } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { getAgentDbPath } from "../../../config";
|
|
6
|
+
import { SessionManager } from "../../../core/session-manager";
|
|
7
|
+
import { setPreferredImageProvider, setPreferredWebSearchProvider } from "../../../core/tools/index";
|
|
8
|
+
import { disableProvider, enableProvider } from "../../../discovery";
|
|
9
|
+
import { AssistantMessageComponent } from "../components/assistant-message";
|
|
10
|
+
import { ExtensionDashboard } from "../components/extensions";
|
|
11
|
+
import { HistorySearchComponent } from "../components/history-search";
|
|
12
|
+
import { ModelSelectorComponent } from "../components/model-selector";
|
|
13
|
+
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
14
|
+
import { SessionSelectorComponent } from "../components/session-selector";
|
|
15
|
+
import { SettingsSelectorComponent } from "../components/settings-selector";
|
|
16
|
+
import { ToolExecutionComponent } from "../components/tool-execution";
|
|
17
|
+
import { TreeSelectorComponent } from "../components/tree-selector";
|
|
18
|
+
import { UserMessageSelectorComponent } from "../components/user-message-selector";
|
|
19
|
+
import { getAvailableThemes, getSymbolTheme, setSymbolPreset, setTheme, theme } from "../theme/theme";
|
|
20
|
+
import type { InteractiveModeContext } from "../types";
|
|
21
|
+
|
|
22
|
+
export class SelectorController {
|
|
23
|
+
constructor(private ctx: InteractiveModeContext) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shows a selector component in place of the editor.
|
|
27
|
+
* @param create Factory that receives a `done` callback and returns the component and focus target
|
|
28
|
+
*/
|
|
29
|
+
showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
|
|
30
|
+
const done = () => {
|
|
31
|
+
this.ctx.editorContainer.clear();
|
|
32
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
33
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
34
|
+
};
|
|
35
|
+
const { component, focus } = create(done);
|
|
36
|
+
this.ctx.editorContainer.clear();
|
|
37
|
+
this.ctx.editorContainer.addChild(component);
|
|
38
|
+
this.ctx.ui.setFocus(focus);
|
|
39
|
+
this.ctx.ui.requestRender();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
showSettingsSelector(): void {
|
|
43
|
+
this.showSelector((done) => {
|
|
44
|
+
const selector = new SettingsSelectorComponent(
|
|
45
|
+
this.ctx.settingsManager,
|
|
46
|
+
{
|
|
47
|
+
availableThinkingLevels: this.ctx.session.getAvailableThinkingLevels(),
|
|
48
|
+
thinkingLevel: this.ctx.session.thinkingLevel,
|
|
49
|
+
availableThemes: getAvailableThemes(),
|
|
50
|
+
cwd: process.cwd(),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
onChange: (id, value) => this.handleSettingChange(id, value),
|
|
54
|
+
onThemePreview: (themeName) => {
|
|
55
|
+
const result = setTheme(themeName, true);
|
|
56
|
+
if (result.success) {
|
|
57
|
+
this.ctx.ui.invalidate();
|
|
58
|
+
this.ctx.ui.requestRender();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
onStatusLinePreview: (settings) => {
|
|
62
|
+
// Update status line with preview settings
|
|
63
|
+
const currentSettings = this.ctx.settingsManager.getStatusLineSettings();
|
|
64
|
+
this.ctx.statusLine.updateSettings({ ...currentSettings, ...settings });
|
|
65
|
+
this.ctx.updateEditorTopBorder();
|
|
66
|
+
this.ctx.ui.requestRender();
|
|
67
|
+
},
|
|
68
|
+
getStatusLinePreview: () => {
|
|
69
|
+
// Return the rendered status line for inline preview
|
|
70
|
+
const width = this.ctx.ui.getWidth();
|
|
71
|
+
return this.ctx.statusLine.getTopBorder(width).content;
|
|
72
|
+
},
|
|
73
|
+
onPluginsChanged: () => {
|
|
74
|
+
this.ctx.ui.requestRender();
|
|
75
|
+
},
|
|
76
|
+
onCancel: () => {
|
|
77
|
+
done();
|
|
78
|
+
// Restore status line to saved settings
|
|
79
|
+
this.ctx.statusLine.updateSettings(this.ctx.settingsManager.getStatusLineSettings());
|
|
80
|
+
this.ctx.updateEditorTopBorder();
|
|
81
|
+
this.ctx.ui.requestRender();
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
return { component: selector, focus: selector };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
showHistorySearch(): void {
|
|
90
|
+
const historyStorage = this.ctx.historyStorage;
|
|
91
|
+
if (!historyStorage) return;
|
|
92
|
+
|
|
93
|
+
this.showSelector((done) => {
|
|
94
|
+
const component = new HistorySearchComponent(
|
|
95
|
+
historyStorage,
|
|
96
|
+
(prompt) => {
|
|
97
|
+
done();
|
|
98
|
+
this.ctx.editor.setText(prompt);
|
|
99
|
+
this.ctx.ui.requestRender();
|
|
100
|
+
},
|
|
101
|
+
() => {
|
|
102
|
+
done();
|
|
103
|
+
this.ctx.ui.requestRender();
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
return { component, focus: component };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Show the Extension Control Center dashboard.
|
|
112
|
+
* Replaces /status with a unified view of all providers and extensions.
|
|
113
|
+
*/
|
|
114
|
+
showExtensionsDashboard(): void {
|
|
115
|
+
this.showSelector((done) => {
|
|
116
|
+
const dashboard = new ExtensionDashboard(process.cwd(), this.ctx.settingsManager, this.ctx.ui.terminal.rows);
|
|
117
|
+
dashboard.onClose = () => {
|
|
118
|
+
done();
|
|
119
|
+
this.ctx.ui.requestRender();
|
|
120
|
+
};
|
|
121
|
+
return { component: dashboard, focus: dashboard };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Handle setting changes from the settings selector.
|
|
127
|
+
* Most settings are saved directly via SettingsManager in the definitions.
|
|
128
|
+
* This handles side effects and session-specific settings.
|
|
129
|
+
*/
|
|
130
|
+
handleSettingChange(id: string, value: string | boolean): void {
|
|
131
|
+
// Discovery provider toggles
|
|
132
|
+
if (id.startsWith("discovery.")) {
|
|
133
|
+
const providerId = id.replace("discovery.", "");
|
|
134
|
+
if (value) {
|
|
135
|
+
enableProvider(providerId);
|
|
136
|
+
} else {
|
|
137
|
+
disableProvider(providerId);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
switch (id) {
|
|
143
|
+
// Session-managed settings (not in SettingsManager)
|
|
144
|
+
case "autoCompact":
|
|
145
|
+
this.ctx.session.setAutoCompactionEnabled(value as boolean);
|
|
146
|
+
this.ctx.statusLine.setAutoCompactEnabled(value as boolean);
|
|
147
|
+
break;
|
|
148
|
+
case "steeringMode":
|
|
149
|
+
this.ctx.session.setSteeringMode(value as "all" | "one-at-a-time");
|
|
150
|
+
break;
|
|
151
|
+
case "followUpMode":
|
|
152
|
+
this.ctx.session.setFollowUpMode(value as "all" | "one-at-a-time");
|
|
153
|
+
break;
|
|
154
|
+
case "interruptMode":
|
|
155
|
+
this.ctx.session.setInterruptMode(value as "immediate" | "wait");
|
|
156
|
+
break;
|
|
157
|
+
case "thinkingLevel":
|
|
158
|
+
this.ctx.session.setThinkingLevel(value as ThinkingLevel);
|
|
159
|
+
this.ctx.statusLine.invalidate();
|
|
160
|
+
this.ctx.updateEditorBorderColor();
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
// Settings with UI side effects
|
|
164
|
+
case "showImages":
|
|
165
|
+
for (const child of this.ctx.chatContainer.children) {
|
|
166
|
+
if (child instanceof ToolExecutionComponent) {
|
|
167
|
+
child.setShowImages(value as boolean);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
case "hideThinking":
|
|
172
|
+
this.ctx.hideThinkingBlock = value as boolean;
|
|
173
|
+
for (const child of this.ctx.chatContainer.children) {
|
|
174
|
+
if (child instanceof AssistantMessageComponent) {
|
|
175
|
+
child.setHideThinkingBlock(value as boolean);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.ctx.chatContainer.clear();
|
|
179
|
+
this.ctx.rebuildChatFromMessages();
|
|
180
|
+
break;
|
|
181
|
+
case "theme": {
|
|
182
|
+
const result = setTheme(value as string, true);
|
|
183
|
+
this.ctx.statusLine.invalidate();
|
|
184
|
+
this.ctx.updateEditorTopBorder();
|
|
185
|
+
this.ctx.ui.invalidate();
|
|
186
|
+
if (!result.success) {
|
|
187
|
+
this.ctx.showError(`Failed to load theme "${value}": ${result.error}\nFell back to dark theme.`);
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case "symbolPreset": {
|
|
192
|
+
setSymbolPreset(value as "unicode" | "nerd" | "ascii");
|
|
193
|
+
this.ctx.statusLine.invalidate();
|
|
194
|
+
this.ctx.updateEditorTopBorder();
|
|
195
|
+
this.ctx.ui.invalidate();
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case "voiceEnabled": {
|
|
199
|
+
if (!value) {
|
|
200
|
+
this.ctx.voiceAutoModeEnabled = false;
|
|
201
|
+
this.ctx.stopVoiceProgressTimer();
|
|
202
|
+
void this.ctx.voiceSupervisor.stop();
|
|
203
|
+
this.ctx.setVoiceStatus(undefined);
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case "statusLinePreset":
|
|
208
|
+
case "statusLineSeparator":
|
|
209
|
+
case "statusLineShowHooks":
|
|
210
|
+
case "statusLineSegments":
|
|
211
|
+
case "statusLineModelThinking":
|
|
212
|
+
case "statusLinePathAbbreviate":
|
|
213
|
+
case "statusLinePathMaxLength":
|
|
214
|
+
case "statusLinePathStripWorkPrefix":
|
|
215
|
+
case "statusLineGitShowBranch":
|
|
216
|
+
case "statusLineGitShowStaged":
|
|
217
|
+
case "statusLineGitShowUnstaged":
|
|
218
|
+
case "statusLineGitShowUntracked":
|
|
219
|
+
case "statusLineTimeFormat":
|
|
220
|
+
case "statusLineTimeShowSeconds": {
|
|
221
|
+
this.ctx.statusLine.updateSettings(this.ctx.settingsManager.getStatusLineSettings());
|
|
222
|
+
this.ctx.updateEditorTopBorder();
|
|
223
|
+
this.ctx.ui.requestRender();
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Provider settings - update runtime preferences
|
|
228
|
+
case "webSearchProvider":
|
|
229
|
+
setPreferredWebSearchProvider(value as "auto" | "exa" | "perplexity" | "anthropic");
|
|
230
|
+
break;
|
|
231
|
+
case "imageProvider":
|
|
232
|
+
setPreferredImageProvider(value as "auto" | "gemini" | "openrouter");
|
|
233
|
+
break;
|
|
234
|
+
|
|
235
|
+
// All other settings are handled by the definitions (get/set on SettingsManager)
|
|
236
|
+
// No additional side effects needed
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
showModelSelector(options?: { temporaryOnly?: boolean }): void {
|
|
241
|
+
this.showSelector((done) => {
|
|
242
|
+
const selector = new ModelSelectorComponent(
|
|
243
|
+
this.ctx.ui,
|
|
244
|
+
this.ctx.session.model,
|
|
245
|
+
this.ctx.settingsManager,
|
|
246
|
+
this.ctx.session.modelRegistry,
|
|
247
|
+
this.ctx.session.scopedModels,
|
|
248
|
+
async (model, role) => {
|
|
249
|
+
try {
|
|
250
|
+
if (role === "temporary") {
|
|
251
|
+
// Temporary: update agent state but don't persist to settings
|
|
252
|
+
await this.ctx.session.setModelTemporary(model);
|
|
253
|
+
this.ctx.statusLine.invalidate();
|
|
254
|
+
this.ctx.updateEditorBorderColor();
|
|
255
|
+
this.ctx.showStatus(`Temporary model: ${model.id}`);
|
|
256
|
+
done();
|
|
257
|
+
this.ctx.ui.requestRender();
|
|
258
|
+
} else if (role === "default") {
|
|
259
|
+
// Default: update agent state and persist
|
|
260
|
+
await this.ctx.session.setModel(model, role);
|
|
261
|
+
this.ctx.statusLine.invalidate();
|
|
262
|
+
this.ctx.updateEditorBorderColor();
|
|
263
|
+
this.ctx.showStatus(`Default model: ${model.id}`);
|
|
264
|
+
// Don't call done() - selector stays open for role assignment
|
|
265
|
+
} else {
|
|
266
|
+
// Other roles (smol, slow): just update settings, not current model
|
|
267
|
+
const roleLabel = role === "smol" ? "Smol" : role;
|
|
268
|
+
this.ctx.showStatus(`${roleLabel} model: ${model.id}`);
|
|
269
|
+
// Don't call done() - selector stays open
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
() => {
|
|
276
|
+
done();
|
|
277
|
+
this.ctx.ui.requestRender();
|
|
278
|
+
},
|
|
279
|
+
options,
|
|
280
|
+
);
|
|
281
|
+
return { component: selector, focus: selector };
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
showUserMessageSelector(): void {
|
|
286
|
+
const userMessages = this.ctx.session.getUserMessagesForBranching();
|
|
287
|
+
|
|
288
|
+
if (userMessages.length === 0) {
|
|
289
|
+
this.ctx.showStatus("No messages to branch from");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.showSelector((done) => {
|
|
294
|
+
const selector = new UserMessageSelectorComponent(
|
|
295
|
+
userMessages.map((m) => ({ id: m.entryId, text: m.text })),
|
|
296
|
+
async (entryId) => {
|
|
297
|
+
const result = await this.ctx.session.branch(entryId);
|
|
298
|
+
if (result.cancelled) {
|
|
299
|
+
// Hook cancelled the branch
|
|
300
|
+
done();
|
|
301
|
+
this.ctx.ui.requestRender();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.ctx.chatContainer.clear();
|
|
306
|
+
this.ctx.renderInitialMessages();
|
|
307
|
+
this.ctx.editor.setText(result.selectedText);
|
|
308
|
+
done();
|
|
309
|
+
this.ctx.showStatus("Branched to new session");
|
|
310
|
+
},
|
|
311
|
+
() => {
|
|
312
|
+
done();
|
|
313
|
+
this.ctx.ui.requestRender();
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
return { component: selector, focus: selector.getMessageList() };
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
showTreeSelector(): void {
|
|
321
|
+
const tree = this.ctx.sessionManager.getTree();
|
|
322
|
+
const realLeafId = this.ctx.sessionManager.getLeafId();
|
|
323
|
+
|
|
324
|
+
// Find the visible leaf for display (skip metadata entries like labels)
|
|
325
|
+
let visibleLeafId = realLeafId;
|
|
326
|
+
while (visibleLeafId) {
|
|
327
|
+
const entry = this.ctx.sessionManager.getEntry(visibleLeafId);
|
|
328
|
+
if (!entry) break;
|
|
329
|
+
if (entry.type !== "label" && entry.type !== "custom") break;
|
|
330
|
+
visibleLeafId = entry.parentId ?? null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (tree.length === 0) {
|
|
334
|
+
this.ctx.showStatus("No entries in session");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this.showSelector((done) => {
|
|
339
|
+
const selector = new TreeSelectorComponent(
|
|
340
|
+
tree,
|
|
341
|
+
visibleLeafId,
|
|
342
|
+
this.ctx.ui.terminal.rows,
|
|
343
|
+
async (entryId) => {
|
|
344
|
+
// Selecting the visible leaf is a no-op (already there)
|
|
345
|
+
if (entryId === visibleLeafId) {
|
|
346
|
+
done();
|
|
347
|
+
this.ctx.showStatus("Already at this point");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Ask about summarization (or skip if disabled in settings)
|
|
352
|
+
done(); // Close selector first
|
|
353
|
+
|
|
354
|
+
const branchSummariesEnabled = this.ctx.settingsManager.getBranchSummaryEnabled();
|
|
355
|
+
const wantsSummary = branchSummariesEnabled
|
|
356
|
+
? await this.ctx.showHookConfirm(
|
|
357
|
+
"Summarize branch?",
|
|
358
|
+
"Create a summary of the branch you're leaving?",
|
|
359
|
+
)
|
|
360
|
+
: false;
|
|
361
|
+
|
|
362
|
+
// Set up escape handler and loader if summarizing
|
|
363
|
+
let summaryLoader: Loader | undefined;
|
|
364
|
+
const originalOnEscape = this.ctx.editor.onEscape;
|
|
365
|
+
|
|
366
|
+
if (wantsSummary) {
|
|
367
|
+
this.ctx.editor.onEscape = () => {
|
|
368
|
+
this.ctx.session.abortBranchSummary();
|
|
369
|
+
};
|
|
370
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
371
|
+
summaryLoader = new Loader(
|
|
372
|
+
this.ctx.ui,
|
|
373
|
+
(spinner) => theme.fg("accent", spinner),
|
|
374
|
+
(text) => theme.fg("muted", text),
|
|
375
|
+
"Summarizing branch... (esc to cancel)",
|
|
376
|
+
getSymbolTheme().spinnerFrames,
|
|
377
|
+
);
|
|
378
|
+
this.ctx.statusContainer.addChild(summaryLoader);
|
|
379
|
+
this.ctx.ui.requestRender();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const result = await this.ctx.session.navigateTree(entryId, { summarize: wantsSummary });
|
|
384
|
+
|
|
385
|
+
if (result.aborted) {
|
|
386
|
+
// Summarization aborted - re-show tree selector
|
|
387
|
+
this.ctx.showStatus("Branch summarization cancelled");
|
|
388
|
+
this.showTreeSelector();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (result.cancelled) {
|
|
392
|
+
this.ctx.showStatus("Navigation cancelled");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Update UI
|
|
397
|
+
this.ctx.chatContainer.clear();
|
|
398
|
+
this.ctx.renderInitialMessages();
|
|
399
|
+
if (result.editorText) {
|
|
400
|
+
this.ctx.editor.setText(result.editorText);
|
|
401
|
+
}
|
|
402
|
+
this.ctx.showStatus("Navigated to selected point");
|
|
403
|
+
} catch (error) {
|
|
404
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
405
|
+
} finally {
|
|
406
|
+
if (summaryLoader) {
|
|
407
|
+
summaryLoader.stop();
|
|
408
|
+
this.ctx.statusContainer.clear();
|
|
409
|
+
}
|
|
410
|
+
this.ctx.editor.onEscape = originalOnEscape;
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
() => {
|
|
414
|
+
done();
|
|
415
|
+
this.ctx.ui.requestRender();
|
|
416
|
+
},
|
|
417
|
+
(entryId, label) => {
|
|
418
|
+
this.ctx.sessionManager.appendLabelChange(entryId, label);
|
|
419
|
+
this.ctx.ui.requestRender();
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
return { component: selector, focus: selector };
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
showSessionSelector(): void {
|
|
427
|
+
this.showSelector((done) => {
|
|
428
|
+
const sessions = SessionManager.list(
|
|
429
|
+
this.ctx.sessionManager.getCwd(),
|
|
430
|
+
this.ctx.sessionManager.getSessionDir(),
|
|
431
|
+
);
|
|
432
|
+
const selector = new SessionSelectorComponent(
|
|
433
|
+
sessions,
|
|
434
|
+
async (sessionPath) => {
|
|
435
|
+
done();
|
|
436
|
+
await this.handleResumeSession(sessionPath);
|
|
437
|
+
},
|
|
438
|
+
() => {
|
|
439
|
+
done();
|
|
440
|
+
this.ctx.ui.requestRender();
|
|
441
|
+
},
|
|
442
|
+
() => {
|
|
443
|
+
void this.ctx.shutdown();
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
return { component: selector, focus: selector.getSessionList() };
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async handleResumeSession(sessionPath: string): Promise<void> {
|
|
451
|
+
// Stop loading animation
|
|
452
|
+
if (this.ctx.loadingAnimation) {
|
|
453
|
+
this.ctx.loadingAnimation.stop();
|
|
454
|
+
this.ctx.loadingAnimation = undefined;
|
|
455
|
+
}
|
|
456
|
+
this.ctx.statusContainer.clear();
|
|
457
|
+
|
|
458
|
+
// Clear UI state
|
|
459
|
+
this.ctx.pendingMessagesContainer.clear();
|
|
460
|
+
this.ctx.compactionQueuedMessages = [];
|
|
461
|
+
this.ctx.streamingComponent = undefined;
|
|
462
|
+
this.ctx.streamingMessage = undefined;
|
|
463
|
+
this.ctx.pendingTools.clear();
|
|
464
|
+
|
|
465
|
+
// Switch session via AgentSession (emits hook and tool session events)
|
|
466
|
+
await this.ctx.session.switchSession(sessionPath);
|
|
467
|
+
|
|
468
|
+
// Clear and re-render the chat
|
|
469
|
+
this.ctx.chatContainer.clear();
|
|
470
|
+
this.ctx.renderInitialMessages();
|
|
471
|
+
this.ctx.showStatus("Resumed session");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async showOAuthSelector(mode: "login" | "logout"): Promise<void> {
|
|
475
|
+
if (mode === "logout") {
|
|
476
|
+
const providers = this.ctx.session.modelRegistry.authStorage.list();
|
|
477
|
+
const loggedInProviders = providers.filter((p) => this.ctx.session.modelRegistry.authStorage.hasOAuth(p));
|
|
478
|
+
if (loggedInProviders.length === 0) {
|
|
479
|
+
this.ctx.showStatus("No OAuth providers logged in. Use /login first.");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
this.showSelector((done) => {
|
|
485
|
+
const selector = new OAuthSelectorComponent(
|
|
486
|
+
mode,
|
|
487
|
+
this.ctx.session.modelRegistry.authStorage,
|
|
488
|
+
async (providerId: string) => {
|
|
489
|
+
done();
|
|
490
|
+
|
|
491
|
+
if (mode === "login") {
|
|
492
|
+
this.ctx.showStatus(`Logging in to ${providerId}...`);
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
496
|
+
onAuth: (info: { url: string; instructions?: string }) => {
|
|
497
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
498
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", info.url), 1, 0));
|
|
499
|
+
// Use OSC 8 hyperlink escape sequence for clickable link
|
|
500
|
+
const hyperlink = `\x1b]8;;${info.url}\x07Click here to login\x1b]8;;\x07`;
|
|
501
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", hyperlink), 1, 0));
|
|
502
|
+
if (info.instructions) {
|
|
503
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
504
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
505
|
+
}
|
|
506
|
+
this.ctx.ui.requestRender();
|
|
507
|
+
|
|
508
|
+
this.ctx.openInBrowser(info.url);
|
|
509
|
+
},
|
|
510
|
+
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
511
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
512
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", prompt.message), 1, 0));
|
|
513
|
+
if (prompt.placeholder) {
|
|
514
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
|
|
515
|
+
}
|
|
516
|
+
this.ctx.ui.requestRender();
|
|
517
|
+
|
|
518
|
+
return new Promise<string>((resolve) => {
|
|
519
|
+
const codeInput = new Input();
|
|
520
|
+
codeInput.onSubmit = () => {
|
|
521
|
+
const code = codeInput.getValue();
|
|
522
|
+
this.ctx.editorContainer.clear();
|
|
523
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
524
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
525
|
+
resolve(code);
|
|
526
|
+
};
|
|
527
|
+
this.ctx.editorContainer.clear();
|
|
528
|
+
this.ctx.editorContainer.addChild(codeInput);
|
|
529
|
+
this.ctx.ui.setFocus(codeInput);
|
|
530
|
+
this.ctx.ui.requestRender();
|
|
531
|
+
});
|
|
532
|
+
},
|
|
533
|
+
onProgress: (message: string) => {
|
|
534
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
|
|
535
|
+
this.ctx.ui.requestRender();
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
// Refresh models to pick up new baseUrl (e.g., github-copilot)
|
|
539
|
+
await this.ctx.session.modelRegistry.refresh();
|
|
540
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
541
|
+
this.ctx.chatContainer.addChild(
|
|
542
|
+
new Text(
|
|
543
|
+
theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`),
|
|
544
|
+
1,
|
|
545
|
+
0,
|
|
546
|
+
),
|
|
547
|
+
);
|
|
548
|
+
this.ctx.chatContainer.addChild(
|
|
549
|
+
new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0),
|
|
550
|
+
);
|
|
551
|
+
this.ctx.ui.requestRender();
|
|
552
|
+
} catch (error: unknown) {
|
|
553
|
+
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
try {
|
|
557
|
+
await this.ctx.session.modelRegistry.authStorage.logout(providerId);
|
|
558
|
+
// Refresh models to reset baseUrl
|
|
559
|
+
await this.ctx.session.modelRegistry.refresh();
|
|
560
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
561
|
+
this.ctx.chatContainer.addChild(
|
|
562
|
+
new Text(
|
|
563
|
+
theme.fg("success", `${theme.status.success} Successfully logged out of ${providerId}`),
|
|
564
|
+
1,
|
|
565
|
+
0,
|
|
566
|
+
),
|
|
567
|
+
);
|
|
568
|
+
this.ctx.chatContainer.addChild(
|
|
569
|
+
new Text(theme.fg("dim", `Credentials removed from ${getAgentDbPath()}`), 1, 0),
|
|
570
|
+
);
|
|
571
|
+
this.ctx.ui.requestRender();
|
|
572
|
+
} catch (error: unknown) {
|
|
573
|
+
this.ctx.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
() => {
|
|
578
|
+
done();
|
|
579
|
+
this.ctx.ui.requestRender();
|
|
580
|
+
},
|
|
581
|
+
);
|
|
582
|
+
return { component: selector, focus: selector };
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|