@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,527 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { Loader, Markdown, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { nanoid } from "nanoid";
|
|
6
|
+
import { getDebugLogPath } from "../../../config";
|
|
7
|
+
import { loadCustomShare } from "../../../core/custom-share";
|
|
8
|
+
import { createCompactionSummaryMessage } from "../../../core/messages";
|
|
9
|
+
import type { TruncationResult } from "../../../core/tools/truncate";
|
|
10
|
+
import { getChangelogPath, parseChangelog } from "../../../utils/changelog";
|
|
11
|
+
import { copyToClipboard } from "../../../utils/clipboard";
|
|
12
|
+
import { ArminComponent } from "../components/armin";
|
|
13
|
+
import { BashExecutionComponent } from "../components/bash-execution";
|
|
14
|
+
import { BorderedLoader } from "../components/bordered-loader";
|
|
15
|
+
import { DynamicBorder } from "../components/dynamic-border";
|
|
16
|
+
import { getMarkdownTheme, getSymbolTheme, theme } from "../theme/theme";
|
|
17
|
+
import type { InteractiveModeContext } from "../types";
|
|
18
|
+
|
|
19
|
+
export class CommandController {
|
|
20
|
+
constructor(private readonly ctx: InteractiveModeContext) {}
|
|
21
|
+
|
|
22
|
+
openInBrowser(urlOrPath: string): void {
|
|
23
|
+
try {
|
|
24
|
+
const args =
|
|
25
|
+
process.platform === "darwin"
|
|
26
|
+
? ["open", urlOrPath]
|
|
27
|
+
: process.platform === "win32"
|
|
28
|
+
? ["cmd", "/c", "start", "", urlOrPath]
|
|
29
|
+
: ["xdg-open", urlOrPath];
|
|
30
|
+
Bun.spawn(args, { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
31
|
+
} catch {
|
|
32
|
+
// Best-effort: browser opening is non-critical
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async handleExportCommand(text: string): Promise<void> {
|
|
37
|
+
const parts = text.split(/\s+/);
|
|
38
|
+
const arg = parts.length > 1 ? parts[1] : undefined;
|
|
39
|
+
|
|
40
|
+
if (arg === "--copy" || arg === "clipboard" || arg === "copy") {
|
|
41
|
+
this.ctx.showWarning("Use /dump to copy the session to clipboard.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const filePath = await this.ctx.session.exportToHtml(arg);
|
|
47
|
+
this.ctx.showStatus(`Session exported to: ${filePath}`);
|
|
48
|
+
this.openInBrowser(filePath);
|
|
49
|
+
} catch (error: unknown) {
|
|
50
|
+
this.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async handleDumpCommand(): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
const formatted = this.ctx.session.formatSessionAsText();
|
|
57
|
+
if (!formatted) {
|
|
58
|
+
this.ctx.showError("No messages to dump yet.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await copyToClipboard(formatted);
|
|
62
|
+
this.ctx.showStatus("Session copied to clipboard");
|
|
63
|
+
} catch (error: unknown) {
|
|
64
|
+
this.ctx.showError(`Failed to copy session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async handleShareCommand(): Promise<void> {
|
|
69
|
+
const tmpFile = path.join(os.tmpdir(), `${nanoid()}.html`);
|
|
70
|
+
try {
|
|
71
|
+
await this.ctx.session.exportToHtml(tmpFile);
|
|
72
|
+
} catch (error: unknown) {
|
|
73
|
+
this.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const customShare = await loadCustomShare();
|
|
79
|
+
if (customShare) {
|
|
80
|
+
const loader = new BorderedLoader(this.ctx.ui, theme, "Sharing...");
|
|
81
|
+
this.ctx.editorContainer.clear();
|
|
82
|
+
this.ctx.editorContainer.addChild(loader);
|
|
83
|
+
this.ctx.ui.setFocus(loader);
|
|
84
|
+
this.ctx.ui.requestRender();
|
|
85
|
+
|
|
86
|
+
const restoreEditor = () => {
|
|
87
|
+
loader.dispose();
|
|
88
|
+
this.ctx.editorContainer.clear();
|
|
89
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
90
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
91
|
+
try {
|
|
92
|
+
fs.unlinkSync(tmpFile);
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore cleanup errors
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const result = await customShare.fn(tmpFile);
|
|
100
|
+
restoreEditor();
|
|
101
|
+
|
|
102
|
+
if (typeof result === "string") {
|
|
103
|
+
this.ctx.showStatus(`Share URL: ${result}`);
|
|
104
|
+
this.openInBrowser(result);
|
|
105
|
+
} else if (result) {
|
|
106
|
+
const parts: string[] = [];
|
|
107
|
+
if (result.url) parts.push(`Share URL: ${result.url}`);
|
|
108
|
+
if (result.message) parts.push(result.message);
|
|
109
|
+
if (parts.length > 0) this.ctx.showStatus(parts.join("\n"));
|
|
110
|
+
if (result.url) this.openInBrowser(result.url);
|
|
111
|
+
} else {
|
|
112
|
+
this.ctx.showStatus("Session shared");
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
restoreEditor();
|
|
117
|
+
this.ctx.showError(`Custom share failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
try {
|
|
123
|
+
fs.unlinkSync(tmpFile);
|
|
124
|
+
} catch {
|
|
125
|
+
// Ignore cleanup errors
|
|
126
|
+
}
|
|
127
|
+
this.ctx.showError(err instanceof Error ? err.message : String(err));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const authResult = Bun.spawnSync(["gh", "auth", "status"]);
|
|
133
|
+
if (authResult.exitCode !== 0) {
|
|
134
|
+
try {
|
|
135
|
+
fs.unlinkSync(tmpFile);
|
|
136
|
+
} catch {}
|
|
137
|
+
this.ctx.showError("GitHub CLI is not logged in. Run 'gh auth login' first.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
try {
|
|
142
|
+
fs.unlinkSync(tmpFile);
|
|
143
|
+
} catch {}
|
|
144
|
+
this.ctx.showError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const loader = new BorderedLoader(this.ctx.ui, theme, "Creating gist...");
|
|
149
|
+
this.ctx.editorContainer.clear();
|
|
150
|
+
this.ctx.editorContainer.addChild(loader);
|
|
151
|
+
this.ctx.ui.setFocus(loader);
|
|
152
|
+
this.ctx.ui.requestRender();
|
|
153
|
+
|
|
154
|
+
const restoreEditor = () => {
|
|
155
|
+
loader.dispose();
|
|
156
|
+
this.ctx.editorContainer.clear();
|
|
157
|
+
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
158
|
+
this.ctx.ui.setFocus(this.ctx.editor);
|
|
159
|
+
try {
|
|
160
|
+
fs.unlinkSync(tmpFile);
|
|
161
|
+
} catch {
|
|
162
|
+
// Ignore cleanup errors
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
let proc: ReturnType<typeof Bun.spawn> | null = null;
|
|
167
|
+
|
|
168
|
+
loader.onAbort = () => {
|
|
169
|
+
proc?.kill();
|
|
170
|
+
restoreEditor();
|
|
171
|
+
this.ctx.showStatus("Share cancelled");
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => {
|
|
176
|
+
proc = Bun.spawn(["gh", "gist", "create", "--public=false", tmpFile], {
|
|
177
|
+
stdout: "pipe",
|
|
178
|
+
stderr: "pipe",
|
|
179
|
+
});
|
|
180
|
+
let stdout = "";
|
|
181
|
+
let stderr = "";
|
|
182
|
+
|
|
183
|
+
const stdoutReader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
|
|
184
|
+
const stderrReader = (proc.stderr as ReadableStream<Uint8Array>).getReader();
|
|
185
|
+
const decoder = new TextDecoder();
|
|
186
|
+
|
|
187
|
+
(async () => {
|
|
188
|
+
try {
|
|
189
|
+
while (true) {
|
|
190
|
+
const { done, value } = await stdoutReader.read();
|
|
191
|
+
if (done) break;
|
|
192
|
+
stdout += decoder.decode(value);
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
195
|
+
})();
|
|
196
|
+
|
|
197
|
+
(async () => {
|
|
198
|
+
try {
|
|
199
|
+
while (true) {
|
|
200
|
+
const { done, value } = await stderrReader.read();
|
|
201
|
+
if (done) break;
|
|
202
|
+
stderr += decoder.decode(value);
|
|
203
|
+
}
|
|
204
|
+
} catch {}
|
|
205
|
+
})();
|
|
206
|
+
|
|
207
|
+
proc.exited.then((code) => resolve({ stdout, stderr, code }));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (loader.signal.aborted) return;
|
|
211
|
+
|
|
212
|
+
restoreEditor();
|
|
213
|
+
|
|
214
|
+
if (result.code !== 0) {
|
|
215
|
+
const errorMsg = result.stderr?.trim() || "Unknown error";
|
|
216
|
+
this.ctx.showError(`Failed to create gist: ${errorMsg}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const gistUrl = result.stdout?.trim();
|
|
221
|
+
const gistId = gistUrl?.split("/").pop();
|
|
222
|
+
if (!gistId) {
|
|
223
|
+
this.ctx.showError("Failed to parse gist ID from gh output");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const previewUrl = `https://gistpreview.github.io/?${gistId}`;
|
|
228
|
+
this.ctx.showStatus(`Share URL: ${previewUrl}\nGist: ${gistUrl}`);
|
|
229
|
+
this.openInBrowser(previewUrl);
|
|
230
|
+
} catch (error: unknown) {
|
|
231
|
+
if (!loader.signal.aborted) {
|
|
232
|
+
restoreEditor();
|
|
233
|
+
this.ctx.showError(`Failed to create gist: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async handleCopyCommand(): Promise<void> {
|
|
239
|
+
const text = this.ctx.session.getLastAssistantText();
|
|
240
|
+
if (!text) {
|
|
241
|
+
this.ctx.showError("No agent messages to copy yet.");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
await copyToClipboard(text);
|
|
247
|
+
this.ctx.showStatus("Copied last agent message to clipboard");
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
handleSessionCommand(): void {
|
|
254
|
+
const stats = this.ctx.session.getSessionStats();
|
|
255
|
+
|
|
256
|
+
let info = `${theme.bold("Session Info")}\n\n`;
|
|
257
|
+
info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
|
|
258
|
+
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
|
|
259
|
+
info += `${theme.bold("Messages")}\n`;
|
|
260
|
+
info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
|
|
261
|
+
info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
|
|
262
|
+
info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;
|
|
263
|
+
info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
|
|
264
|
+
info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
|
|
265
|
+
info += `${theme.bold("Tokens")}\n`;
|
|
266
|
+
info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
|
|
267
|
+
info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
|
|
268
|
+
if (stats.tokens.cacheRead > 0) {
|
|
269
|
+
info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
|
|
270
|
+
}
|
|
271
|
+
if (stats.tokens.cacheWrite > 0) {
|
|
272
|
+
info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
|
|
273
|
+
}
|
|
274
|
+
info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
|
|
275
|
+
|
|
276
|
+
if (stats.cost > 0) {
|
|
277
|
+
info += `\n${theme.bold("Cost")}\n`;
|
|
278
|
+
info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
282
|
+
this.ctx.chatContainer.addChild(new Text(info, 1, 0));
|
|
283
|
+
this.ctx.ui.requestRender();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
handleChangelogCommand(): void {
|
|
287
|
+
const changelogPath = getChangelogPath();
|
|
288
|
+
const allEntries = parseChangelog(changelogPath);
|
|
289
|
+
|
|
290
|
+
const changelogMarkdown =
|
|
291
|
+
allEntries.length > 0
|
|
292
|
+
? allEntries
|
|
293
|
+
.reverse()
|
|
294
|
+
.map((e) => e.content)
|
|
295
|
+
.join("\n\n")
|
|
296
|
+
: "No changelog entries found.";
|
|
297
|
+
|
|
298
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
299
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
300
|
+
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
|
301
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
302
|
+
this.ctx.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, getMarkdownTheme()));
|
|
303
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
304
|
+
this.ctx.ui.requestRender();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
handleHotkeysCommand(): void {
|
|
308
|
+
const hotkeys = `
|
|
309
|
+
**Navigation**
|
|
310
|
+
| Key | Action |
|
|
311
|
+
|-----|--------|
|
|
312
|
+
| \`Arrow keys\` | Move cursor / browse history (Up when empty) |
|
|
313
|
+
| \`Option+Left/Right\` | Move by word |
|
|
314
|
+
| \`Ctrl+A\` / \`Home\` / \`Cmd+Left\` | Start of line |
|
|
315
|
+
| \`Ctrl+E\` / \`End\` / \`Cmd+Right\` | End of line |
|
|
316
|
+
|
|
317
|
+
**Editing**
|
|
318
|
+
| Key | Action |
|
|
319
|
+
|-----|--------|
|
|
320
|
+
| \`Enter\` | Send message |
|
|
321
|
+
| \`Shift+Enter\` / \`Alt+Enter\` | New line |
|
|
322
|
+
| \`Ctrl+W\` / \`Option+Backspace\` | Delete word backwards |
|
|
323
|
+
| \`Ctrl+U\` | Delete to start of line |
|
|
324
|
+
| \`Ctrl+K\` | Delete to end of line |
|
|
325
|
+
|
|
326
|
+
**Other**
|
|
327
|
+
| Key | Action |
|
|
328
|
+
|-----|--------|
|
|
329
|
+
| \`Tab\` | Path completion / accept autocomplete |
|
|
330
|
+
| \`Escape\` | Cancel autocomplete / abort streaming |
|
|
331
|
+
| \`Ctrl+C\` | Clear editor (first) / exit (second) |
|
|
332
|
+
| \`Ctrl+D\` | Exit (when editor is empty) |
|
|
333
|
+
| \`Ctrl+Z\` | Suspend to background |
|
|
334
|
+
| \`Shift+Tab\` | Cycle thinking level |
|
|
335
|
+
| \`Ctrl+P\` | Cycle role models (slow/default/smol) |
|
|
336
|
+
| \`Shift+Ctrl+P\` | Cycle role models (temporary) |
|
|
337
|
+
| \`Ctrl+Y\` | Select model (temporary) |
|
|
338
|
+
| \`Ctrl+L\` | Select model (set roles) |
|
|
339
|
+
| \`Ctrl+R\` | Search prompt history |
|
|
340
|
+
| \`Ctrl+O\` | Toggle tool output expansion |
|
|
341
|
+
| \`Ctrl+T\` | Toggle thinking block visibility |
|
|
342
|
+
| \`Ctrl+G\` | Edit message in external editor |
|
|
343
|
+
| \`/\` | Slash commands |
|
|
344
|
+
| \`!\` | Run bash command |
|
|
345
|
+
| \`!!\` | Run bash command (excluded from context) |
|
|
346
|
+
`;
|
|
347
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
348
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
349
|
+
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
|
|
350
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
351
|
+
this.ctx.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, getMarkdownTheme()));
|
|
352
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
353
|
+
this.ctx.ui.requestRender();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async handleClearCommand(): Promise<void> {
|
|
357
|
+
if (this.ctx.loadingAnimation) {
|
|
358
|
+
this.ctx.loadingAnimation.stop();
|
|
359
|
+
this.ctx.loadingAnimation = undefined;
|
|
360
|
+
}
|
|
361
|
+
this.ctx.statusContainer.clear();
|
|
362
|
+
|
|
363
|
+
await this.ctx.session.newSession();
|
|
364
|
+
|
|
365
|
+
this.ctx.statusLine.invalidate();
|
|
366
|
+
this.ctx.updateEditorTopBorder();
|
|
367
|
+
|
|
368
|
+
this.ctx.chatContainer.clear();
|
|
369
|
+
this.ctx.pendingMessagesContainer.clear();
|
|
370
|
+
this.ctx.compactionQueuedMessages = [];
|
|
371
|
+
this.ctx.streamingComponent = undefined;
|
|
372
|
+
this.ctx.streamingMessage = undefined;
|
|
373
|
+
this.ctx.pendingTools.clear();
|
|
374
|
+
|
|
375
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
376
|
+
this.ctx.chatContainer.addChild(
|
|
377
|
+
new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
|
|
378
|
+
);
|
|
379
|
+
this.ctx.ui.requestRender();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
handleDebugCommand(): void {
|
|
383
|
+
const width = this.ctx.ui.terminal.columns;
|
|
384
|
+
const allLines = this.ctx.ui.render(width);
|
|
385
|
+
|
|
386
|
+
const debugLogPath = getDebugLogPath();
|
|
387
|
+
const debugData = [
|
|
388
|
+
`Debug output at ${new Date().toISOString()}`,
|
|
389
|
+
`Terminal width: ${width}`,
|
|
390
|
+
`Total lines: ${allLines.length}`,
|
|
391
|
+
"",
|
|
392
|
+
"=== All rendered lines with visible widths ===",
|
|
393
|
+
...allLines.map((line, idx) => {
|
|
394
|
+
const vw = visibleWidth(line);
|
|
395
|
+
const escaped = JSON.stringify(line);
|
|
396
|
+
return `[${idx}] (w=${vw}) ${escaped}`;
|
|
397
|
+
}),
|
|
398
|
+
"",
|
|
399
|
+
"=== Agent messages (JSONL) ===",
|
|
400
|
+
...this.ctx.session.messages.map((msg) => JSON.stringify(msg)),
|
|
401
|
+
"",
|
|
402
|
+
].join("\n");
|
|
403
|
+
|
|
404
|
+
fs.mkdirSync(path.dirname(debugLogPath), { recursive: true });
|
|
405
|
+
fs.writeFileSync(debugLogPath, debugData);
|
|
406
|
+
|
|
407
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
408
|
+
this.ctx.chatContainer.addChild(
|
|
409
|
+
new Text(
|
|
410
|
+
`${theme.fg("accent", `${theme.status.success} Debug log written`)}\n${theme.fg("muted", debugLogPath)}`,
|
|
411
|
+
1,
|
|
412
|
+
1,
|
|
413
|
+
),
|
|
414
|
+
);
|
|
415
|
+
this.ctx.ui.requestRender();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
handleArminSaysHi(): void {
|
|
419
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
420
|
+
this.ctx.chatContainer.addChild(new ArminComponent(this.ctx.ui));
|
|
421
|
+
this.ctx.ui.requestRender();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
|
|
425
|
+
const isDeferred = this.ctx.session.isStreaming;
|
|
426
|
+
this.ctx.bashComponent = new BashExecutionComponent(command, this.ctx.ui, excludeFromContext);
|
|
427
|
+
|
|
428
|
+
if (isDeferred) {
|
|
429
|
+
this.ctx.pendingMessagesContainer.addChild(this.ctx.bashComponent);
|
|
430
|
+
this.ctx.pendingBashComponents.push(this.ctx.bashComponent);
|
|
431
|
+
} else {
|
|
432
|
+
this.ctx.chatContainer.addChild(this.ctx.bashComponent);
|
|
433
|
+
}
|
|
434
|
+
this.ctx.ui.requestRender();
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
const result = await this.ctx.session.executeBash(
|
|
438
|
+
command,
|
|
439
|
+
(chunk) => {
|
|
440
|
+
if (this.ctx.bashComponent) {
|
|
441
|
+
this.ctx.bashComponent.appendOutput(chunk);
|
|
442
|
+
this.ctx.ui.requestRender();
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
{ excludeFromContext },
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
if (this.ctx.bashComponent) {
|
|
449
|
+
this.ctx.bashComponent.setComplete(
|
|
450
|
+
result.exitCode,
|
|
451
|
+
result.cancelled,
|
|
452
|
+
result.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,
|
|
453
|
+
result.fullOutputPath,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (this.ctx.bashComponent) {
|
|
458
|
+
this.ctx.bashComponent.setComplete(undefined, false);
|
|
459
|
+
}
|
|
460
|
+
this.ctx.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.ctx.bashComponent = undefined;
|
|
464
|
+
this.ctx.ui.requestRender();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async handleCompactCommand(customInstructions?: string): Promise<void> {
|
|
468
|
+
const entries = this.ctx.sessionManager.getEntries();
|
|
469
|
+
const messageCount = entries.filter((e) => e.type === "message").length;
|
|
470
|
+
|
|
471
|
+
if (messageCount < 2) {
|
|
472
|
+
this.ctx.showWarning("Nothing to compact (no messages yet)");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
await this.executeCompaction(customInstructions, false);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async executeCompaction(customInstructions?: string, isAuto = false): Promise<void> {
|
|
480
|
+
if (this.ctx.loadingAnimation) {
|
|
481
|
+
this.ctx.loadingAnimation.stop();
|
|
482
|
+
this.ctx.loadingAnimation = undefined;
|
|
483
|
+
}
|
|
484
|
+
this.ctx.statusContainer.clear();
|
|
485
|
+
|
|
486
|
+
const originalOnEscape = this.ctx.editor.onEscape;
|
|
487
|
+
this.ctx.editor.onEscape = () => {
|
|
488
|
+
this.ctx.session.abortCompaction();
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
492
|
+
const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
|
|
493
|
+
const compactingLoader = new Loader(
|
|
494
|
+
this.ctx.ui,
|
|
495
|
+
(spinner) => theme.fg("accent", spinner),
|
|
496
|
+
(text) => theme.fg("muted", text),
|
|
497
|
+
label,
|
|
498
|
+
getSymbolTheme().spinnerFrames,
|
|
499
|
+
);
|
|
500
|
+
this.ctx.statusContainer.addChild(compactingLoader);
|
|
501
|
+
this.ctx.ui.requestRender();
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const result = await this.ctx.session.compact(customInstructions);
|
|
505
|
+
|
|
506
|
+
this.ctx.rebuildChatFromMessages();
|
|
507
|
+
|
|
508
|
+
const msg = createCompactionSummaryMessage(result.summary, result.tokensBefore, new Date().toISOString());
|
|
509
|
+
this.ctx.addMessageToChat(msg);
|
|
510
|
+
|
|
511
|
+
this.ctx.statusLine.invalidate();
|
|
512
|
+
this.ctx.updateEditorTopBorder();
|
|
513
|
+
} catch (error) {
|
|
514
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
515
|
+
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
|
516
|
+
this.ctx.showError("Compaction cancelled");
|
|
517
|
+
} else {
|
|
518
|
+
this.ctx.showError(`Compaction failed: ${message}`);
|
|
519
|
+
}
|
|
520
|
+
} finally {
|
|
521
|
+
compactingLoader.stop();
|
|
522
|
+
this.ctx.statusContainer.clear();
|
|
523
|
+
this.ctx.editor.onEscape = originalOnEscape;
|
|
524
|
+
}
|
|
525
|
+
await this.ctx.flushCompactionQueue({ willRetry: false });
|
|
526
|
+
}
|
|
527
|
+
}
|