@oh-my-pi/pi-coding-agent 5.5.0 → 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 (96) hide show
  1. package/CHANGELOG.md +98 -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
@@ -59,16 +59,28 @@ export class InputController {
59
59
  this.ctx.ui.onDebug = () => this.ctx.handleDebugCommand();
60
60
  this.ctx.editor.onCtrlL = () => this.ctx.showModelSelector();
61
61
  this.ctx.editor.onCtrlR = () => this.ctx.showHistorySearch();
62
- this.ctx.editor.onCtrlO = () => this.toggleToolOutputExpansion();
63
62
  this.ctx.editor.onCtrlT = () => this.ctx.toggleTodoExpansion();
64
63
  this.ctx.editor.onCtrlG = () => this.openExternalEditor();
65
64
  this.ctx.editor.onQuestionMark = () => this.ctx.handleHotkeysCommand();
66
65
  this.ctx.editor.onCtrlV = () => this.handleImagePaste();
67
- this.ctx.editor.onAltUp = () => this.handleDequeue();
68
66
 
69
67
  // Wire up extension shortcuts
70
68
  this.registerExtensionShortcuts();
71
69
 
70
+ const expandToolsKeys = this.ctx.keybindings.getKeys("expandTools");
71
+ this.ctx.editor.onCtrlO = expandToolsKeys.includes("ctrl+o") ? () => this.toggleToolOutputExpansion() : undefined;
72
+ for (const key of expandToolsKeys) {
73
+ if (key === "ctrl+o") continue;
74
+ this.ctx.editor.setCustomKeyHandler(key, () => this.toggleToolOutputExpansion());
75
+ }
76
+
77
+ const dequeueKeys = this.ctx.keybindings.getKeys("dequeue");
78
+ this.ctx.editor.onAltUp = dequeueKeys.includes("alt+up") ? () => this.handleDequeue() : undefined;
79
+ for (const key of dequeueKeys) {
80
+ if (key === "alt+up") continue;
81
+ this.ctx.editor.setCustomKeyHandler(key, () => this.handleDequeue());
82
+ }
83
+
72
84
  this.ctx.editor.onChange = (text: string) => {
73
85
  const wasBashMode = this.ctx.isBashMode;
74
86
  this.ctx.isBashMode = text.trimStart().startsWith("!");
@@ -122,13 +134,33 @@ export class InputController {
122
134
 
123
135
  if (!text) return;
124
136
 
137
+ const runner = this.ctx.session.extensionRunner;
138
+ let inputImages = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
139
+
140
+ if (runner?.hasHandlers("input")) {
141
+ const result = await runner.emitInput(text, inputImages, "interactive");
142
+ if (result?.handled) {
143
+ this.ctx.editor.setText("");
144
+ this.ctx.pendingImages = [];
145
+ return;
146
+ }
147
+ if (result?.text !== undefined) {
148
+ text = result.text.trim();
149
+ }
150
+ if (result?.images !== undefined) {
151
+ inputImages = result.images;
152
+ }
153
+ }
154
+
155
+ if (!text) return;
156
+
125
157
  // Handle slash commands
126
158
  if (text === "/settings") {
127
159
  this.ctx.showSettingsSelector();
128
160
  this.ctx.editor.setText("");
129
161
  return;
130
162
  }
131
- if (text === "/model") {
163
+ if (text === "/model" || text === "/models") {
132
164
  this.ctx.showModelSelector();
133
165
  this.ctx.editor.setText("");
134
166
  return;
@@ -246,7 +278,11 @@ export class InputController {
246
278
  try {
247
279
  const content = fs.readFileSync(skillPath, "utf-8");
248
280
  const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
249
- const message = args ? `${body}\n\n---\n\nUser: ${args}` : body;
281
+ const metaLines = [`Skill: ${skillPath}`];
282
+ if (args) {
283
+ metaLines.push(`User: ${args}`);
284
+ }
285
+ const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
250
286
  await this.ctx.session.prompt(message);
251
287
  } catch (err) {
252
288
  this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
@@ -288,7 +324,7 @@ export class InputController {
288
324
  if (this.ctx.session.isStreaming) {
289
325
  this.ctx.editor.addToHistory(text);
290
326
  this.ctx.editor.setText("");
291
- const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
327
+ const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
292
328
  this.ctx.pendingImages = [];
293
329
  await this.ctx.session.prompt(text, { streamingBehavior: "steer", images });
294
330
  this.ctx.updatePendingMessagesDisplay();
@@ -317,7 +353,7 @@ export class InputController {
317
353
 
318
354
  if (this.ctx.onInputCallback) {
319
355
  // Include any pending images from clipboard paste
320
- const images = this.ctx.pendingImages.length > 0 ? [...this.ctx.pendingImages] : undefined;
356
+ const images = inputImages && inputImages.length > 0 ? [...inputImages] : undefined;
321
357
  this.ctx.pendingImages = [];
322
358
  this.ctx.onInputCallback({ text, images });
323
359
  }
@@ -19,8 +19,9 @@ import {
19
19
  import chalk from "chalk";
20
20
  import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
21
21
  import type { ExtensionUIContext } from "../../core/extensions/index";
22
+ import type { CompactOptions } from "../../core/extensions/types";
22
23
  import { HistoryStorage } from "../../core/history-storage";
23
- import type { KeybindingsManager } from "../../core/keybindings";
24
+ import { KeybindingsManager } from "../../core/keybindings";
24
25
  import { logger } from "../../core/logger";
25
26
  import type { SessionContext, SessionManager } from "../../core/session-manager";
26
27
  import { getRecentSessions } from "../../core/session-manager";
@@ -71,6 +72,7 @@ export class InteractiveMode implements InteractiveModeContext {
71
72
  public session: AgentSession;
72
73
  public sessionManager: SessionManager;
73
74
  public settingsManager: SettingsManager;
75
+ public keybindings: KeybindingsManager;
74
76
  public agent: AgentSession["agent"];
75
77
  public voiceSupervisor: VoiceSupervisor;
76
78
  public historyStorage?: HistoryStorage;
@@ -126,8 +128,9 @@ export class InteractiveMode implements InteractiveModeContext {
126
128
  private cleanupUnsubscribe?: () => void;
127
129
  private readonly version: string;
128
130
  private readonly changelogMarkdown: string | undefined;
129
- private readonly lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined =
131
+ public readonly lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined =
130
132
  undefined;
133
+ public mcpManager?: import("../../core/mcp/index").MCPManager;
131
134
  private readonly toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
132
135
 
133
136
  private readonly commandController: CommandController;
@@ -144,23 +147,25 @@ export class InteractiveMode implements InteractiveModeContext {
144
147
  changelogMarkdown: string | undefined = undefined,
145
148
  setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
146
149
  lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined = undefined,
150
+ mcpManager?: import("../../core/mcp/index").MCPManager,
147
151
  ) {
148
152
  this.session = session;
149
153
  this.sessionManager = session.sessionManager;
150
154
  this.settingsManager = session.settingsManager;
155
+ this.keybindings = KeybindingsManager.inMemory();
151
156
  this.agent = session.agent;
152
157
  this.version = version;
153
158
  this.changelogMarkdown = changelogMarkdown;
154
159
  this.toolUiContextSetter = setToolUIContext;
155
160
  this.lspServers = lspServers;
161
+ this.mcpManager = mcpManager;
156
162
 
157
- this.ui = new TUI(new ProcessTerminal());
163
+ this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
158
164
  this.chatContainer = new Container();
159
165
  this.pendingMessagesContainer = new Container();
160
166
  this.statusContainer = new Container();
161
167
  this.todoContainer = new Container();
162
168
  this.editor = new CustomEditor(getEditorTheme());
163
- this.editor.setUseTerminalCursor(true);
164
169
  this.editor.onAutocompleteCancel = () => {
165
170
  this.ui.requestRender(true);
166
171
  };
@@ -226,6 +231,7 @@ export class InteractiveMode implements InteractiveModeContext {
226
231
  const hookCommands: SlashCommand[] = (this.session.extensionRunner?.getRegisteredCommands() ?? []).map((cmd) => ({
227
232
  name: cmd.name,
228
233
  description: cmd.description ?? "(hook command)",
234
+ getArgumentCompletions: cmd.getArgumentCompletions,
229
235
  }));
230
236
 
231
237
  // Convert custom commands (TypeScript) to SlashCommand format
@@ -259,6 +265,8 @@ export class InteractiveMode implements InteractiveModeContext {
259
265
  async init(): Promise<void> {
260
266
  if (this.isInitialized) return;
261
267
 
268
+ this.keybindings = await KeybindingsManager.create();
269
+
262
270
  // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
263
271
  this.cleanupUnsubscribe = registerAsyncCleanup(() => this.sessionManager.flush());
264
272
 
@@ -295,8 +303,34 @@ export class InteractiveMode implements InteractiveModeContext {
295
303
  fileTypes: s.fileTypes,
296
304
  })) ?? [];
297
305
 
298
- // Add welcome header
299
- const welcome = new WelcomeComponent(this.version, modelName, providerName, recentSessions, lspServerInfo);
306
+ const startupQuiet = this.settingsManager.getStartupQuiet();
307
+
308
+ if (!startupQuiet) {
309
+ // Add welcome header
310
+ const welcome = new WelcomeComponent(this.version, modelName, providerName, recentSessions, lspServerInfo);
311
+
312
+ // Setup UI layout
313
+ this.ui.addChild(new Spacer(1));
314
+ this.ui.addChild(welcome);
315
+ this.ui.addChild(new Spacer(1));
316
+
317
+ // Add changelog if provided
318
+ if (this.changelogMarkdown) {
319
+ this.ui.addChild(new DynamicBorder());
320
+ if (this.settingsManager.getCollapseChangelog()) {
321
+ const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
322
+ const latestVersion = versionMatch ? versionMatch[1] : this.version;
323
+ const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
324
+ this.ui.addChild(new Text(condensedText, 1, 0));
325
+ } else {
326
+ this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
327
+ this.ui.addChild(new Spacer(1));
328
+ this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
329
+ this.ui.addChild(new Spacer(1));
330
+ }
331
+ this.ui.addChild(new DynamicBorder());
332
+ }
333
+ }
300
334
 
301
335
  // Set terminal title if session already has one (resumed session)
302
336
  const existingTitle = this.sessionManager.getSessionTitle();
@@ -304,28 +338,6 @@ export class InteractiveMode implements InteractiveModeContext {
304
338
  setTerminalTitle(`pi: ${existingTitle}`);
305
339
  }
306
340
 
307
- // Setup UI layout
308
- this.ui.addChild(new Spacer(1));
309
- this.ui.addChild(welcome);
310
- this.ui.addChild(new Spacer(1));
311
-
312
- // Add changelog if provided
313
- if (this.changelogMarkdown) {
314
- this.ui.addChild(new DynamicBorder());
315
- if (this.settingsManager.getCollapseChangelog()) {
316
- const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
317
- const latestVersion = versionMatch ? versionMatch[1] : this.version;
318
- const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
319
- this.ui.addChild(new Text(condensedText, 1, 0));
320
- } else {
321
- this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
322
- this.ui.addChild(new Spacer(1));
323
- this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
324
- this.ui.addChild(new Spacer(1));
325
- }
326
- this.ui.addChild(new DynamicBorder());
327
- }
328
-
329
341
  this.ui.addChild(this.chatContainer);
330
342
  this.ui.addChild(this.pendingMessagesContainer);
331
343
  this.ui.addChild(this.statusContainer);
@@ -344,6 +356,7 @@ export class InteractiveMode implements InteractiveModeContext {
344
356
  // Start the UI
345
357
  this.ui.start();
346
358
  this.isInitialized = true;
359
+ this.ui.requestRender(true);
347
360
 
348
361
  // Set initial terminal title (will be updated when session title is generated)
349
362
  this.ui.terminal.setTitle("π");
@@ -369,6 +382,15 @@ export class InteractiveMode implements InteractiveModeContext {
369
382
 
370
383
  // Initial top border update
371
384
  this.updateEditorTopBorder();
385
+
386
+ if (!startupQuiet) {
387
+ const templateNames = this.session.promptTemplates.map((template) => template.name).sort();
388
+ if (templateNames.length > 0) {
389
+ const preview = templateNames.slice(0, 3).join(", ");
390
+ const suffix = templateNames.length > 3 ? ` +${templateNames.length - 3} more` : "";
391
+ this.showStatus(`Loaded prompt templates: ${preview}${suffix}`);
392
+ }
393
+ }
372
394
  }
373
395
 
374
396
  async getUserInput(): Promise<{ text: string; images?: ImageContent[] }> {
@@ -510,6 +532,10 @@ export class InteractiveMode implements InteractiveModeContext {
510
532
  // Emit shutdown event to hooks
511
533
  await this.session.emitCustomToolSessionEvent("shutdown");
512
534
 
535
+ if (this.isInitialized) {
536
+ await this.ui.waitForRender();
537
+ }
538
+
513
539
  this.stop();
514
540
  process.exit(0);
515
541
  }
@@ -654,8 +680,8 @@ export class InteractiveMode implements InteractiveModeContext {
654
680
  return this.commandController.handleCompactCommand(customInstructions);
655
681
  }
656
682
 
657
- executeCompaction(customInstructions?: string, isAuto?: boolean): Promise<void> {
658
- return this.commandController.executeCompaction(customInstructions, isAuto);
683
+ executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void> {
684
+ return this.commandController.executeCompaction(customInstructionsOrOptions, isAuto);
659
685
  }
660
686
 
661
687
  openInBrowser(urlOrPath: string): void {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "title": "Pi Coding Agent Theme",
4
- "description": "Theme schema for OMP coding agent",
3
+ "title": "OMP Coding Agent Theme",
4
+ "description": "Theme schema for the OMP coding agent",
5
5
  "type": "object",
6
6
  "required": ["name", "colors"],
7
7
  "properties": {
@@ -3,8 +3,10 @@ import type { AssistantMessage, ImageContent, Message } from "@oh-my-pi/pi-ai";
3
3
  import type { Component, Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
4
4
  import type { AgentSession, AgentSessionEvent } from "../../core/agent-session";
5
5
  import type { ExtensionUIContext } from "../../core/extensions/index";
6
+ import type { CompactOptions } from "../../core/extensions/types";
6
7
  import type { HistoryStorage } from "../../core/history-storage";
7
8
  import type { KeybindingsManager } from "../../core/keybindings";
9
+ import type { MCPManager } from "../../core/mcp/index";
8
10
  import type { SessionContext, SessionManager } from "../../core/session-manager";
9
11
  import type { SettingsManager } from "../../core/settings-manager";
10
12
  import type { VoiceSupervisor } from "../../core/voice-supervisor";
@@ -45,9 +47,12 @@ export interface InteractiveModeContext {
45
47
  session: AgentSession;
46
48
  sessionManager: SessionManager;
47
49
  settingsManager: SettingsManager;
50
+ keybindings: KeybindingsManager;
48
51
  agent: AgentSession["agent"];
49
52
  voiceSupervisor: VoiceSupervisor;
50
53
  historyStorage?: HistoryStorage;
54
+ mcpManager?: MCPManager;
55
+ lspServers?: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }>;
51
56
 
52
57
  // State
53
58
  isInitialized: boolean;
@@ -140,7 +145,7 @@ export interface InteractiveModeContext {
140
145
  handleArminSaysHi(): void;
141
146
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
142
147
  handleCompactCommand(customInstructions?: string): Promise<void>;
143
- executeCompaction(customInstructions?: string, isAuto?: boolean): Promise<void>;
148
+ executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
144
149
  openInBrowser(urlOrPath: string): void;
145
150
 
146
151
  // Selector handling
@@ -340,7 +340,8 @@ export class UiHelpers {
340
340
  const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
341
341
  this.ctx.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
342
342
  }
343
- const hintText = theme.fg("dim", `${theme.tree.hook} Alt+Up to edit`);
343
+ const dequeueKey = this.ctx.keybindings.getDisplayString("dequeue") || "Alt+Up";
344
+ const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to edit`);
344
345
  this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
345
346
  }
346
347
  }
@@ -58,6 +58,9 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
58
58
  appendEntry: (customType, data) => {
59
59
  session.sessionManager.appendCustomEntry(customType, data);
60
60
  },
61
+ setLabel: (targetId, label) => {
62
+ session.sessionManager.appendLabelChange(targetId, label);
63
+ },
61
64
  getActiveTools: () => session.getActiveToolNames(),
62
65
  getAllTools: () => session.getAllToolNames(),
63
66
  setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
@@ -77,9 +80,19 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
77
80
  abort: () => session.abort(),
78
81
  hasPendingMessages: () => session.queuedMessageCount > 0,
79
82
  shutdown: () => {},
83
+ getContextUsage: () => session.getContextUsage(),
84
+ compact: async (instructionsOrOptions) => {
85
+ const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
86
+ const options =
87
+ instructionsOrOptions && typeof instructionsOrOptions === "object"
88
+ ? instructionsOrOptions
89
+ : undefined;
90
+ await session.compact(instructions, options);
91
+ },
80
92
  },
81
93
  // ExtensionCommandContextActions - commands invokable via prompt("/command")
82
94
  {
95
+ getContextUsage: () => session.getContextUsage(),
83
96
  waitForIdle: () => session.agent.waitForIdle(),
84
97
  newSession: async (options) => {
85
98
  const success = await session.newSession({ parentSession: options?.parentSession });
@@ -96,6 +109,14 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
96
109
  const result = await session.navigateTree(targetId, { summarize: options?.summarize });
97
110
  return { cancelled: result.cancelled };
98
111
  },
112
+ compact: async (instructionsOrOptions) => {
113
+ const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
114
+ const options =
115
+ instructionsOrOptions && typeof instructionsOrOptions === "object"
116
+ ? instructionsOrOptions
117
+ : undefined;
118
+ await session.compact(instructions, options);
119
+ },
99
120
  },
100
121
  // No UI context
101
122
  );
@@ -157,4 +178,6 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
157
178
  else resolve();
158
179
  });
159
180
  });
181
+
182
+ await session.dispose();
160
183
  }
@@ -289,6 +289,9 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
289
289
  appendEntry: (customType, data) => {
290
290
  session.sessionManager.appendCustomEntry(customType, data);
291
291
  },
292
+ setLabel: (targetId, label) => {
293
+ session.sessionManager.appendLabelChange(targetId, label);
294
+ },
292
295
  getActiveTools: () => session.getActiveToolNames(),
293
296
  getAllTools: () => session.getAllToolNames(),
294
297
  setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
@@ -310,9 +313,19 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
310
313
  shutdown: () => {
311
314
  shutdownState.requested = true;
312
315
  },
316
+ getContextUsage: () => session.getContextUsage(),
317
+ compact: async (instructionsOrOptions) => {
318
+ const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
319
+ const options =
320
+ instructionsOrOptions && typeof instructionsOrOptions === "object"
321
+ ? instructionsOrOptions
322
+ : undefined;
323
+ await session.compact(instructions, options);
324
+ },
313
325
  },
314
326
  // ExtensionCommandContextActions - commands invokable via prompt("/command")
315
327
  {
328
+ getContextUsage: () => session.getContextUsage(),
316
329
  waitForIdle: () => session.agent.waitForIdle(),
317
330
  newSession: async (options) => {
318
331
  const success = await session.newSession({ parentSession: options?.parentSession });
@@ -330,6 +343,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
330
343
  const result = await session.navigateTree(targetId, { summarize: options?.summarize });
331
344
  return { cancelled: result.cancelled };
332
345
  },
346
+ compact: async (instructionsOrOptions) => {
347
+ const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
348
+ const options =
349
+ instructionsOrOptions && typeof instructionsOrOptions === "object"
350
+ ? instructionsOrOptions
351
+ : undefined;
352
+ await session.compact(instructions, options);
353
+ },
333
354
  },
334
355
  createExtensionUIContext(),
335
356
  );
@@ -43,7 +43,7 @@ You are a senior engineer reviewing a proposed code change. Your goal: identify
43
43
  4. Call `report_finding` for each issue
44
44
  5. Call `complete` with your verdict — **review is incomplete until `complete` is called**
45
45
 
46
- Bash is read-only: `git diff`, `git log`, `git show`, `gh pr diff`. No file modifications or builds.
46
+ Bash is read-only here: `git diff`, `git log`, `git show`, `gh pr diff`. No file modifications or builds.
47
47
 
48
48
  # What to Flag
49
49
 
@@ -67,6 +67,7 @@ This matters. Get it right.
67
67
 
68
68
  Every tool is a choice. The wrong choice is friction. The right choice is invisible.
69
69
 
70
+ {{#has tools "bash"}}
70
71
  ### What bash IS for
71
72
  File and system operations:
72
73
  - `mv`, `cp`, `rm`, `ln -s` — moving, copying, deleting, symlinking
@@ -95,6 +96,35 @@ Specialized tools exist. Use them.
95
96
  {{#has tools "edit"}}- Content-addressed edits: `edit` finds text. Use bash for position/pattern (append, line N, regex).{{/has}}
96
97
  {{#has tools "git"}}- Git operations: `git` tool has guards. Bash git has none.{{/has}}
97
98
 
99
+ {{/has}}
100
+
101
+ {{#has tools "python"}}
102
+ ### What python IS for
103
+ Python is your scripting language. Bash is for build tools and system commands only.
104
+
105
+ **Use Python for:**
106
+ - Loops, conditionals, any multi-step logic
107
+ - Text processing (sorting, filtering, column extraction, regex)
108
+ - File operations (copy, move, concat, batch transforms)
109
+ - Displaying content to the user
110
+ - Anything you'd write a bash script for
111
+
112
+ **Use bash only for:**
113
+ - Build commands: `cargo`, `npm`, `make`, `docker`
114
+ - Git operations (when git tool unavailable)
115
+ - System commands with no Python equivalent
116
+
117
+ The prelude provides shell-like helpers: `cat()`, `sed()`, `rsed()`, `find()`, `grep()`, `batch()`.
118
+ Do not write bash loops, sed pipelines, or awk scripts. Write Python.
119
+
120
+ ### Python for user-facing output
121
+ When the user asks you to display, concatenate, merge, or transform content:
122
+ → Python. One operation. Clean output.
123
+
124
+ Do not read files individually just to print them back. That's mechanical and wasteful.
125
+ Read/grep are for YOUR reconnaissance. Python is for THE USER's request.
126
+ {{/has}}
127
+
98
128
  ### Hierarchy of trust
99
129
  The most constrained tool is the most trustworthy.
100
130
 
@@ -104,7 +134,8 @@ The most constrained tool is the most trustworthy.
104
134
  {{#has tools "read"}}4. **read** — content truth{{/has}}
105
135
  {{#has tools "edit"}}5. **edit** — surgical change{{/has}}
106
136
  {{#has tools "git"}}6. **git** — versioned change with safety{{/has}}
107
- 7. **bash** — everything else ({{#unless (includes tools "git")}}git, {{/unless}}npm, docker, make, cargo)
137
+ {{#has tools "bash"}}7. **bash** — everything else ({{#unless (includes tools "git")}}git, {{/unless}}npm, docker, make, cargo){{/has}}
138
+ {{#unless (includes tools "bash")}}{{#has tools "python"}}7. **python** — stateful scripting and REPL work{{/has}}{{/unless}}
108
139
 
109
140
  {{#has tools "lsp"}}
110
141
  ### LSP knows what grep guesses
@@ -0,0 +1,91 @@
1
+ Executes Python code in an IPython kernel (session or per-call) with optional timeout.
2
+
3
+ ## When to use Python
4
+
5
+ **Use Python for user-facing operations:**
6
+ - Displaying, concatenating, or merging files → `cat(*paths)`
7
+ - Batch transformations across files → `batch(paths, fn)`, `rsed()`
8
+ - Formatted output, tables, summaries
9
+ - Any loop, conditional, or multi-step logic
10
+ - Anything you'd write a bash script for
11
+
12
+ **Use specialized tools for YOUR reconnaissance:**
13
+ - Reading to understand code → Read tool
14
+ - Searching to locate something → Grep tool
15
+ - Finding files to identify targets → Find tool
16
+
17
+ The distinction: Read/Grep/Find gather info for *your* decisions. Python executes *the user's* request.
18
+
19
+ **Prefer Python over bash for:**
20
+ - Loops and iteration → Python for-loops, not bash for/while
21
+ - Text processing → `sed()`, `cols()`, `sort_lines()`, not sed/awk/cut
22
+ - File operations → prelude helpers, not mv/cp/rm commands
23
+ - Conditionals → Python if/else, not bash [[ ]]
24
+
25
+ **Shell commands:** Use `sh()` or `run()`, never raw `subprocess`:
26
+ ```python
27
+ # Good
28
+ sh("bun run check")
29
+ run("cargo build --release")
30
+
31
+ # Bad - never use subprocess directly
32
+ import subprocess
33
+ subprocess.run(["bun", "run", "check"], ...)
34
+ ```
35
+
36
+ ## Prelude helpers
37
+
38
+ All helpers auto-print results and return values for chaining.
39
+
40
+ {{#if categories.length}}
41
+ {{#each categories}}
42
+ ### {{name}}
43
+ ```
44
+ {{#each functions}}
45
+ {{name}}{{signature}}
46
+ {{docstring}}
47
+ {{/each}}
48
+ ```
49
+
50
+ {{/each}}
51
+ {{else}}
52
+ (Documentation unavailable — Python kernel failed to start)
53
+ {{/if}}
54
+
55
+ ## Examples
56
+
57
+ ```python
58
+ # Concatenate all markdown files in docs/
59
+ cat(*find("*.md", "docs"))
60
+
61
+ # Mass rename: foo -> bar across all .py files
62
+ rsed(r'\bfoo\b', 'bar', glob_pattern="*.py")
63
+
64
+ # Process files in batch
65
+ batch(find("*.json"), lambda p: json.loads(p.read_text()))
66
+
67
+ # Sort and deduplicate lines
68
+ sort_lines(read("data.txt"), unique=True)
69
+
70
+ # Extract columns 0 and 2 from TSV
71
+ cols(read("data.tsv"), 0, 2, sep="\t")
72
+ ```
73
+
74
+ ## Notes
75
+
76
+ - Code executes as IPython cells; users see the full cell output (including rendered figures, tables, etc.)
77
+ - Kernel persists for the session by default; per-call mode uses a fresh kernel each call. Use `reset: true` to clear state when session mode is active
78
+ - Use `workdir` parameter instead of `os.chdir()` in tool call
79
+ - Use `plt.show()` to display figures
80
+ - Use `display()` from IPython.display for rich output (HTML, Markdown, images, etc.)
81
+ - Output streams in real time, truncated after 50KB
82
+
83
+ ## Rich output rendering
84
+
85
+ The user sees output like a Jupyter notebook—rich displays are fully rendered:
86
+ - `display(JSON(data))` → interactive JSON tree
87
+ - `display(HTML(...))` → rendered HTML
88
+ - `display(Markdown(...))` → formatted markdown
89
+ - `plt.show()` → inline figures
90
+
91
+ **You will see object repr** (e.g., `<IPython.core.display.JSON object>`) **but the user sees the rendered output.** Trust that `display()` calls work correctly—do not assume the user sees only the repr.