@oh-my-pi/pi-coding-agent 5.4.2 → 5.6.7
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 +103 -0
- package/docs/python-repl.md +77 -0
- package/examples/hooks/snake.ts +7 -7
- package/package.json +5 -5
- package/src/bun-imports.d.ts +6 -0
- package/src/cli/args.ts +7 -0
- package/src/cli/setup-cli.ts +231 -0
- package/src/cli.ts +2 -0
- package/src/core/agent-session.ts +118 -15
- package/src/core/bash-executor.ts +3 -84
- package/src/core/compaction/compaction.ts +10 -5
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/loader.ts +13 -1
- package/src/core/extensions/runner.ts +50 -2
- package/src/core/extensions/types.ts +67 -2
- package/src/core/keybindings.ts +51 -1
- package/src/core/prompt-templates.ts +15 -0
- package/src/core/python-executor-display.test.ts +42 -0
- package/src/core/python-executor-lifecycle.test.ts +99 -0
- package/src/core/python-executor-mapping.test.ts +41 -0
- package/src/core/python-executor-per-call.test.ts +49 -0
- package/src/core/python-executor-session.test.ts +103 -0
- package/src/core/python-executor-streaming.test.ts +77 -0
- package/src/core/python-executor-timeout.test.ts +35 -0
- package/src/core/python-executor.lifecycle.test.ts +139 -0
- package/src/core/python-executor.result.test.ts +49 -0
- package/src/core/python-executor.test.ts +180 -0
- package/src/core/python-executor.ts +313 -0
- package/src/core/python-gateway-coordinator.ts +832 -0
- package/src/core/python-kernel-display.test.ts +54 -0
- package/src/core/python-kernel-env.test.ts +138 -0
- package/src/core/python-kernel-session.test.ts +87 -0
- package/src/core/python-kernel-ws.test.ts +104 -0
- package/src/core/python-kernel.lifecycle.test.ts +249 -0
- package/src/core/python-kernel.test.ts +549 -0
- package/src/core/python-kernel.ts +1178 -0
- package/src/core/python-prelude.py +889 -0
- package/src/core/python-prelude.test.ts +140 -0
- package/src/core/python-prelude.ts +3 -0
- package/src/core/sdk.ts +24 -6
- package/src/core/session-manager.ts +174 -82
- package/src/core/settings-manager-python.test.ts +23 -0
- package/src/core/settings-manager.ts +202 -0
- package/src/core/streaming-output.test.ts +26 -0
- package/src/core/streaming-output.ts +100 -0
- package/src/core/system-prompt.python.test.ts +17 -0
- package/src/core/system-prompt.ts +3 -1
- package/src/core/timings.ts +1 -1
- package/src/core/tools/bash.ts +13 -2
- package/src/core/tools/edit-diff.ts +9 -1
- package/src/core/tools/index.test.ts +50 -23
- package/src/core/tools/index.ts +83 -1
- package/src/core/tools/python-execution.test.ts +68 -0
- package/src/core/tools/python-fallback.test.ts +72 -0
- package/src/core/tools/python-renderer.test.ts +36 -0
- package/src/core/tools/python-tool-mode.test.ts +43 -0
- package/src/core/tools/python.test.ts +121 -0
- package/src/core/tools/python.ts +760 -0
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/schema-validation.test.ts +1 -0
- package/src/core/tools/task/executor.ts +146 -3
- package/src/core/tools/task/worker-protocol.ts +32 -2
- package/src/core/tools/task/worker.ts +182 -15
- package/src/index.ts +6 -0
- package/src/main.ts +136 -40
- package/src/modes/interactive/components/custom-editor.ts +16 -31
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
- package/src/modes/interactive/components/history-search.ts +5 -8
- package/src/modes/interactive/components/hook-editor.ts +3 -4
- package/src/modes/interactive/components/hook-input.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +5 -15
- package/src/modes/interactive/components/index.ts +1 -0
- package/src/modes/interactive/components/keybinding-hints.ts +66 -0
- package/src/modes/interactive/components/model-selector.ts +53 -66
- package/src/modes/interactive/components/oauth-selector.ts +5 -5
- package/src/modes/interactive/components/session-selector.ts +29 -23
- package/src/modes/interactive/components/settings-defs.ts +404 -196
- package/src/modes/interactive/components/settings-selector.ts +14 -10
- package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
- package/src/modes/interactive/components/tool-execution.ts +8 -0
- package/src/modes/interactive/components/tree-selector.ts +29 -23
- package/src/modes/interactive/components/user-message-selector.ts +6 -17
- package/src/modes/interactive/controllers/command-controller.ts +86 -37
- package/src/modes/interactive/controllers/event-controller.ts +8 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
- package/src/modes/interactive/controllers/input-controller.ts +42 -6
- package/src/modes/interactive/interactive-mode.ts +56 -30
- package/src/modes/interactive/theme/theme-schema.json +2 -2
- package/src/modes/interactive/types.ts +6 -1
- package/src/modes/interactive/utils/ui-helpers.ts +2 -1
- package/src/modes/print-mode.ts +23 -0
- package/src/modes/rpc/rpc-mode.ts +21 -0
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/system-prompt.md +32 -1
- package/src/prompts/tools/python.md +91 -0
- package/src/prompts/tools/task.md +5 -1
package/src/main.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { homedir, tmpdir } from "node:os";
|
|
9
9
|
import { join, resolve } from "node:path";
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
10
11
|
import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
11
12
|
import chalk from "chalk";
|
|
12
13
|
import { type Args, parseArgs, printHelp } from "./cli/args";
|
|
@@ -15,19 +16,22 @@ import { processFileArguments } from "./cli/file-processor";
|
|
|
15
16
|
import { listModels } from "./cli/list-models";
|
|
16
17
|
import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli";
|
|
17
18
|
import { selectSession } from "./cli/session-picker";
|
|
19
|
+
import { parseSetupArgs, printSetupHelp, runSetupCommand } from "./cli/setup-cli";
|
|
18
20
|
import { parseUpdateArgs, printUpdateHelp, runUpdateCommand } from "./cli/update-cli";
|
|
19
21
|
import { findConfigFile, getModelsPath, VERSION } from "./config";
|
|
20
22
|
import type { AgentSession } from "./core/agent-session";
|
|
21
23
|
import { exportFromFile } from "./core/export-html/index";
|
|
22
24
|
import type { ExtensionUIContext } from "./core/index";
|
|
23
25
|
import type { ModelRegistry } from "./core/model-registry";
|
|
24
|
-
import { parseModelPattern, resolveModelScope, type ScopedModel } from "./core/model-resolver";
|
|
26
|
+
import { parseModelPattern, parseModelString, resolveModelScope, type ScopedModel } from "./core/model-resolver";
|
|
25
27
|
import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage, discoverModels } from "./core/sdk";
|
|
26
|
-
import { SessionManager } from "./core/session-manager";
|
|
28
|
+
import { type SessionInfo, SessionManager } from "./core/session-manager";
|
|
27
29
|
import { SettingsManager } from "./core/settings-manager";
|
|
28
30
|
import { resolvePromptInput } from "./core/system-prompt";
|
|
29
31
|
import { printTimings, time } from "./core/timings";
|
|
32
|
+
import { initializeWithSettings } from "./discovery";
|
|
30
33
|
import { runMigrations, showDeprecationWarnings } from "./migrations";
|
|
34
|
+
import { runAsyncCleanup } from "./modes/cleanup";
|
|
31
35
|
import { InteractiveMode, installTerminalCrashHandlers, runPrintMode, runRpcMode } from "./modes/index";
|
|
32
36
|
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme";
|
|
33
37
|
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
|
|
@@ -50,6 +54,25 @@ async function checkForNewVersion(currentVersion: string): Promise<string | unde
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
const writeStdout = (message: string): void => {
|
|
58
|
+
process.stdout.write(`${message}\n`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const writeStderr = (message: string): void => {
|
|
62
|
+
process.stderr.write(`${message}\n`);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
async function readPipedInput(): Promise<string | undefined> {
|
|
66
|
+
if (process.stdin.isTTY !== false) return undefined;
|
|
67
|
+
try {
|
|
68
|
+
const text = await Bun.stdin.text();
|
|
69
|
+
if (text.trim().length === 0) return undefined;
|
|
70
|
+
return text;
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
53
76
|
async function runInteractiveMode(
|
|
54
77
|
session: AgentSession,
|
|
55
78
|
version: string,
|
|
@@ -61,10 +84,11 @@ async function runInteractiveMode(
|
|
|
61
84
|
initialMessages: string[],
|
|
62
85
|
setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
|
|
63
86
|
lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined,
|
|
87
|
+
mcpManager: import("./core/mcp/index").MCPManager | undefined,
|
|
64
88
|
initialMessage?: string,
|
|
65
89
|
initialImages?: ImageContent[],
|
|
66
90
|
): Promise<void> {
|
|
67
|
-
const mode = new InteractiveMode(session, version, changelogMarkdown, setExtensionUIContext, lspServers);
|
|
91
|
+
const mode = new InteractiveMode(session, version, changelogMarkdown, setExtensionUIContext, lspServers, mcpManager);
|
|
68
92
|
|
|
69
93
|
await mode.init();
|
|
70
94
|
|
|
@@ -145,25 +169,32 @@ async function prepareInitialMessage(
|
|
|
145
169
|
}
|
|
146
170
|
|
|
147
171
|
/**
|
|
148
|
-
* Resolve a session argument to a
|
|
149
|
-
* If it looks like a path, use as-is. Otherwise try to match as session ID prefix.
|
|
172
|
+
* Resolve a session argument to a local or global session match.
|
|
150
173
|
*/
|
|
151
|
-
function
|
|
152
|
-
// If it looks like a file path, use as-is
|
|
153
|
-
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
|
154
|
-
return sessionArg;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Try to match as session ID (full or partial UUID)
|
|
174
|
+
function resolveSessionMatch(sessionArg: string, cwd: string, sessionDir?: string): SessionInfo | undefined {
|
|
158
175
|
const sessions = SessionManager.list(cwd, sessionDir);
|
|
159
|
-
|
|
176
|
+
let matches = sessions.filter((session) => session.id.startsWith(sessionArg));
|
|
160
177
|
|
|
161
|
-
if (matches.length
|
|
162
|
-
|
|
178
|
+
if (matches.length === 0 && !sessionDir) {
|
|
179
|
+
const globalSessions = SessionManager.listAll();
|
|
180
|
+
matches = globalSessions.filter((session) => session.id.startsWith(sessionArg));
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
|
|
166
|
-
|
|
183
|
+
return matches[0];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function promptForkSession(session: SessionInfo): Promise<boolean> {
|
|
187
|
+
if (!process.stdin.isTTY) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const message = `Session found in different project: ${session.cwd}. Fork into current directory? [y/N] `;
|
|
191
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
192
|
+
try {
|
|
193
|
+
const answer = (await rl.question(message)).trim().toLowerCase();
|
|
194
|
+
return answer === "y" || answer === "yes";
|
|
195
|
+
} finally {
|
|
196
|
+
rl.close();
|
|
197
|
+
}
|
|
167
198
|
}
|
|
168
199
|
|
|
169
200
|
function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | undefined {
|
|
@@ -196,8 +227,24 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
|
|
|
196
227
|
return SessionManager.inMemory();
|
|
197
228
|
}
|
|
198
229
|
if (parsed.session) {
|
|
199
|
-
const
|
|
200
|
-
|
|
230
|
+
const sessionArg = parsed.session;
|
|
231
|
+
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
|
232
|
+
return await SessionManager.open(sessionArg, parsed.sessionDir);
|
|
233
|
+
}
|
|
234
|
+
const match = resolveSessionMatch(sessionArg, cwd, parsed.sessionDir);
|
|
235
|
+
if (!match) {
|
|
236
|
+
throw new Error(`Session "${sessionArg}" not found.`);
|
|
237
|
+
}
|
|
238
|
+
const normalizedCwd = resolve(cwd);
|
|
239
|
+
const normalizedMatchCwd = resolve(match.cwd || cwd);
|
|
240
|
+
if (normalizedCwd !== normalizedMatchCwd) {
|
|
241
|
+
const shouldFork = await promptForkSession(match);
|
|
242
|
+
if (!shouldFork) {
|
|
243
|
+
throw new Error(`Session "${sessionArg}" is in another project (${match.cwd}).`);
|
|
244
|
+
}
|
|
245
|
+
return await SessionManager.forkFrom(match.path, cwd, parsed.sessionDir);
|
|
246
|
+
}
|
|
247
|
+
return await SessionManager.open(match.path, parsed.sessionDir);
|
|
201
248
|
}
|
|
202
249
|
if (parsed.continue) {
|
|
203
250
|
return await SessionManager.continueRecent(cwd, parsed.sessionDir);
|
|
@@ -279,6 +326,19 @@ function discoverSystemPromptFile(): string | undefined {
|
|
|
279
326
|
return undefined;
|
|
280
327
|
}
|
|
281
328
|
|
|
329
|
+
/** Discover APPEND_SYSTEM.md file if no CLI append system prompt was provided */
|
|
330
|
+
function discoverAppendSystemPromptFile(): string | undefined {
|
|
331
|
+
const projectPath = findConfigFile("APPEND_SYSTEM.md", { user: false });
|
|
332
|
+
if (projectPath) {
|
|
333
|
+
return projectPath;
|
|
334
|
+
}
|
|
335
|
+
const globalPath = findConfigFile("APPEND_SYSTEM.md", { user: true });
|
|
336
|
+
if (globalPath) {
|
|
337
|
+
return globalPath;
|
|
338
|
+
}
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
282
342
|
async function buildSessionOptions(
|
|
283
343
|
parsed: Args,
|
|
284
344
|
scopedModels: ScopedModel[],
|
|
@@ -293,7 +353,8 @@ async function buildSessionOptions(
|
|
|
293
353
|
// Auto-discover SYSTEM.md if no CLI system prompt provided
|
|
294
354
|
const systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();
|
|
295
355
|
const resolvedSystemPrompt = resolvePromptInput(systemPromptSource, "system prompt");
|
|
296
|
-
const
|
|
356
|
+
const appendPromptSource = parsed.appendSystemPrompt ?? discoverAppendSystemPromptFile();
|
|
357
|
+
const resolvedAppendPrompt = resolvePromptInput(appendPromptSource, "append system prompt");
|
|
297
358
|
|
|
298
359
|
if (sessionManager) {
|
|
299
360
|
options.sessionManager = sessionManager;
|
|
@@ -304,10 +365,10 @@ async function buildSessionOptions(
|
|
|
304
365
|
const available = modelRegistry.getAvailable();
|
|
305
366
|
const { model, warning } = parseModelPattern(parsed.model, available);
|
|
306
367
|
if (warning) {
|
|
307
|
-
|
|
368
|
+
writeStderr(chalk.yellow(`Warning: ${warning}`));
|
|
308
369
|
}
|
|
309
370
|
if (!model) {
|
|
310
|
-
|
|
371
|
+
writeStderr(chalk.red(`Model "${parsed.model}" not found`));
|
|
311
372
|
process.exit(1);
|
|
312
373
|
}
|
|
313
374
|
options.model = model;
|
|
@@ -315,7 +376,22 @@ async function buildSessionOptions(
|
|
|
315
376
|
modelRoles: { default: `${model.provider}/${model.id}` },
|
|
316
377
|
});
|
|
317
378
|
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
|
318
|
-
|
|
379
|
+
const remembered = settingsManager.getModelRole("default");
|
|
380
|
+
if (remembered) {
|
|
381
|
+
const parsedModel = parseModelString(remembered);
|
|
382
|
+
const rememberedModel = parsedModel
|
|
383
|
+
? scopedModels.find(
|
|
384
|
+
(scopedModel) =>
|
|
385
|
+
scopedModel.model.provider === parsedModel.provider && scopedModel.model.id === parsedModel.id,
|
|
386
|
+
)
|
|
387
|
+
: scopedModels.find((scopedModel) => scopedModel.model.id.toLowerCase() === remembered.toLowerCase());
|
|
388
|
+
if (rememberedModel) {
|
|
389
|
+
options.model = rememberedModel.model;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!options.model) {
|
|
393
|
+
options.model = scopedModels[0].model;
|
|
394
|
+
}
|
|
319
395
|
}
|
|
320
396
|
|
|
321
397
|
// Thinking level
|
|
@@ -378,13 +454,14 @@ async function buildSessionOptions(
|
|
|
378
454
|
}
|
|
379
455
|
|
|
380
456
|
// Additional extension paths from CLI
|
|
381
|
-
const cliExtensionPaths = [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
|
|
457
|
+
const cliExtensionPaths = parsed.noExtensions ? [] : [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
|
|
382
458
|
if (cliExtensionPaths.length > 0) {
|
|
383
459
|
options.additionalExtensionPaths = cliExtensionPaths;
|
|
384
460
|
}
|
|
385
461
|
|
|
386
462
|
if (parsed.noExtensions) {
|
|
387
463
|
options.disableExtensionDiscovery = true;
|
|
464
|
+
options.additionalExtensionPaths = [];
|
|
388
465
|
}
|
|
389
466
|
|
|
390
467
|
return options;
|
|
@@ -430,6 +507,17 @@ export async function main(args: string[]) {
|
|
|
430
507
|
return;
|
|
431
508
|
}
|
|
432
509
|
|
|
510
|
+
// Handle setup subcommand
|
|
511
|
+
const setupCmd = parseSetupArgs(args);
|
|
512
|
+
if (setupCmd) {
|
|
513
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
514
|
+
printSetupHelp();
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
await runSetupCommand(setupCmd);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
433
521
|
const parsed = parseArgs(args);
|
|
434
522
|
time("parseArgs");
|
|
435
523
|
await maybeAutoChdir(parsed);
|
|
@@ -443,7 +531,7 @@ export async function main(args: string[]) {
|
|
|
443
531
|
time("discoverModels");
|
|
444
532
|
|
|
445
533
|
if (parsed.version) {
|
|
446
|
-
|
|
534
|
+
writeStdout(VERSION);
|
|
447
535
|
return;
|
|
448
536
|
}
|
|
449
537
|
|
|
@@ -462,17 +550,17 @@ export async function main(args: string[]) {
|
|
|
462
550
|
try {
|
|
463
551
|
const outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;
|
|
464
552
|
const result = await exportFromFile(parsed.export, outputPath);
|
|
465
|
-
|
|
553
|
+
writeStdout(`Exported to: ${result}`);
|
|
466
554
|
return;
|
|
467
555
|
} catch (error: unknown) {
|
|
468
556
|
const message = error instanceof Error ? error.message : "Failed to export session";
|
|
469
|
-
|
|
557
|
+
writeStderr(chalk.red(`Error: ${message}`));
|
|
470
558
|
process.exit(1);
|
|
471
559
|
}
|
|
472
560
|
}
|
|
473
561
|
|
|
474
562
|
if (parsed.mode === "rpc" && parsed.fileArgs.length > 0) {
|
|
475
|
-
|
|
563
|
+
writeStderr(chalk.red("Error: @file arguments are not supported in RPC mode"));
|
|
476
564
|
process.exit(1);
|
|
477
565
|
}
|
|
478
566
|
|
|
@@ -480,13 +568,17 @@ export async function main(args: string[]) {
|
|
|
480
568
|
const settingsManager = await SettingsManager.create(cwd);
|
|
481
569
|
settingsManager.applyEnvironmentVariables();
|
|
482
570
|
time("SettingsManager.create");
|
|
483
|
-
const
|
|
571
|
+
const pipedInput = await readPipedInput();
|
|
572
|
+
let { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
|
|
573
|
+
if (pipedInput) {
|
|
574
|
+
initialMessage = initialMessage ? `${initialMessage}\n${pipedInput}` : pipedInput;
|
|
575
|
+
}
|
|
484
576
|
time("prepareInitialMessage");
|
|
485
|
-
const
|
|
577
|
+
const autoPrint = pipedInput !== undefined && !parsed.print && parsed.mode === undefined;
|
|
578
|
+
const isInteractive = !parsed.print && !autoPrint && parsed.mode === undefined;
|
|
486
579
|
const mode = parsed.mode || "text";
|
|
487
580
|
|
|
488
581
|
// Initialize discovery system with settings for provider persistence
|
|
489
|
-
const { initializeWithSettings } = await import("./discovery");
|
|
490
582
|
initializeWithSettings(settingsManager);
|
|
491
583
|
time("initializeWithSettings");
|
|
492
584
|
|
|
@@ -524,13 +616,13 @@ export async function main(args: string[]) {
|
|
|
524
616
|
const sessions = SessionManager.list(cwd, parsed.sessionDir);
|
|
525
617
|
time("SessionManager.list");
|
|
526
618
|
if (sessions.length === 0) {
|
|
527
|
-
|
|
619
|
+
writeStdout(chalk.dim("No sessions found"));
|
|
528
620
|
return;
|
|
529
621
|
}
|
|
530
622
|
const selectedPath = await selectSession(sessions);
|
|
531
623
|
time("selectSession");
|
|
532
624
|
if (!selectedPath) {
|
|
533
|
-
|
|
625
|
+
writeStdout(chalk.dim("No session selected"));
|
|
534
626
|
return;
|
|
535
627
|
}
|
|
536
628
|
sessionManager = await SessionManager.open(selectedPath);
|
|
@@ -551,14 +643,15 @@ export async function main(args: string[]) {
|
|
|
551
643
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
552
644
|
if (parsed.apiKey) {
|
|
553
645
|
if (!sessionOptions.model) {
|
|
554
|
-
|
|
646
|
+
writeStderr(chalk.red("--api-key requires a model to be specified via --provider/--model or -m/--models"));
|
|
555
647
|
process.exit(1);
|
|
556
648
|
}
|
|
557
649
|
authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);
|
|
558
650
|
}
|
|
559
651
|
|
|
560
652
|
time("buildSessionOptions");
|
|
561
|
-
const { session, setToolUIContext, modelFallbackMessage, lspServers } =
|
|
653
|
+
const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } =
|
|
654
|
+
await createAgentSession(sessionOptions);
|
|
562
655
|
time("createAgentSession");
|
|
563
656
|
|
|
564
657
|
// Re-parse CLI args with extension flags and apply values
|
|
@@ -578,10 +671,10 @@ export async function main(args: string[]) {
|
|
|
578
671
|
time("applyExtensionFlags");
|
|
579
672
|
|
|
580
673
|
if (!isInteractive && !session.model) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
674
|
+
writeStderr(chalk.red("No models available."));
|
|
675
|
+
writeStderr(chalk.yellow("\nSet an API key environment variable:"));
|
|
676
|
+
writeStderr(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
|
|
677
|
+
writeStderr(chalk.yellow(`\nOr create ${getModelsPath()}`));
|
|
585
678
|
process.exit(1);
|
|
586
679
|
}
|
|
587
680
|
|
|
@@ -612,7 +705,7 @@ export async function main(args: string[]) {
|
|
|
612
705
|
return `${scopedModel.model.id}${thinkingStr}`;
|
|
613
706
|
})
|
|
614
707
|
.join(", ");
|
|
615
|
-
|
|
708
|
+
writeStdout(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
|
|
616
709
|
}
|
|
617
710
|
|
|
618
711
|
installTerminalCrashHandlers();
|
|
@@ -628,6 +721,7 @@ export async function main(args: string[]) {
|
|
|
628
721
|
parsed.messages,
|
|
629
722
|
setToolUIContext,
|
|
630
723
|
lspServers,
|
|
724
|
+
mcpManager,
|
|
631
725
|
initialMessage,
|
|
632
726
|
initialImages,
|
|
633
727
|
);
|
|
@@ -638,7 +732,9 @@ export async function main(args: string[]) {
|
|
|
638
732
|
initialMessage,
|
|
639
733
|
initialImages,
|
|
640
734
|
});
|
|
735
|
+
await session.dispose();
|
|
641
736
|
stopThemeWatcher();
|
|
737
|
+
await runAsyncCleanup();
|
|
642
738
|
if (process.stdout.writableLength > 0) {
|
|
643
739
|
await new Promise<void>((resolve) => process.stdout.once("drain", resolve));
|
|
644
740
|
}
|
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Editor,
|
|
3
|
-
isCapsLock,
|
|
4
|
-
isCtrlC,
|
|
5
|
-
isCtrlD,
|
|
6
|
-
isCtrlG,
|
|
7
|
-
isCtrlL,
|
|
8
|
-
isCtrlO,
|
|
9
|
-
isCtrlP,
|
|
10
|
-
isCtrlT,
|
|
11
|
-
isCtrlV,
|
|
12
|
-
isCtrlZ,
|
|
13
|
-
isEscape,
|
|
14
|
-
isShiftCtrlP,
|
|
15
|
-
isShiftTab,
|
|
16
|
-
type KeyId,
|
|
17
|
-
matchesKey,
|
|
18
|
-
} from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { Editor, type KeyId, matchesKey, parseKittySequence } from "@oh-my-pi/pi-tui";
|
|
19
2
|
|
|
20
3
|
/**
|
|
21
4
|
* Custom editor that handles Escape and Ctrl+C keys for coding-agent
|
|
@@ -66,19 +49,21 @@ export class CustomEditor extends Editor {
|
|
|
66
49
|
}
|
|
67
50
|
|
|
68
51
|
handleInput(data: string): void {
|
|
69
|
-
|
|
52
|
+
const parsed = parseKittySequence(data);
|
|
53
|
+
if (parsed && (parsed.modifier & 64) !== 0 && this.onCapsLock) {
|
|
54
|
+
// Caps Lock is modifier bit 64
|
|
70
55
|
this.onCapsLock();
|
|
71
56
|
return;
|
|
72
57
|
}
|
|
73
58
|
|
|
74
59
|
// Intercept Ctrl+V for image paste (async - fires and handles result)
|
|
75
|
-
if (
|
|
60
|
+
if (matchesKey(data, "ctrl+v") && this.onCtrlV) {
|
|
76
61
|
void this.onCtrlV();
|
|
77
62
|
return;
|
|
78
63
|
}
|
|
79
64
|
|
|
80
65
|
// Intercept Ctrl+G for external editor
|
|
81
|
-
if (
|
|
66
|
+
if (matchesKey(data, "ctrl+g") && this.onCtrlG) {
|
|
82
67
|
this.onCtrlG();
|
|
83
68
|
return;
|
|
84
69
|
}
|
|
@@ -90,19 +75,19 @@ export class CustomEditor extends Editor {
|
|
|
90
75
|
}
|
|
91
76
|
|
|
92
77
|
// Intercept Ctrl+Z for suspend
|
|
93
|
-
if (
|
|
78
|
+
if (matchesKey(data, "ctrl+z") && this.onCtrlZ) {
|
|
94
79
|
this.onCtrlZ();
|
|
95
80
|
return;
|
|
96
81
|
}
|
|
97
82
|
|
|
98
83
|
// Intercept Ctrl+T for thinking block visibility toggle
|
|
99
|
-
if (
|
|
84
|
+
if (matchesKey(data, "ctrl+t") && this.onCtrlT) {
|
|
100
85
|
this.onCtrlT();
|
|
101
86
|
return;
|
|
102
87
|
}
|
|
103
88
|
|
|
104
89
|
// Intercept Ctrl+L for model selector
|
|
105
|
-
if (
|
|
90
|
+
if (matchesKey(data, "ctrl+l") && this.onCtrlL) {
|
|
106
91
|
this.onCtrlL();
|
|
107
92
|
return;
|
|
108
93
|
}
|
|
@@ -114,44 +99,44 @@ export class CustomEditor extends Editor {
|
|
|
114
99
|
}
|
|
115
100
|
|
|
116
101
|
// Intercept Ctrl+O for tool output expansion
|
|
117
|
-
if (
|
|
102
|
+
if (matchesKey(data, "ctrl+o") && this.onCtrlO) {
|
|
118
103
|
this.onCtrlO();
|
|
119
104
|
return;
|
|
120
105
|
}
|
|
121
106
|
|
|
122
107
|
// Intercept Shift+Ctrl+P for backward model cycling (check before Ctrl+P)
|
|
123
|
-
if (
|
|
108
|
+
if ((matchesKey(data, "shift+ctrl+p") || matchesKey(data, "ctrl+shift+p")) && this.onShiftCtrlP) {
|
|
124
109
|
this.onShiftCtrlP();
|
|
125
110
|
return;
|
|
126
111
|
}
|
|
127
112
|
|
|
128
113
|
// Intercept Ctrl+P for model cycling
|
|
129
|
-
if (
|
|
114
|
+
if (matchesKey(data, "ctrl+p") && this.onCtrlP) {
|
|
130
115
|
this.onCtrlP();
|
|
131
116
|
return;
|
|
132
117
|
}
|
|
133
118
|
|
|
134
119
|
// Intercept Shift+Tab for thinking level cycling
|
|
135
|
-
if (
|
|
120
|
+
if (matchesKey(data, "shift+tab") && this.onShiftTab) {
|
|
136
121
|
this.onShiftTab();
|
|
137
122
|
return;
|
|
138
123
|
}
|
|
139
124
|
|
|
140
125
|
// Intercept Escape key - but only if autocomplete is NOT active
|
|
141
126
|
// (let parent handle escape for autocomplete cancellation)
|
|
142
|
-
if (
|
|
127
|
+
if ((matchesKey(data, "escape") || matchesKey(data, "esc")) && this.onEscape && !this.isShowingAutocomplete()) {
|
|
143
128
|
this.onEscape();
|
|
144
129
|
return;
|
|
145
130
|
}
|
|
146
131
|
|
|
147
132
|
// Intercept Ctrl+C
|
|
148
|
-
if (
|
|
133
|
+
if (matchesKey(data, "ctrl+c") && this.onCtrlC) {
|
|
149
134
|
this.onCtrlC();
|
|
150
135
|
return;
|
|
151
136
|
}
|
|
152
137
|
|
|
153
138
|
// Intercept Ctrl+D (only when editor is empty)
|
|
154
|
-
if (
|
|
139
|
+
if (matchesKey(data, "ctrl+d")) {
|
|
155
140
|
if (this.getText().length === 0 && this.onCtrlD) {
|
|
156
141
|
this.onCtrlD();
|
|
157
142
|
}
|
|
@@ -12,18 +12,7 @@
|
|
|
12
12
|
* - Esc: Close dashboard (clears search first if active)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
type Component,
|
|
17
|
-
Container,
|
|
18
|
-
isCtrlC,
|
|
19
|
-
isEscape,
|
|
20
|
-
isShiftTab,
|
|
21
|
-
isTab,
|
|
22
|
-
Spacer,
|
|
23
|
-
Text,
|
|
24
|
-
truncateToWidth,
|
|
25
|
-
visibleWidth,
|
|
26
|
-
} from "@oh-my-pi/pi-tui";
|
|
15
|
+
import { type Component, Container, matchesKey, Spacer, Text, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
27
16
|
import type { SettingsManager } from "../../../../core/settings-manager";
|
|
28
17
|
import { theme } from "../../theme/theme";
|
|
29
18
|
import { DynamicBorder } from "../dynamic-border";
|
|
@@ -239,13 +228,13 @@ export class ExtensionDashboard extends Container {
|
|
|
239
228
|
|
|
240
229
|
handleInput(data: string): void {
|
|
241
230
|
// Ctrl+C - close immediately
|
|
242
|
-
if (
|
|
231
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
243
232
|
this.onClose?.();
|
|
244
233
|
return;
|
|
245
234
|
}
|
|
246
235
|
|
|
247
236
|
// Escape - clear search first, then close
|
|
248
|
-
if (
|
|
237
|
+
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
|
249
238
|
if (this.state.searchQuery.length > 0) {
|
|
250
239
|
this.state.searchQuery = "";
|
|
251
240
|
this.state.searchFiltered = this.state.tabFiltered;
|
|
@@ -259,11 +248,11 @@ export class ExtensionDashboard extends Container {
|
|
|
259
248
|
}
|
|
260
249
|
|
|
261
250
|
// Tab/Shift+Tab: Cycle through tabs
|
|
262
|
-
if (
|
|
251
|
+
if (matchesKey(data, "tab")) {
|
|
263
252
|
this.switchTab(1);
|
|
264
253
|
return;
|
|
265
254
|
}
|
|
266
|
-
if (
|
|
255
|
+
if (matchesKey(data, "shift+tab")) {
|
|
267
256
|
this.switchTab(-1);
|
|
268
257
|
return;
|
|
269
258
|
}
|
|
@@ -6,15 +6,7 @@
|
|
|
6
6
|
* master switch is off.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
type Component,
|
|
11
|
-
isArrowDown,
|
|
12
|
-
isArrowUp,
|
|
13
|
-
isBackspace,
|
|
14
|
-
isEnter,
|
|
15
|
-
truncateToWidth,
|
|
16
|
-
visibleWidth,
|
|
17
|
-
} from "@oh-my-pi/pi-tui";
|
|
9
|
+
import { type Component, matchesKey, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
18
10
|
import { isProviderEnabled } from "../../../../discovery";
|
|
19
11
|
import { theme } from "../../theme/theme";
|
|
20
12
|
import { applyFilter } from "./state-manager";
|
|
@@ -404,12 +396,12 @@ export class ExtensionList implements Component {
|
|
|
404
396
|
const charCode = data.length === 1 ? data.charCodeAt(0) : -1;
|
|
405
397
|
|
|
406
398
|
// Navigation
|
|
407
|
-
if (
|
|
399
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
408
400
|
this.moveSelectionUp();
|
|
409
401
|
return;
|
|
410
402
|
}
|
|
411
403
|
|
|
412
|
-
if (
|
|
404
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
413
405
|
this.moveSelectionDown();
|
|
414
406
|
return;
|
|
415
407
|
}
|
|
@@ -431,7 +423,7 @@ export class ExtensionList implements Component {
|
|
|
431
423
|
}
|
|
432
424
|
|
|
433
425
|
// Enter: Same as space - toggle selected item
|
|
434
|
-
if (
|
|
426
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
435
427
|
const item = this.listItems[this.selectedIndex];
|
|
436
428
|
if (item?.type === "master") {
|
|
437
429
|
this.callbacks.onMasterToggle?.(item.providerId);
|
|
@@ -446,7 +438,7 @@ export class ExtensionList implements Component {
|
|
|
446
438
|
}
|
|
447
439
|
|
|
448
440
|
// Backspace: Delete from search query
|
|
449
|
-
if (
|
|
441
|
+
if (matchesKey(data, "backspace")) {
|
|
450
442
|
if (this.searchQuery.length > 0) {
|
|
451
443
|
this.setSearchQuery(this.searchQuery.slice(0, -1));
|
|
452
444
|
}
|
|
@@ -2,10 +2,7 @@ import {
|
|
|
2
2
|
type Component,
|
|
3
3
|
Container,
|
|
4
4
|
Input,
|
|
5
|
-
|
|
6
|
-
isArrowUp,
|
|
7
|
-
isEnter,
|
|
8
|
-
isEscape,
|
|
5
|
+
matchesKey,
|
|
9
6
|
Spacer,
|
|
10
7
|
Text,
|
|
11
8
|
truncateToWidth,
|
|
@@ -116,21 +113,21 @@ export class HistorySearchComponent extends Container {
|
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
handleInput(keyData: string): void {
|
|
119
|
-
if (
|
|
116
|
+
if (matchesKey(keyData, "up")) {
|
|
120
117
|
if (this.results.length === 0) return;
|
|
121
118
|
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
122
119
|
this.resultsList.setSelectedIndex(this.selectedIndex);
|
|
123
120
|
return;
|
|
124
121
|
}
|
|
125
122
|
|
|
126
|
-
if (
|
|
123
|
+
if (matchesKey(keyData, "down")) {
|
|
127
124
|
if (this.results.length === 0) return;
|
|
128
125
|
this.selectedIndex = Math.min(this.results.length - 1, this.selectedIndex + 1);
|
|
129
126
|
this.resultsList.setSelectedIndex(this.selectedIndex);
|
|
130
127
|
return;
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
if (
|
|
130
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
134
131
|
const selected = this.results[this.selectedIndex];
|
|
135
132
|
if (selected) {
|
|
136
133
|
this.onSelect(selected.prompt);
|
|
@@ -138,7 +135,7 @@ export class HistorySearchComponent extends Container {
|
|
|
138
135
|
return;
|
|
139
136
|
}
|
|
140
137
|
|
|
141
|
-
if (
|
|
138
|
+
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
|
142
139
|
this.onCancel();
|
|
143
140
|
return;
|
|
144
141
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { Container, Editor,
|
|
9
|
+
import { Container, Editor, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import { nanoid } from "nanoid";
|
|
11
11
|
import { getEditorTheme, theme } from "../theme/theme";
|
|
12
12
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -40,7 +40,6 @@ export class HookEditorComponent extends Container {
|
|
|
40
40
|
|
|
41
41
|
// Create editor
|
|
42
42
|
this.editor = new Editor(getEditorTheme());
|
|
43
|
-
this.editor.setUseTerminalCursor(true);
|
|
44
43
|
if (prefill) {
|
|
45
44
|
this.editor.setText(prefill);
|
|
46
45
|
}
|
|
@@ -69,13 +68,13 @@ export class HookEditorComponent extends Container {
|
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
// Escape to cancel
|
|
72
|
-
if (
|
|
71
|
+
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
|
73
72
|
this.onCancelCallback();
|
|
74
73
|
return;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
// Ctrl+G for external editor
|
|
78
|
-
if (
|
|
77
|
+
if (matchesKey(keyData, "ctrl+g")) {
|
|
79
78
|
this.openExternalEditor();
|
|
80
79
|
return;
|
|
81
80
|
}
|