@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 * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
5
|
+
import { nanoid } from "nanoid";
|
|
6
|
+
import type { AgentSessionEvent } from "../../../core/agent-session";
|
|
7
|
+
import { generateSessionTitle, setTerminalTitle } from "../../../core/title-generator";
|
|
8
|
+
import { readImageFromClipboard } from "../../../utils/clipboard";
|
|
9
|
+
import { resizeImage } from "../../../utils/image-resize";
|
|
10
|
+
import { theme } from "../theme/theme";
|
|
11
|
+
import type { InteractiveModeContext } from "../types";
|
|
12
|
+
|
|
13
|
+
interface Expandable {
|
|
14
|
+
setExpanded(expanded: boolean): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isExpandable(obj: unknown): obj is Expandable {
|
|
18
|
+
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class InputController {
|
|
22
|
+
constructor(private ctx: InteractiveModeContext) {}
|
|
23
|
+
|
|
24
|
+
setupKeyHandlers(): void {
|
|
25
|
+
this.ctx.editor.onEscape = () => {
|
|
26
|
+
if (this.ctx.loadingAnimation) {
|
|
27
|
+
// Abort and restore queued messages to editor
|
|
28
|
+
const queuedMessages = this.ctx.session.clearQueue();
|
|
29
|
+
const queuedText = [...queuedMessages.steering, ...queuedMessages.followUp].join("\n\n");
|
|
30
|
+
const currentText = this.ctx.editor.getText();
|
|
31
|
+
const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n");
|
|
32
|
+
this.ctx.editor.setText(combinedText);
|
|
33
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
34
|
+
this.ctx.agent.abort();
|
|
35
|
+
} else if (this.ctx.session.isBashRunning) {
|
|
36
|
+
this.ctx.session.abortBash();
|
|
37
|
+
} else if (this.ctx.isBashMode) {
|
|
38
|
+
this.ctx.editor.setText("");
|
|
39
|
+
this.ctx.isBashMode = false;
|
|
40
|
+
this.ctx.updateEditorBorderColor();
|
|
41
|
+
} else if (!this.ctx.editor.getText().trim()) {
|
|
42
|
+
// Double-escape with empty editor triggers /tree or /branch based on setting
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (now - this.ctx.lastEscapeTime < 500) {
|
|
45
|
+
if (this.ctx.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
46
|
+
this.ctx.showTreeSelector();
|
|
47
|
+
} else {
|
|
48
|
+
this.ctx.showUserMessageSelector();
|
|
49
|
+
}
|
|
50
|
+
this.ctx.lastEscapeTime = 0;
|
|
51
|
+
} else {
|
|
52
|
+
this.ctx.lastEscapeTime = now;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.ctx.editor.onCtrlC = () => this.handleCtrlC();
|
|
58
|
+
this.ctx.editor.onCtrlD = () => this.handleCtrlD();
|
|
59
|
+
this.ctx.editor.onCtrlZ = () => this.handleCtrlZ();
|
|
60
|
+
this.ctx.editor.onShiftTab = () => this.cycleThinkingLevel();
|
|
61
|
+
this.ctx.editor.onCtrlP = () => this.cycleRoleModel();
|
|
62
|
+
this.ctx.editor.onShiftCtrlP = () => this.cycleRoleModel({ temporary: true });
|
|
63
|
+
this.ctx.editor.onCtrlY = () => this.ctx.showModelSelector({ temporaryOnly: true });
|
|
64
|
+
|
|
65
|
+
// Global debug handler on TUI (works regardless of focus)
|
|
66
|
+
this.ctx.ui.onDebug = () => this.ctx.handleDebugCommand();
|
|
67
|
+
this.ctx.editor.onCtrlL = () => this.ctx.showModelSelector();
|
|
68
|
+
this.ctx.editor.onCtrlR = () => this.ctx.showHistorySearch();
|
|
69
|
+
this.ctx.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
|
70
|
+
this.ctx.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
|
71
|
+
this.ctx.editor.onCtrlG = () => this.openExternalEditor();
|
|
72
|
+
this.ctx.editor.onQuestionMark = () => this.ctx.handleHotkeysCommand();
|
|
73
|
+
this.ctx.editor.onCtrlV = () => this.handleImagePaste();
|
|
74
|
+
this.ctx.editor.onAltUp = () => this.handleDequeue();
|
|
75
|
+
|
|
76
|
+
// Wire up extension shortcuts
|
|
77
|
+
this.registerExtensionShortcuts();
|
|
78
|
+
|
|
79
|
+
this.ctx.editor.onChange = (text: string) => {
|
|
80
|
+
const wasBashMode = this.ctx.isBashMode;
|
|
81
|
+
this.ctx.isBashMode = text.trimStart().startsWith("!");
|
|
82
|
+
if (wasBashMode !== this.ctx.isBashMode) {
|
|
83
|
+
this.ctx.updateEditorBorderColor();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
this.ctx.editor.onAltEnter = async (text: string) => {
|
|
88
|
+
text = text.trim();
|
|
89
|
+
if (!text) return;
|
|
90
|
+
|
|
91
|
+
// Queue follow-up messages while compaction is running
|
|
92
|
+
if (this.ctx.session.isCompacting) {
|
|
93
|
+
this.ctx.queueCompactionMessage(text, "followUp");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Alt+Enter queues a follow-up message (waits until agent finishes)
|
|
98
|
+
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
|
99
|
+
if (this.ctx.session.isStreaming) {
|
|
100
|
+
this.ctx.editor.addToHistory(text);
|
|
101
|
+
this.ctx.editor.setText("");
|
|
102
|
+
await this.ctx.session.prompt(text, { streamingBehavior: "followUp" });
|
|
103
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
104
|
+
this.ctx.ui.requestRender();
|
|
105
|
+
}
|
|
106
|
+
// If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
|
|
107
|
+
else if (this.ctx.editor.onSubmit) {
|
|
108
|
+
this.ctx.editor.onSubmit(text);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setupEditorSubmitHandler(): void {
|
|
114
|
+
this.ctx.editor.onSubmit = async (text: string) => {
|
|
115
|
+
text = text.trim();
|
|
116
|
+
|
|
117
|
+
// Empty submit while streaming with queued messages: flush queues immediately
|
|
118
|
+
if (!text && this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount > 0) {
|
|
119
|
+
// Abort current stream and let queued messages be processed
|
|
120
|
+
await this.ctx.session.abort();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!text) return;
|
|
125
|
+
|
|
126
|
+
// Handle slash commands
|
|
127
|
+
if (text === "/settings") {
|
|
128
|
+
this.ctx.showSettingsSelector();
|
|
129
|
+
this.ctx.editor.setText("");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (text === "/model") {
|
|
133
|
+
this.ctx.showModelSelector();
|
|
134
|
+
this.ctx.editor.setText("");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (text.startsWith("/export")) {
|
|
138
|
+
await this.ctx.handleExportCommand(text);
|
|
139
|
+
this.ctx.editor.setText("");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (text === "/dump") {
|
|
143
|
+
await this.ctx.handleDumpCommand();
|
|
144
|
+
this.ctx.editor.setText("");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (text === "/share") {
|
|
148
|
+
await this.ctx.handleShareCommand();
|
|
149
|
+
this.ctx.editor.setText("");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (text === "/copy") {
|
|
153
|
+
await this.ctx.handleCopyCommand();
|
|
154
|
+
this.ctx.editor.setText("");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (text === "/session") {
|
|
158
|
+
this.ctx.handleSessionCommand();
|
|
159
|
+
this.ctx.editor.setText("");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (text === "/changelog") {
|
|
163
|
+
this.ctx.handleChangelogCommand();
|
|
164
|
+
this.ctx.editor.setText("");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (text === "/hotkeys") {
|
|
168
|
+
this.ctx.handleHotkeysCommand();
|
|
169
|
+
this.ctx.editor.setText("");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (text === "/extensions" || text === "/status") {
|
|
173
|
+
this.ctx.showExtensionsDashboard();
|
|
174
|
+
this.ctx.editor.setText("");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (text === "/branch") {
|
|
178
|
+
if (this.ctx.settingsManager.getDoubleEscapeAction() === "tree") {
|
|
179
|
+
this.ctx.showTreeSelector();
|
|
180
|
+
} else {
|
|
181
|
+
this.ctx.showUserMessageSelector();
|
|
182
|
+
}
|
|
183
|
+
this.ctx.editor.setText("");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (text === "/tree") {
|
|
187
|
+
this.ctx.showTreeSelector();
|
|
188
|
+
this.ctx.editor.setText("");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (text === "/login") {
|
|
192
|
+
this.ctx.showOAuthSelector("login");
|
|
193
|
+
this.ctx.editor.setText("");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (text === "/logout") {
|
|
197
|
+
this.ctx.showOAuthSelector("logout");
|
|
198
|
+
this.ctx.editor.setText("");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (text === "/new") {
|
|
202
|
+
this.ctx.editor.setText("");
|
|
203
|
+
await this.ctx.handleClearCommand();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (text === "/compact" || text.startsWith("/compact ")) {
|
|
207
|
+
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
|
208
|
+
this.ctx.editor.setText("");
|
|
209
|
+
await this.ctx.handleCompactCommand(customInstructions);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (text === "/background" || text === "/bg") {
|
|
213
|
+
this.ctx.editor.setText("");
|
|
214
|
+
this.handleBackgroundCommand();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (text === "/debug") {
|
|
218
|
+
this.ctx.handleDebugCommand();
|
|
219
|
+
this.ctx.editor.setText("");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (text === "/arminsayshi") {
|
|
223
|
+
this.ctx.handleArminSaysHi();
|
|
224
|
+
this.ctx.editor.setText("");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (text === "/resume") {
|
|
228
|
+
this.ctx.showSessionSelector();
|
|
229
|
+
this.ctx.editor.setText("");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (text === "/exit") {
|
|
233
|
+
this.ctx.editor.setText("");
|
|
234
|
+
void this.ctx.shutdown();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle bash command (! for normal, !! for excluded from context)
|
|
239
|
+
if (text.startsWith("!")) {
|
|
240
|
+
const isExcluded = text.startsWith("!!");
|
|
241
|
+
const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
|
|
242
|
+
if (command) {
|
|
243
|
+
if (this.ctx.session.isBashRunning) {
|
|
244
|
+
this.ctx.showWarning("A bash command is already running. Press Esc to cancel it first.");
|
|
245
|
+
this.ctx.editor.setText(text);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this.ctx.editor.addToHistory(text);
|
|
249
|
+
await this.ctx.handleBashCommand(command, isExcluded);
|
|
250
|
+
this.ctx.isBashMode = false;
|
|
251
|
+
this.ctx.updateEditorBorderColor();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Queue input during compaction
|
|
257
|
+
if (this.ctx.session.isCompacting) {
|
|
258
|
+
if (this.ctx.pendingImages.length > 0) {
|
|
259
|
+
this.ctx.showStatus("Compaction in progress. Retry after it completes to send images.");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.ctx.queueCompactionMessage(text, "steer");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// If streaming, use prompt() with steer behavior
|
|
267
|
+
// This handles extension commands (execute immediately), prompt template expansion, and queueing
|
|
268
|
+
if (this.ctx.session.isStreaming) {
|
|
269
|
+
this.ctx.editor.addToHistory(text);
|
|
270
|
+
this.ctx.editor.setText("");
|
|
271
|
+
const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
|
|
272
|
+
this.ctx.pendingImages = [];
|
|
273
|
+
await this.ctx.session.prompt(text, { streamingBehavior: "steer", images });
|
|
274
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
275
|
+
this.ctx.ui.requestRender();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Normal message submission
|
|
280
|
+
// First, move any pending bash components to chat
|
|
281
|
+
this.ctx.flushPendingBashComponents();
|
|
282
|
+
|
|
283
|
+
// Generate session title on first message
|
|
284
|
+
const hasUserMessages = this.ctx.agent.state.messages.some((m: AgentMessage) => m.role === "user");
|
|
285
|
+
if (!hasUserMessages && !this.ctx.sessionManager.getSessionTitle()) {
|
|
286
|
+
const registry = this.ctx.session.modelRegistry;
|
|
287
|
+
const smolModel = this.ctx.settingsManager.getModelRole("smol");
|
|
288
|
+
generateSessionTitle(text, registry, smolModel, this.ctx.session.sessionId)
|
|
289
|
+
.then(async (title) => {
|
|
290
|
+
if (title) {
|
|
291
|
+
await this.ctx.sessionManager.setSessionTitle(title);
|
|
292
|
+
setTerminalTitle(`omp: ${title}`);
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
.catch(() => {});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (this.ctx.onInputCallback) {
|
|
299
|
+
// Include any pending images from clipboard paste
|
|
300
|
+
const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
|
|
301
|
+
this.ctx.pendingImages = [];
|
|
302
|
+
this.ctx.onInputCallback({ text, images });
|
|
303
|
+
}
|
|
304
|
+
this.ctx.editor.addToHistory(text);
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
handleCtrlC(): void {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
if (now - this.ctx.lastSigintTime < 500) {
|
|
311
|
+
void this.ctx.shutdown();
|
|
312
|
+
} else {
|
|
313
|
+
this.ctx.clearEditor();
|
|
314
|
+
this.ctx.lastSigintTime = now;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
handleCtrlD(): void {
|
|
319
|
+
// Only called when editor is empty (enforced by CustomEditor)
|
|
320
|
+
void this.ctx.shutdown();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
handleCtrlZ(): void {
|
|
324
|
+
// Set up handler to restore TUI when resumed
|
|
325
|
+
process.once("SIGCONT", () => {
|
|
326
|
+
this.ctx.ui.start();
|
|
327
|
+
this.ctx.ui.requestRender(true);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Stop the TUI (restore terminal to normal mode)
|
|
331
|
+
this.ctx.ui.stop();
|
|
332
|
+
|
|
333
|
+
// Send SIGTSTP to process group (pid=0 means all processes in group)
|
|
334
|
+
process.kill(0, "SIGTSTP");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
handleDequeue(): void {
|
|
338
|
+
const message = this.ctx.session.popLastQueuedMessage();
|
|
339
|
+
if (!message) return;
|
|
340
|
+
|
|
341
|
+
// Prepend to existing editor text (if any)
|
|
342
|
+
const currentText = this.ctx.editor.getText();
|
|
343
|
+
const newText = currentText ? `${message}\n\n${currentText}` : message;
|
|
344
|
+
this.ctx.editor.setText(newText);
|
|
345
|
+
this.ctx.updatePendingMessagesDisplay();
|
|
346
|
+
this.ctx.ui.requestRender();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
handleBackgroundCommand(): void {
|
|
350
|
+
if (this.ctx.isBackgrounded) {
|
|
351
|
+
this.ctx.showStatus("Background mode already enabled");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!this.ctx.session.isStreaming && this.ctx.session.queuedMessageCount === 0) {
|
|
355
|
+
this.ctx.showWarning("Agent is idle; nothing to background");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this.ctx.isBackgrounded = true;
|
|
360
|
+
const backgroundUiContext = this.ctx.createBackgroundUiContext();
|
|
361
|
+
|
|
362
|
+
// Background mode disables interactive UI so tools like ask fail fast.
|
|
363
|
+
this.ctx.setToolUIContext(backgroundUiContext, false);
|
|
364
|
+
this.ctx.initializeHookRunner(backgroundUiContext, false);
|
|
365
|
+
|
|
366
|
+
if (this.ctx.loadingAnimation) {
|
|
367
|
+
this.ctx.loadingAnimation.stop();
|
|
368
|
+
this.ctx.loadingAnimation = undefined;
|
|
369
|
+
}
|
|
370
|
+
if (this.ctx.autoCompactionLoader) {
|
|
371
|
+
this.ctx.autoCompactionLoader.stop();
|
|
372
|
+
this.ctx.autoCompactionLoader = undefined;
|
|
373
|
+
}
|
|
374
|
+
if (this.ctx.retryLoader) {
|
|
375
|
+
this.ctx.retryLoader.stop();
|
|
376
|
+
this.ctx.retryLoader = undefined;
|
|
377
|
+
}
|
|
378
|
+
this.ctx.statusContainer.clear();
|
|
379
|
+
this.ctx.statusLine.dispose();
|
|
380
|
+
|
|
381
|
+
if (this.ctx.unsubscribe) {
|
|
382
|
+
this.ctx.unsubscribe();
|
|
383
|
+
}
|
|
384
|
+
this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
|
|
385
|
+
await this.ctx.handleBackgroundEvent(event);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Backgrounding keeps the current process to preserve in-flight agent state.
|
|
389
|
+
if (this.ctx.isInitialized) {
|
|
390
|
+
this.ctx.ui.stop();
|
|
391
|
+
this.ctx.isInitialized = false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
process.stdout.write("Background mode enabled. Run `bg` to continue in background.\n");
|
|
395
|
+
|
|
396
|
+
if (process.platform === "win32" || !process.stdout.isTTY) {
|
|
397
|
+
process.stdout.write("Backgrounding requires POSIX job control; continuing in foreground.\n");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
process.kill(0, "SIGTSTP");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async handleImagePaste(): Promise<boolean> {
|
|
405
|
+
try {
|
|
406
|
+
const image = await readImageFromClipboard();
|
|
407
|
+
if (image) {
|
|
408
|
+
let imageData = image;
|
|
409
|
+
if (this.ctx.settingsManager.getImageAutoResize()) {
|
|
410
|
+
try {
|
|
411
|
+
const resized = await resizeImage({
|
|
412
|
+
type: "image",
|
|
413
|
+
data: image.data,
|
|
414
|
+
mimeType: image.mimeType,
|
|
415
|
+
});
|
|
416
|
+
imageData = { data: resized.data, mimeType: resized.mimeType };
|
|
417
|
+
} catch {
|
|
418
|
+
imageData = image;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.ctx.pendingImages.push({
|
|
423
|
+
type: "image",
|
|
424
|
+
data: imageData.data,
|
|
425
|
+
mimeType: imageData.mimeType,
|
|
426
|
+
});
|
|
427
|
+
// Insert styled placeholder at cursor like Claude does
|
|
428
|
+
const imageNum = this.ctx.pendingImages.length;
|
|
429
|
+
const placeholder = theme.bold(theme.underline(`[Image #${imageNum}]`));
|
|
430
|
+
this.ctx.editor.insertText(`${placeholder} `);
|
|
431
|
+
this.ctx.ui.requestRender();
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
// No image in clipboard - show hint
|
|
435
|
+
this.ctx.showStatus("No image in clipboard (use terminal paste for text)");
|
|
436
|
+
return false;
|
|
437
|
+
} catch {
|
|
438
|
+
this.ctx.showStatus("Failed to read clipboard");
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
cycleThinkingLevel(): void {
|
|
444
|
+
const newLevel = this.ctx.session.cycleThinkingLevel();
|
|
445
|
+
if (newLevel === undefined) {
|
|
446
|
+
this.ctx.showStatus("Current model does not support thinking");
|
|
447
|
+
} else {
|
|
448
|
+
this.ctx.statusLine.invalidate();
|
|
449
|
+
this.ctx.updateEditorBorderColor();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
|
|
454
|
+
try {
|
|
455
|
+
const roleOrder = ["slow", "default", "smol"];
|
|
456
|
+
const result = await this.ctx.session.cycleRoleModels(roleOrder, options);
|
|
457
|
+
if (!result) {
|
|
458
|
+
this.ctx.showStatus("Only one role model available");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
this.ctx.statusLine.invalidate();
|
|
463
|
+
this.ctx.updateEditorBorderColor();
|
|
464
|
+
const roleLabel = result.role === "default" ? "default" : result.role;
|
|
465
|
+
const roleLabelStyled = theme.bold(theme.fg("accent", roleLabel));
|
|
466
|
+
const thinkingStr =
|
|
467
|
+
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
|
468
|
+
const tempLabel = options?.temporary ? " (temporary)" : "";
|
|
469
|
+
const cycleSeparator = theme.fg("dim", " > ");
|
|
470
|
+
const cycleLabel = roleOrder
|
|
471
|
+
.map((role) => {
|
|
472
|
+
if (role === result.role) {
|
|
473
|
+
return theme.bold(theme.fg("accent", role));
|
|
474
|
+
}
|
|
475
|
+
return theme.fg("muted", role);
|
|
476
|
+
})
|
|
477
|
+
.join(cycleSeparator);
|
|
478
|
+
const orderLabel = ` (cycle: ${cycleLabel})`;
|
|
479
|
+
this.ctx.showStatus(
|
|
480
|
+
`Switched to ${roleLabelStyled}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}${orderLabel}`,
|
|
481
|
+
{ dim: false },
|
|
482
|
+
);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
toggleToolOutputExpansion(): void {
|
|
489
|
+
this.ctx.toolOutputExpanded = !this.ctx.toolOutputExpanded;
|
|
490
|
+
for (const child of this.ctx.chatContainer.children) {
|
|
491
|
+
if (isExpandable(child)) {
|
|
492
|
+
child.setExpanded(this.ctx.toolOutputExpanded);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
this.ctx.ui.requestRender();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
toggleThinkingBlockVisibility(): void {
|
|
499
|
+
this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
|
|
500
|
+
this.ctx.settingsManager.setHideThinkingBlock(this.ctx.hideThinkingBlock);
|
|
501
|
+
|
|
502
|
+
// Rebuild chat from session messages
|
|
503
|
+
this.ctx.chatContainer.clear();
|
|
504
|
+
this.ctx.rebuildChatFromMessages();
|
|
505
|
+
|
|
506
|
+
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
507
|
+
if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
|
|
508
|
+
this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.hideThinkingBlock);
|
|
509
|
+
this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
|
|
510
|
+
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
this.ctx.showStatus(`Thinking blocks: ${this.ctx.hideThinkingBlock ? "hidden" : "visible"}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
openExternalEditor(): void {
|
|
517
|
+
// Determine editor (respect $VISUAL, then $EDITOR)
|
|
518
|
+
const editorCmd = process.env.VISUAL || process.env.EDITOR;
|
|
519
|
+
if (!editorCmd) {
|
|
520
|
+
this.ctx.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const currentText = this.ctx.editor.getText();
|
|
525
|
+
const tmpFile = path.join(os.tmpdir(), `omp-editor-${nanoid()}.omp.md`);
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
// Write current content to temp file
|
|
529
|
+
fs.writeFileSync(tmpFile, currentText, "utf-8");
|
|
530
|
+
|
|
531
|
+
// Stop TUI to release terminal
|
|
532
|
+
this.ctx.ui.stop();
|
|
533
|
+
|
|
534
|
+
// Split by space to support editor arguments (e.g., "code --wait")
|
|
535
|
+
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
536
|
+
|
|
537
|
+
// Spawn editor synchronously with inherited stdio for interactive editing
|
|
538
|
+
const result = Bun.spawnSync([editor, ...editorArgs, tmpFile], {
|
|
539
|
+
stdin: "inherit",
|
|
540
|
+
stdout: "inherit",
|
|
541
|
+
stderr: "inherit",
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// On successful exit (exitCode 0), replace editor content
|
|
545
|
+
if (result.exitCode === 0) {
|
|
546
|
+
const newContent = fs.readFileSync(tmpFile, "utf-8").replace(/\n$/, "");
|
|
547
|
+
this.ctx.editor.setText(newContent);
|
|
548
|
+
}
|
|
549
|
+
// On non-zero exit, keep original text (no action needed)
|
|
550
|
+
} finally {
|
|
551
|
+
// Clean up temp file
|
|
552
|
+
try {
|
|
553
|
+
fs.unlinkSync(tmpFile);
|
|
554
|
+
} catch {
|
|
555
|
+
// Ignore cleanup errors
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Restart TUI
|
|
559
|
+
this.ctx.ui.start();
|
|
560
|
+
this.ctx.ui.requestRender();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
registerExtensionShortcuts(): void {
|
|
565
|
+
const runner = this.ctx.session.extensionRunner;
|
|
566
|
+
if (!runner) return;
|
|
567
|
+
|
|
568
|
+
const shortcuts = runner.getShortcuts();
|
|
569
|
+
for (const [keyId, shortcut] of shortcuts) {
|
|
570
|
+
this.ctx.editor.setCustomKeyHandler(keyId, () => {
|
|
571
|
+
const ctx = runner.createCommandContext();
|
|
572
|
+
try {
|
|
573
|
+
shortcut.handler(ctx);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
runner.emitError({
|
|
576
|
+
extensionPath: shortcut.extensionPath,
|
|
577
|
+
event: "shortcut",
|
|
578
|
+
error: err instanceof Error ? err.message : String(err),
|
|
579
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|