@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/docs/python-repl.md +77 -0
  3. package/examples/hooks/snake.ts +7 -7
  4. package/package.json +5 -5
  5. package/src/bun-imports.d.ts +6 -0
  6. package/src/cli/args.ts +7 -0
  7. package/src/cli/setup-cli.ts +231 -0
  8. package/src/cli.ts +2 -0
  9. package/src/core/agent-session.ts +118 -15
  10. package/src/core/bash-executor.ts +3 -84
  11. package/src/core/compaction/compaction.ts +10 -5
  12. package/src/core/extensions/index.ts +2 -0
  13. package/src/core/extensions/loader.ts +13 -1
  14. package/src/core/extensions/runner.ts +50 -2
  15. package/src/core/extensions/types.ts +67 -2
  16. package/src/core/keybindings.ts +51 -1
  17. package/src/core/prompt-templates.ts +15 -0
  18. package/src/core/python-executor-display.test.ts +42 -0
  19. package/src/core/python-executor-lifecycle.test.ts +99 -0
  20. package/src/core/python-executor-mapping.test.ts +41 -0
  21. package/src/core/python-executor-per-call.test.ts +49 -0
  22. package/src/core/python-executor-session.test.ts +103 -0
  23. package/src/core/python-executor-streaming.test.ts +77 -0
  24. package/src/core/python-executor-timeout.test.ts +35 -0
  25. package/src/core/python-executor.lifecycle.test.ts +139 -0
  26. package/src/core/python-executor.result.test.ts +49 -0
  27. package/src/core/python-executor.test.ts +180 -0
  28. package/src/core/python-executor.ts +313 -0
  29. package/src/core/python-gateway-coordinator.ts +832 -0
  30. package/src/core/python-kernel-display.test.ts +54 -0
  31. package/src/core/python-kernel-env.test.ts +138 -0
  32. package/src/core/python-kernel-session.test.ts +87 -0
  33. package/src/core/python-kernel-ws.test.ts +104 -0
  34. package/src/core/python-kernel.lifecycle.test.ts +249 -0
  35. package/src/core/python-kernel.test.ts +549 -0
  36. package/src/core/python-kernel.ts +1178 -0
  37. package/src/core/python-prelude.py +889 -0
  38. package/src/core/python-prelude.test.ts +140 -0
  39. package/src/core/python-prelude.ts +3 -0
  40. package/src/core/sdk.ts +24 -6
  41. package/src/core/session-manager.ts +174 -82
  42. package/src/core/settings-manager-python.test.ts +23 -0
  43. package/src/core/settings-manager.ts +202 -0
  44. package/src/core/streaming-output.test.ts +26 -0
  45. package/src/core/streaming-output.ts +100 -0
  46. package/src/core/system-prompt.python.test.ts +17 -0
  47. package/src/core/system-prompt.ts +3 -1
  48. package/src/core/timings.ts +1 -1
  49. package/src/core/tools/bash.ts +13 -2
  50. package/src/core/tools/edit-diff.ts +9 -1
  51. package/src/core/tools/index.test.ts +50 -23
  52. package/src/core/tools/index.ts +83 -1
  53. package/src/core/tools/python-execution.test.ts +68 -0
  54. package/src/core/tools/python-fallback.test.ts +72 -0
  55. package/src/core/tools/python-renderer.test.ts +36 -0
  56. package/src/core/tools/python-tool-mode.test.ts +43 -0
  57. package/src/core/tools/python.test.ts +121 -0
  58. package/src/core/tools/python.ts +760 -0
  59. package/src/core/tools/renderers.ts +2 -0
  60. package/src/core/tools/schema-validation.test.ts +1 -0
  61. package/src/core/tools/task/executor.ts +146 -3
  62. package/src/core/tools/task/worker-protocol.ts +32 -2
  63. package/src/core/tools/task/worker.ts +182 -15
  64. package/src/index.ts +6 -0
  65. package/src/main.ts +136 -40
  66. package/src/modes/interactive/components/custom-editor.ts +16 -31
  67. package/src/modes/interactive/components/extensions/extension-dashboard.ts +5 -16
  68. package/src/modes/interactive/components/extensions/extension-list.ts +5 -13
  69. package/src/modes/interactive/components/history-search.ts +5 -8
  70. package/src/modes/interactive/components/hook-editor.ts +3 -4
  71. package/src/modes/interactive/components/hook-input.ts +3 -3
  72. package/src/modes/interactive/components/hook-selector.ts +5 -15
  73. package/src/modes/interactive/components/index.ts +1 -0
  74. package/src/modes/interactive/components/keybinding-hints.ts +66 -0
  75. package/src/modes/interactive/components/model-selector.ts +53 -66
  76. package/src/modes/interactive/components/oauth-selector.ts +5 -5
  77. package/src/modes/interactive/components/session-selector.ts +29 -23
  78. package/src/modes/interactive/components/settings-defs.ts +404 -196
  79. package/src/modes/interactive/components/settings-selector.ts +14 -10
  80. package/src/modes/interactive/components/status-line-segment-editor.ts +7 -7
  81. package/src/modes/interactive/components/tool-execution.ts +8 -0
  82. package/src/modes/interactive/components/tree-selector.ts +29 -23
  83. package/src/modes/interactive/components/user-message-selector.ts +6 -17
  84. package/src/modes/interactive/controllers/command-controller.ts +86 -37
  85. package/src/modes/interactive/controllers/event-controller.ts +8 -0
  86. package/src/modes/interactive/controllers/extension-ui-controller.ts +51 -0
  87. package/src/modes/interactive/controllers/input-controller.ts +42 -6
  88. package/src/modes/interactive/interactive-mode.ts +56 -30
  89. package/src/modes/interactive/theme/theme-schema.json +2 -2
  90. package/src/modes/interactive/types.ts +6 -1
  91. package/src/modes/interactive/utils/ui-helpers.ts +2 -1
  92. package/src/modes/print-mode.ts +23 -0
  93. package/src/modes/rpc/rpc-mode.ts +21 -0
  94. package/src/prompts/agents/reviewer.md +1 -1
  95. package/src/prompts/system/system-prompt.md +32 -1
  96. package/src/prompts/tools/python.md +91 -0
  97. 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 file path.
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 resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): string {
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
- const matches = sessions.filter((session) => session.id.startsWith(sessionArg));
176
+ let matches = sessions.filter((session) => session.id.startsWith(sessionArg));
160
177
 
161
- if (matches.length >= 1) {
162
- return matches[0].path; // Already sorted by modified time (most recent first)
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
- // No match - return original (will create new session)
166
- return sessionArg;
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 resolvedPath = resolveSessionPath(parsed.session, cwd, parsed.sessionDir);
200
- return await SessionManager.open(resolvedPath, parsed.sessionDir);
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 resolvedAppendPrompt = resolvePromptInput(parsed.appendSystemPrompt, "append system prompt");
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
- console.warn(chalk.yellow(`Warning: ${warning}`));
368
+ writeStderr(chalk.yellow(`Warning: ${warning}`));
308
369
  }
309
370
  if (!model) {
310
- console.error(chalk.red(`Model "${parsed.model}" not found`));
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
- options.model = scopedModels[0].model;
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
- console.log(VERSION);
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
- console.log(`Exported to: ${result}`);
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
- console.error(chalk.red(`Error: ${message}`));
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
- console.error(chalk.red("Error: @file arguments are not supported in RPC mode"));
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 { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
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 isInteractive = !parsed.print && parsed.mode === undefined;
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
- console.log(chalk.dim("No sessions found"));
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
- console.log(chalk.dim("No session selected"));
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
- console.error(chalk.red("--api-key requires a model to be specified via --provider/--model or -m/--models"));
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 } = await createAgentSession(sessionOptions);
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
- console.error(chalk.red("No models available."));
582
- console.error(chalk.yellow("\nSet an API key environment variable:"));
583
- console.error(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
584
- console.error(chalk.yellow(`\nOr create ${getModelsPath()}`));
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
- console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
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
- if (isCapsLock(data) && this.onCapsLock) {
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 (isCtrlV(data) && this.onCtrlV) {
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 (isCtrlG(data) && this.onCtrlG) {
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 (isCtrlZ(data) && this.onCtrlZ) {
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 (isCtrlT(data) && this.onCtrlT) {
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 (isCtrlL(data) && this.onCtrlL) {
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 (isCtrlO(data) && this.onCtrlO) {
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 (isShiftCtrlP(data) && this.onShiftCtrlP) {
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 (isCtrlP(data) && this.onCtrlP) {
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 (isShiftTab(data) && this.onShiftTab) {
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 (isEscape(data) && this.onEscape && !this.isShowingAutocomplete()) {
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 (isCtrlC(data) && this.onCtrlC) {
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 (isCtrlD(data)) {
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 (isCtrlC(data)) {
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 (isEscape(data)) {
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 (isTab(data)) {
251
+ if (matchesKey(data, "tab")) {
263
252
  this.switchTab(1);
264
253
  return;
265
254
  }
266
- if (isShiftTab(data)) {
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 (isArrowUp(data) || data === "k") {
399
+ if (matchesKey(data, "up") || data === "k") {
408
400
  this.moveSelectionUp();
409
401
  return;
410
402
  }
411
403
 
412
- if (isArrowDown(data) || data === "j") {
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 (isEnter(data)) {
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 (isBackspace(data)) {
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
- isArrowDown,
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 (isArrowUp(keyData)) {
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 (isArrowDown(keyData)) {
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 (isEnter(keyData)) {
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 (isEscape(keyData)) {
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, isCtrlG, isEscape, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
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 (isEscape(keyData)) {
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 (isCtrlG(keyData)) {
77
+ if (matchesKey(keyData, "ctrl+g")) {
79
78
  this.openExternalEditor();
80
79
  return;
81
80
  }