@oh-my-pi/pi-coding-agent 6.8.5 → 6.9.69

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 (155) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/package.json +6 -6
  3. package/src/cli/stats-cli.ts +191 -0
  4. package/src/core/agent-session.ts +103 -1
  5. package/src/core/extensions/index.ts +2 -0
  6. package/src/core/extensions/runner.ts +31 -0
  7. package/src/core/extensions/types.ts +24 -0
  8. package/src/core/messages.ts +48 -0
  9. package/src/core/sdk.ts +0 -2
  10. package/src/core/session-manager.ts +10 -1
  11. package/src/core/settings-manager.ts +0 -105
  12. package/src/core/tools/bash.ts +5 -7
  13. package/src/core/tools/index.ts +1 -5
  14. package/src/core/tools/patch/applicator.ts +115 -17
  15. package/src/core/tools/patch/index.ts +1 -1
  16. package/src/core/tools/patch/normalize.ts +185 -10
  17. package/src/core/tools/python.ts +444 -86
  18. package/src/core/tools/task/executor.ts +2 -6
  19. package/src/core/tools/task/index.ts +30 -12
  20. package/src/core/tools/task/render.ts +163 -30
  21. package/src/core/tools/task/template.ts +37 -0
  22. package/src/core/tools/task/types.ts +6 -2
  23. package/src/core/tools/task/worker.ts +1 -1
  24. package/src/index.ts +2 -2
  25. package/src/main.ts +12 -0
  26. package/src/modes/interactive/components/python-execution.ts +180 -0
  27. package/src/modes/interactive/components/settings-defs.ts +0 -70
  28. package/src/modes/interactive/components/settings-selector.ts +0 -1
  29. package/src/modes/interactive/components/welcome.ts +1 -0
  30. package/src/modes/interactive/controllers/command-controller.ts +46 -0
  31. package/src/modes/interactive/controllers/event-controller.ts +0 -11
  32. package/src/modes/interactive/controllers/input-controller.ts +28 -1
  33. package/src/modes/interactive/controllers/selector-controller.ts +0 -9
  34. package/src/modes/interactive/interactive-mode.ts +10 -58
  35. package/src/modes/interactive/theme/dark.json +2 -9
  36. package/src/modes/interactive/theme/defaults/alabaster.json +2 -8
  37. package/src/modes/interactive/theme/defaults/amethyst.json +2 -9
  38. package/src/modes/interactive/theme/defaults/anthracite.json +2 -9
  39. package/src/modes/interactive/theme/defaults/basalt.json +89 -88
  40. package/src/modes/interactive/theme/defaults/birch.json +2 -8
  41. package/src/modes/interactive/theme/defaults/dark-abyss.json +2 -8
  42. package/src/modes/interactive/theme/defaults/dark-arctic.json +2 -9
  43. package/src/modes/interactive/theme/defaults/dark-aurora.json +3 -2
  44. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +2 -1
  45. package/src/modes/interactive/theme/defaults/dark-cavern.json +2 -8
  46. package/src/modes/interactive/theme/defaults/dark-copper.json +3 -2
  47. package/src/modes/interactive/theme/defaults/dark-cosmos.json +2 -8
  48. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +2 -9
  49. package/src/modes/interactive/theme/defaults/dark-dracula.json +2 -9
  50. package/src/modes/interactive/theme/defaults/dark-eclipse.json +2 -8
  51. package/src/modes/interactive/theme/defaults/dark-ember.json +3 -2
  52. package/src/modes/interactive/theme/defaults/dark-equinox.json +2 -8
  53. package/src/modes/interactive/theme/defaults/dark-forest.json +2 -9
  54. package/src/modes/interactive/theme/defaults/dark-github.json +2 -9
  55. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +2 -9
  56. package/src/modes/interactive/theme/defaults/dark-lavender.json +3 -2
  57. package/src/modes/interactive/theme/defaults/dark-lunar.json +2 -8
  58. package/src/modes/interactive/theme/defaults/dark-midnight.json +3 -2
  59. package/src/modes/interactive/theme/defaults/dark-monochrome.json +2 -9
  60. package/src/modes/interactive/theme/defaults/dark-monokai.json +2 -9
  61. package/src/modes/interactive/theme/defaults/dark-nebula.json +2 -8
  62. package/src/modes/interactive/theme/defaults/dark-nord.json +2 -9
  63. package/src/modes/interactive/theme/defaults/dark-ocean.json +2 -9
  64. package/src/modes/interactive/theme/defaults/dark-one.json +2 -9
  65. package/src/modes/interactive/theme/defaults/dark-rainforest.json +2 -8
  66. package/src/modes/interactive/theme/defaults/dark-reef.json +2 -8
  67. package/src/modes/interactive/theme/defaults/dark-retro.json +2 -9
  68. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +2 -1
  69. package/src/modes/interactive/theme/defaults/dark-sakura.json +3 -2
  70. package/src/modes/interactive/theme/defaults/dark-slate.json +3 -2
  71. package/src/modes/interactive/theme/defaults/dark-solarized.json +2 -1
  72. package/src/modes/interactive/theme/defaults/dark-solstice.json +2 -8
  73. package/src/modes/interactive/theme/defaults/dark-starfall.json +2 -8
  74. package/src/modes/interactive/theme/defaults/dark-sunset.json +2 -9
  75. package/src/modes/interactive/theme/defaults/dark-swamp.json +2 -8
  76. package/src/modes/interactive/theme/defaults/dark-synthwave.json +2 -1
  77. package/src/modes/interactive/theme/defaults/dark-taiga.json +2 -8
  78. package/src/modes/interactive/theme/defaults/dark-terminal.json +3 -2
  79. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +2 -9
  80. package/src/modes/interactive/theme/defaults/dark-tundra.json +2 -8
  81. package/src/modes/interactive/theme/defaults/dark-twilight.json +2 -8
  82. package/src/modes/interactive/theme/defaults/dark-volcanic.json +2 -8
  83. package/src/modes/interactive/theme/defaults/graphite.json +2 -9
  84. package/src/modes/interactive/theme/defaults/light-arctic.json +2 -1
  85. package/src/modes/interactive/theme/defaults/light-aurora-day.json +2 -8
  86. package/src/modes/interactive/theme/defaults/light-canyon.json +2 -8
  87. package/src/modes/interactive/theme/defaults/light-catppuccin.json +2 -1
  88. package/src/modes/interactive/theme/defaults/light-cirrus.json +2 -8
  89. package/src/modes/interactive/theme/defaults/light-coral.json +3 -2
  90. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +2 -9
  91. package/src/modes/interactive/theme/defaults/light-dawn.json +2 -8
  92. package/src/modes/interactive/theme/defaults/light-dunes.json +2 -8
  93. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +3 -2
  94. package/src/modes/interactive/theme/defaults/light-forest.json +2 -9
  95. package/src/modes/interactive/theme/defaults/light-frost.json +3 -2
  96. package/src/modes/interactive/theme/defaults/light-github.json +2 -1
  97. package/src/modes/interactive/theme/defaults/light-glacier.json +2 -8
  98. package/src/modes/interactive/theme/defaults/light-gruvbox.json +2 -9
  99. package/src/modes/interactive/theme/defaults/light-haze.json +2 -8
  100. package/src/modes/interactive/theme/defaults/light-honeycomb.json +3 -2
  101. package/src/modes/interactive/theme/defaults/light-lagoon.json +2 -8
  102. package/src/modes/interactive/theme/defaults/light-lavender.json +3 -2
  103. package/src/modes/interactive/theme/defaults/light-meadow.json +2 -8
  104. package/src/modes/interactive/theme/defaults/light-mint.json +3 -2
  105. package/src/modes/interactive/theme/defaults/light-monochrome.json +2 -1
  106. package/src/modes/interactive/theme/defaults/light-ocean.json +2 -9
  107. package/src/modes/interactive/theme/defaults/light-one.json +2 -8
  108. package/src/modes/interactive/theme/defaults/light-opal.json +2 -8
  109. package/src/modes/interactive/theme/defaults/light-orchard.json +2 -8
  110. package/src/modes/interactive/theme/defaults/light-paper.json +3 -2
  111. package/src/modes/interactive/theme/defaults/light-prism.json +2 -8
  112. package/src/modes/interactive/theme/defaults/light-retro.json +2 -9
  113. package/src/modes/interactive/theme/defaults/light-sand.json +3 -2
  114. package/src/modes/interactive/theme/defaults/light-savanna.json +2 -8
  115. package/src/modes/interactive/theme/defaults/light-solarized.json +2 -1
  116. package/src/modes/interactive/theme/defaults/light-soleil.json +2 -8
  117. package/src/modes/interactive/theme/defaults/light-sunset.json +2 -9
  118. package/src/modes/interactive/theme/defaults/light-synthwave.json +2 -9
  119. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +2 -9
  120. package/src/modes/interactive/theme/defaults/light-wetland.json +2 -8
  121. package/src/modes/interactive/theme/defaults/light-zenith.json +2 -8
  122. package/src/modes/interactive/theme/defaults/limestone.json +2 -8
  123. package/src/modes/interactive/theme/defaults/mahogany.json +2 -9
  124. package/src/modes/interactive/theme/defaults/marble.json +2 -8
  125. package/src/modes/interactive/theme/defaults/obsidian.json +89 -88
  126. package/src/modes/interactive/theme/defaults/onyx.json +89 -88
  127. package/src/modes/interactive/theme/defaults/pearl.json +2 -8
  128. package/src/modes/interactive/theme/defaults/porcelain.json +89 -88
  129. package/src/modes/interactive/theme/defaults/quartz.json +2 -8
  130. package/src/modes/interactive/theme/defaults/sandstone.json +2 -8
  131. package/src/modes/interactive/theme/defaults/titanium.json +88 -87
  132. package/src/modes/interactive/theme/light.json +2 -8
  133. package/src/modes/interactive/theme/theme-schema.json +5 -0
  134. package/src/modes/interactive/theme/theme.ts +7 -0
  135. package/src/modes/interactive/types.ts +5 -15
  136. package/src/modes/interactive/utils/ui-helpers.ts +20 -0
  137. package/src/prompts/system/system-prompt.md +8 -0
  138. package/src/prompts/tools/python.md +40 -2
  139. package/src/prompts/tools/task.md +8 -13
  140. package/src/core/custom-commands/bundled/wt/index.ts +0 -435
  141. package/src/core/tools/git.ts +0 -213
  142. package/src/core/voice-controller.ts +0 -135
  143. package/src/core/voice-supervisor.ts +0 -976
  144. package/src/core/voice.ts +0 -314
  145. package/src/lib/worktree/collapse.ts +0 -180
  146. package/src/lib/worktree/constants.ts +0 -14
  147. package/src/lib/worktree/errors.ts +0 -23
  148. package/src/lib/worktree/git.ts +0 -60
  149. package/src/lib/worktree/index.ts +0 -15
  150. package/src/lib/worktree/operations.ts +0 -216
  151. package/src/lib/worktree/session.ts +0 -114
  152. package/src/lib/worktree/stats.ts +0 -67
  153. package/src/modes/interactive/utils/voice-manager.ts +0 -96
  154. package/src/prompts/tools/git.md +0 -9
  155. package/src/prompts/voice-summary.md +0 -12
package/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [6.9.69] - 2026-01-21
6
+
7
+ ### Added
8
+
9
+ - Added cell-by-cell status tracking with duration and exit code for Python execution
10
+ - Added syntax highlighting for Python code in execution display
11
+ - Added template system with {{placeholders}} for task tool context
12
+ - Added task variables support for filling context placeholders
13
+ - Added enhanced task progress display with variable values
14
+ - Added concurrent work handling guidance in system prompt
15
+ - Added extension system support for user Python execution events
16
+ - Added Python mode border color theming across all themes
17
+ - Added Python execution indicator to welcome screen help text
18
+ - Added `omp stats` command for viewing AI usage statistics dashboard
19
+ - Added support for JSON output and console summary of usage statistics
20
+ - Added configurable port option for stats dashboard server
21
+ - Added multi-cell Python execution with sequential processing in persistent kernel
22
+ - Added cell titles for better Python code organization and debugging
23
+ - Added `$` command prefix for user-initiated Python execution in shared kernel
24
+ - Added `$$` prefix variant for Python execution excluded from LLM context
25
+
26
+ ### Changed
27
+
28
+ - Updated Python execution to display cells in bordered blocks with status indicators
29
+ - Changed task tool to use template-based context instead of simple concatenation
30
+ - Enhanced Python execution component with proper syntax highlighting
31
+ - Improved patch applicator to preserve exact indentation when intended
32
+ - Updated task tool schema to require vars instead of task field
33
+ - Updated Python execution component to use pythonMode theming instead of bashMode
34
+ - Enhanced UI helpers to handle pending Python components properly
35
+ - Changed Python tool to use `cells` array instead of single `code` parameter
36
+ - Renamed `workdir` parameter to `cwd` in Bash and Python tools for consistency
37
+ - Updated Python tool to display cell-by-cell output when multiple cells are provided
38
+
39
+ ### Fixed
40
+
41
+ - Fixed indentation preservation for exact matches and indentation-only patches
42
+ - Fixed Python execution status updates to show real-time cell progress
43
+ - Fixed indentation adjustment logic to handle edge cases with mixed indentation levels
44
+ - Fixed patch indentation normalization for fuzzy matches, tab/space diffs, and ambiguous context alignment
45
+
46
+ ## [6.9.0] - 2026-01-21
47
+ ### Removed
48
+
49
+ - Removed Git tool and all related functionality
50
+ - Removed voice control and TTS features
51
+ - Removed worktree management system
52
+ - Removed bundled wt custom command
53
+ - Removed voice-related settings and configuration options
54
+ - Removed @oh-my-pi/pi-git-tool dependency
55
+
5
56
  ## [6.8.5] - 2026-01-21
6
57
  ### Breaking Changes
7
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "6.8.5",
3
+ "version": "6.9.69",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -40,11 +40,11 @@
40
40
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
41
41
  },
42
42
  "dependencies": {
43
- "@oh-my-pi/pi-agent-core": "6.8.5",
44
- "@oh-my-pi/pi-ai": "6.8.5",
45
- "@oh-my-pi/pi-git-tool": "6.8.5",
46
- "@oh-my-pi/pi-tui": "6.8.5",
47
- "@oh-my-pi/pi-utils": "6.8.5",
43
+ "@oh-my-pi/omp-stats": "6.9.69",
44
+ "@oh-my-pi/pi-agent-core": "6.9.69",
45
+ "@oh-my-pi/pi-ai": "6.9.69",
46
+ "@oh-my-pi/pi-tui": "6.9.69",
47
+ "@oh-my-pi/pi-utils": "6.9.69",
48
48
  "@openai/agents": "^0.3.7",
49
49
  "@sinclair/typebox": "^0.34.46",
50
50
  "ajv": "^8.17.1",
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Stats CLI command handlers.
3
+ *
4
+ * Handles `omp stats` subcommand for viewing AI usage statistics.
5
+ */
6
+
7
+ import chalk from "chalk";
8
+ import { APP_NAME } from "../config";
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ export interface StatsCommandArgs {
15
+ port: number;
16
+ json: boolean;
17
+ summary: boolean;
18
+ }
19
+
20
+ // =============================================================================
21
+ // Argument Parser
22
+ // =============================================================================
23
+
24
+ /**
25
+ * Parse stats subcommand arguments.
26
+ * Returns undefined if not a stats command.
27
+ */
28
+ export function parseStatsArgs(args: string[]): StatsCommandArgs | undefined {
29
+ if (args.length === 0 || args[0] !== "stats") {
30
+ return undefined;
31
+ }
32
+
33
+ const result: StatsCommandArgs = {
34
+ port: 3847,
35
+ json: false,
36
+ summary: false,
37
+ };
38
+
39
+ for (let i = 1; i < args.length; i++) {
40
+ const arg = args[i];
41
+ if (arg === "--json" || arg === "-j") {
42
+ result.json = true;
43
+ } else if (arg === "--summary" || arg === "-s") {
44
+ result.summary = true;
45
+ } else if ((arg === "--port" || arg === "-p") && i + 1 < args.length) {
46
+ result.port = parseInt(args[++i], 10);
47
+ } else if (arg.startsWith("--port=")) {
48
+ result.port = parseInt(arg.split("=")[1], 10);
49
+ }
50
+ }
51
+
52
+ return result;
53
+ }
54
+
55
+ // =============================================================================
56
+ // Formatting Helpers
57
+ // =============================================================================
58
+
59
+ function formatNumber(n: number): string {
60
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
61
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
62
+ return n.toFixed(0);
63
+ }
64
+
65
+ function formatCost(n: number): string {
66
+ if (n < 0.01) return `$${n.toFixed(4)}`;
67
+ if (n < 1) return `$${n.toFixed(3)}`;
68
+ return `$${n.toFixed(2)}`;
69
+ }
70
+
71
+ function formatDuration(ms: number | null): string {
72
+ if (ms === null) return "-";
73
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
74
+ return `${(ms / 1000).toFixed(1)}s`;
75
+ }
76
+
77
+ function formatPercent(n: number): string {
78
+ return `${(n * 100).toFixed(1)}%`;
79
+ }
80
+
81
+ // =============================================================================
82
+ // Command Handler
83
+ // =============================================================================
84
+
85
+ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
86
+ // Lazy import to avoid loading stats module when not needed
87
+ const { getDashboardStats, syncAllSessions, getTotalMessageCount } = await import("@oh-my-pi/omp-stats");
88
+ const { startServer } = await import("@oh-my-pi/omp-stats/src/server");
89
+ const { closeDb } = await import("@oh-my-pi/omp-stats/src/db");
90
+
91
+ // Sync session files first
92
+ console.log("Syncing session files...");
93
+ const { processed, files } = await syncAllSessions();
94
+ const total = await getTotalMessageCount();
95
+ console.log(`Synced ${processed} new entries from ${files} files (${total} total)\n`);
96
+
97
+ if (cmd.json) {
98
+ const stats = await getDashboardStats();
99
+ console.log(JSON.stringify(stats, null, 2));
100
+ return;
101
+ }
102
+
103
+ if (cmd.summary) {
104
+ await printStatsSummary();
105
+ return;
106
+ }
107
+
108
+ // Start the dashboard server
109
+ const { port } = startServer(cmd.port);
110
+ console.log(chalk.green(`Dashboard available at: http://localhost:${port}`));
111
+ console.log("Press Ctrl+C to stop\n");
112
+
113
+ // Keep process running
114
+ process.on("SIGINT", () => {
115
+ console.log("\nShutting down...");
116
+ closeDb();
117
+ process.exit(0);
118
+ });
119
+
120
+ // Keep the process alive
121
+ await new Promise(() => {});
122
+ }
123
+
124
+ async function printStatsSummary(): Promise<void> {
125
+ const { getDashboardStats } = await import("@oh-my-pi/omp-stats");
126
+ const stats = await getDashboardStats();
127
+ const { overall, byModel, byFolder } = stats;
128
+
129
+ console.log(chalk.bold("\n=== AI Usage Statistics ===\n"));
130
+
131
+ console.log(chalk.bold("Overall:"));
132
+ console.log(` Requests: ${formatNumber(overall.totalRequests)} (${formatNumber(overall.failedRequests)} errors)`);
133
+ console.log(` Error Rate: ${formatPercent(overall.errorRate)}`);
134
+ console.log(` Total Tokens: ${formatNumber(overall.totalInputTokens + overall.totalOutputTokens)}`);
135
+ console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`);
136
+ console.log(` Total Cost: ${formatCost(overall.totalCost)}`);
137
+ console.log(` Avg Duration: ${formatDuration(overall.avgDuration)}`);
138
+ console.log(` Avg TTFT: ${formatDuration(overall.avgTtft)}`);
139
+ if (overall.avgTokensPerSecond !== null) {
140
+ console.log(` Avg Tokens/s: ${overall.avgTokensPerSecond.toFixed(1)}`);
141
+ }
142
+
143
+ if (byModel.length > 0) {
144
+ console.log(chalk.bold("\nBy Model:"));
145
+ for (const m of byModel.slice(0, 10)) {
146
+ console.log(
147
+ ` ${m.model}: ${formatNumber(m.totalRequests)} reqs, ${formatCost(m.totalCost)}, ${formatPercent(m.cacheRate)} cache`,
148
+ );
149
+ }
150
+ }
151
+
152
+ if (byFolder.length > 0) {
153
+ console.log(chalk.bold("\nBy Folder:"));
154
+ for (const f of byFolder.slice(0, 10)) {
155
+ console.log(` ${f.folder}: ${formatNumber(f.totalRequests)} reqs, ${formatCost(f.totalCost)}`);
156
+ }
157
+ }
158
+
159
+ console.log("");
160
+ }
161
+
162
+ // =============================================================================
163
+ // Help
164
+ // =============================================================================
165
+
166
+ export function printStatsHelp(): void {
167
+ console.log(`${chalk.bold(`${APP_NAME} stats`)} - AI Usage Statistics Dashboard
168
+
169
+ ${chalk.bold("Usage:")}
170
+ ${APP_NAME} stats [options]
171
+
172
+ ${chalk.bold("Options:")}
173
+ -p, --port <port> Port for the dashboard server (default: 3847)
174
+ -j, --json Output stats as JSON and exit
175
+ -s, --summary Print summary to console and exit
176
+ -h, --help Show this help message
177
+
178
+ ${chalk.bold("Examples:")}
179
+ ${APP_NAME} stats # Start dashboard server
180
+ ${APP_NAME} stats --json # Print stats as JSON
181
+ ${APP_NAME} stats --summary # Print summary to console
182
+ ${APP_NAME} stats --port 8080 # Start on custom port
183
+
184
+ ${chalk.bold("Metrics:")}
185
+ - Total requests and error rate
186
+ - Token usage (input, output, cache)
187
+ - Cost breakdown
188
+ - Average duration and time to first token (TTFT)
189
+ - Tokens per second throughput
190
+ `);
191
+ }
@@ -50,10 +50,11 @@ import type {
50
50
  import type { CompactOptions, ContextUsage } from "./extensions/types";
51
51
  import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
52
52
  import type { HookCommandContext } from "./hooks/types";
53
- import type { BashExecutionMessage, CustomMessage } from "./messages";
53
+ import type { BashExecutionMessage, CustomMessage, PythonExecutionMessage } from "./messages";
54
54
  import type { ModelRegistry } from "./model-registry";
55
55
  import { parseModelString } from "./model-resolver";
56
56
  import { expandPromptTemplate, type PromptTemplate, parseCommandArgs, renderPromptTemplate } from "./prompt-templates";
57
+ import { executePython as executePythonCommand, type PythonResult } from "./python-executor";
57
58
  import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
58
59
  import type { SettingsManager, SkillsSettings } from "./settings-manager";
59
60
  import type { Skill, SkillWarning } from "./skills";
@@ -248,6 +249,10 @@ export class AgentSession {
248
249
  private _bashAbortController: AbortController | undefined = undefined;
249
250
  private _pendingBashMessages: BashExecutionMessage[] = [];
250
251
 
252
+ // Python execution state
253
+ private _pythonAbortController: AbortController | undefined = undefined;
254
+ private _pendingPythonMessages: PythonExecutionMessage[] = [];
255
+
251
256
  // Extension system
252
257
  private _extensionRunner: ExtensionRunner | undefined = undefined;
253
258
  private _turnIndex = 0;
@@ -1005,6 +1010,7 @@ export class AgentSession {
1005
1010
 
1006
1011
  // Flush any pending bash messages before the new prompt
1007
1012
  this._flushPendingBashMessages();
1013
+ this._flushPendingPythonMessages();
1008
1014
 
1009
1015
  // Reset todo reminder count on new user prompt
1010
1016
  this._todoReminderCount = 0;
@@ -2631,6 +2637,102 @@ export class AgentSession {
2631
2637
  this._pendingBashMessages = [];
2632
2638
  }
2633
2639
 
2640
+ // =========================================================================
2641
+ // User-Initiated Python Execution
2642
+ // =========================================================================
2643
+
2644
+ /**
2645
+ * Execute Python code in the shared kernel.
2646
+ * Uses the same kernel session as the agent's Python tool, allowing collaborative editing.
2647
+ * @param code The Python code to execute
2648
+ * @param onChunk Optional streaming callback for output
2649
+ * @param options.excludeFromContext If true, execution won't be sent to LLM ($$ prefix)
2650
+ */
2651
+ async executePython(
2652
+ code: string,
2653
+ onChunk?: (chunk: string) => void,
2654
+ options?: { excludeFromContext?: boolean },
2655
+ ): Promise<PythonResult> {
2656
+ this._pythonAbortController = new AbortController();
2657
+
2658
+ try {
2659
+ // Use the same session ID as the Python tool for kernel sharing
2660
+ const sessionFile = this.sessionManager.getSessionFile();
2661
+ const cwd = this.sessionManager.getCwd();
2662
+ const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
2663
+
2664
+ const result = await executePythonCommand(code, {
2665
+ cwd,
2666
+ sessionId,
2667
+ kernelMode: this.settingsManager?.getPythonKernelMode?.() ?? "session",
2668
+ useSharedGateway: this.settingsManager?.getPythonSharedGateway?.() ?? true,
2669
+ onChunk,
2670
+ signal: this._pythonAbortController.signal,
2671
+ });
2672
+
2673
+ this.recordPythonResult(code, result, options);
2674
+ return result;
2675
+ } finally {
2676
+ this._pythonAbortController = undefined;
2677
+ }
2678
+ }
2679
+
2680
+ /**
2681
+ * Record a Python execution result in session history.
2682
+ */
2683
+ recordPythonResult(code: string, result: PythonResult, options?: { excludeFromContext?: boolean }): void {
2684
+ const pythonMessage: PythonExecutionMessage = {
2685
+ role: "pythonExecution",
2686
+ code,
2687
+ output: result.output,
2688
+ exitCode: result.exitCode,
2689
+ cancelled: result.cancelled,
2690
+ truncated: result.truncated,
2691
+ fullOutputPath: result.fullOutputPath,
2692
+ timestamp: Date.now(),
2693
+ excludeFromContext: options?.excludeFromContext,
2694
+ };
2695
+
2696
+ // If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering
2697
+ if (this.isStreaming) {
2698
+ this._pendingPythonMessages.push(pythonMessage);
2699
+ } else {
2700
+ this.agent.appendMessage(pythonMessage);
2701
+ this.sessionManager.appendMessage(pythonMessage);
2702
+ }
2703
+ }
2704
+
2705
+ /**
2706
+ * Cancel running Python execution.
2707
+ */
2708
+ abortPython(): void {
2709
+ this._pythonAbortController?.abort();
2710
+ }
2711
+
2712
+ /** Whether a Python execution is currently running */
2713
+ get isPythonRunning(): boolean {
2714
+ return this._pythonAbortController !== undefined;
2715
+ }
2716
+
2717
+ /** Whether there are pending Python messages waiting to be flushed */
2718
+ get hasPendingPythonMessages(): boolean {
2719
+ return this._pendingPythonMessages.length > 0;
2720
+ }
2721
+
2722
+ /**
2723
+ * Flush pending Python messages to agent state and session.
2724
+ */
2725
+ private _flushPendingPythonMessages(): void {
2726
+ if (this._pendingPythonMessages.length === 0) return;
2727
+
2728
+ for (const pythonMessage of this._pendingPythonMessages) {
2729
+ this.agent.appendMessage(pythonMessage);
2730
+ this.sessionManager.appendMessage(pythonMessage);
2731
+ }
2732
+
2733
+ this._pendingPythonMessages = [];
2734
+ }
2735
+
2634
2736
  // =========================================================================
2635
2737
  // Session Management
2636
2738
  // =========================================================================
@@ -98,6 +98,8 @@ export type {
98
98
  TurnStartEvent,
99
99
  UserBashEvent,
100
100
  UserBashEventResult,
101
+ UserPythonEvent,
102
+ UserPythonEventResult,
101
103
  WriteToolResultEvent,
102
104
  } from "./types";
103
105
  // Type guards
@@ -40,6 +40,8 @@ import type {
40
40
  ToolResultEventResult,
41
41
  UserBashEvent,
42
42
  UserBashEventResult,
43
+ UserPythonEvent,
44
+ UserPythonEventResult,
43
45
  } from "./types";
44
46
 
45
47
  /** Combined result from all before_agent_start handlers */
@@ -461,6 +463,35 @@ export class ExtensionRunner {
461
463
  return undefined;
462
464
  }
463
465
 
466
+ async emitUserPython(event: UserPythonEvent): Promise<UserPythonEventResult | undefined> {
467
+ const ctx = this.createContext();
468
+
469
+ for (const ext of this.extensions) {
470
+ const handlers = ext.handlers.get("user_python");
471
+ if (!handlers || handlers.length === 0) continue;
472
+
473
+ for (const handler of handlers) {
474
+ try {
475
+ const handlerResult = await handler(event, ctx);
476
+ if (handlerResult) {
477
+ return handlerResult as UserPythonEventResult;
478
+ }
479
+ } catch (err) {
480
+ const message = err instanceof Error ? err.message : String(err);
481
+ const stack = err instanceof Error ? err.stack : undefined;
482
+ this.emitError({
483
+ extensionPath: ext.path,
484
+ event: "user_python",
485
+ error: message,
486
+ stack,
487
+ });
488
+ }
489
+ }
490
+ }
491
+
492
+ return undefined;
493
+ }
494
+
464
495
  /** Emit input event. Transforms chain, "handled" short-circuits. */
465
496
  async emitInput(
466
497
  text: string,
@@ -21,6 +21,7 @@ import type { ExecOptions, ExecResult } from "../exec";
21
21
  import type { KeybindingsManager } from "../keybindings";
22
22
  import type { CustomMessage } from "../messages";
23
23
  import type { ModelRegistry } from "../model-registry";
24
+ import type { PythonResult } from "../python-executor";
24
25
  import type {
25
26
  BranchSummaryEntry,
26
27
  CompactionEntry,
@@ -405,6 +406,21 @@ export interface UserBashEvent {
405
406
  cwd: string;
406
407
  }
407
408
 
409
+ // ============================================================================
410
+ // User Python Events
411
+ // ============================================================================
412
+
413
+ /** Fired when user executes Python code via $ or $$ prefix */
414
+ export interface UserPythonEvent {
415
+ type: "user_python";
416
+ /** The Python code to execute */
417
+ code: string;
418
+ /** True if $$ prefix was used (excluded from LLM context) */
419
+ excludeFromContext: boolean;
420
+ /** Current working directory */
421
+ cwd: string;
422
+ }
423
+
408
424
  // ============================================================================
409
425
  // Input Events
410
426
  // ============================================================================
@@ -521,6 +537,7 @@ export type ExtensionEvent =
521
537
  | TurnStartEvent
522
538
  | TurnEndEvent
523
539
  | UserBashEvent
540
+ | UserPythonEvent
524
541
  | InputEvent
525
542
  | ToolCallEvent
526
543
  | ToolResultEvent;
@@ -554,6 +571,12 @@ export interface UserBashEventResult {
554
571
  result?: BashResult;
555
572
  }
556
573
 
574
+ /** Result from user_python event handler */
575
+ export interface UserPythonEventResult {
576
+ /** Full replacement: extension handled execution, use this result */
577
+ result?: PythonResult;
578
+ }
579
+
557
580
  export interface ToolResultEventResult {
558
581
  content?: (TextContent | ImageContent)[];
559
582
  details?: unknown;
@@ -671,6 +694,7 @@ export interface ExtensionAPI {
671
694
  on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
672
695
  on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
673
696
  on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
697
+ on(event: "user_python", handler: ExtensionHandler<UserPythonEvent, UserPythonEventResult>): void;
674
698
 
675
699
  // =========================================================================
676
700
  // Tool Registration
@@ -39,6 +39,23 @@ export interface BashExecutionMessage {
39
39
  excludeFromContext?: boolean;
40
40
  }
41
41
 
42
+ /**
43
+ * Message type for user-initiated Python executions via the $ command.
44
+ * Shares the same kernel session as the agent's Python tool.
45
+ */
46
+ export interface PythonExecutionMessage {
47
+ role: "pythonExecution";
48
+ code: string;
49
+ output: string;
50
+ exitCode: number | undefined;
51
+ cancelled: boolean;
52
+ truncated: boolean;
53
+ fullOutputPath?: string;
54
+ timestamp: number;
55
+ /** If true, this message is excluded from LLM context ($$ prefix) */
56
+ excludeFromContext?: boolean;
57
+ }
58
+
42
59
  /**
43
60
  * Message type for extension-injected messages via sendMessage().
44
61
  */
@@ -95,6 +112,7 @@ export interface FileMentionMessage {
95
112
  declare module "@oh-my-pi/pi-agent-core" {
96
113
  interface CustomAgentMessages {
97
114
  bashExecution: BashExecutionMessage;
115
+ pythonExecution: PythonExecutionMessage;
98
116
  custom: CustomMessage;
99
117
  hookMessage: HookMessage;
100
118
  branchSummary: BranchSummaryMessage;
@@ -124,6 +142,27 @@ export function bashExecutionToText(msg: BashExecutionMessage): string {
124
142
  return text;
125
143
  }
126
144
 
145
+ /**
146
+ * Convert a PythonExecutionMessage to user message text for LLM context.
147
+ */
148
+ export function pythonExecutionToText(msg: PythonExecutionMessage): string {
149
+ let text = `Ran Python:\n\`\`\`python\n${msg.code}\n\`\`\`\n`;
150
+ if (msg.output) {
151
+ text += `Output:\n\`\`\`\n${msg.output}\n\`\`\``;
152
+ } else {
153
+ text += "(no output)";
154
+ }
155
+ if (msg.cancelled) {
156
+ text += "\n\n(execution cancelled)";
157
+ } else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) {
158
+ text += `\n\nExecution failed with code ${msg.exitCode}`;
159
+ }
160
+ if (msg.truncated && msg.fullOutputPath) {
161
+ text += `\n\n[Output truncated. Full output: ${msg.fullOutputPath}]`;
162
+ }
163
+ return text;
164
+ }
165
+
127
166
  export function createBranchSummaryMessage(summary: string, fromId: string, timestamp: string): BranchSummaryMessage {
128
167
  return {
129
168
  role: "branchSummary",
@@ -185,6 +224,15 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
185
224
  content: [{ type: "text", text: bashExecutionToText(m) }],
186
225
  timestamp: m.timestamp,
187
226
  };
227
+ case "pythonExecution":
228
+ if (m.excludeFromContext) {
229
+ return undefined;
230
+ }
231
+ return {
232
+ role: "user",
233
+ content: [{ type: "text", text: pythonExecutionToText(m) }],
234
+ timestamp: m.timestamp,
235
+ };
188
236
  case "custom":
189
237
  case "hookMessage": {
190
238
  const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
package/src/core/sdk.ts CHANGED
@@ -86,7 +86,6 @@ import {
86
86
  createTools,
87
87
  EditTool,
88
88
  FindTool,
89
- GitTool,
90
89
  GrepTool,
91
90
  getWebSearchTools,
92
91
  LsTool,
@@ -218,7 +217,6 @@ export {
218
217
  createTools,
219
218
  EditTool,
220
219
  FindTool,
221
- GitTool,
222
220
  GrepTool,
223
221
  loadSshTool,
224
222
  LsTool,
@@ -13,6 +13,7 @@ import {
13
13
  createCustomMessage,
14
14
  type FileMentionMessage,
15
15
  type HookMessage,
16
+ type PythonExecutionMessage,
16
17
  } from "./messages";
17
18
  import type { SessionStorage, SessionStorageWriter } from "./session-storage";
18
19
  import { FileSessionStorage, MemorySessionStorage } from "./session-storage";
@@ -1306,7 +1307,15 @@ export class SessionManager {
1306
1307
  * so it is easier to find them.
1307
1308
  * These need to be appended via appendCompaction() and appendBranchSummary() methods.
1308
1309
  */
1309
- appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage | FileMentionMessage): string {
1310
+ appendMessage(
1311
+ message:
1312
+ | Message
1313
+ | CustomMessage
1314
+ | HookMessage
1315
+ | BashExecutionMessage
1316
+ | PythonExecutionMessage
1317
+ | FileMentionMessage,
1318
+ ): string {
1310
1319
  const entry: SessionMessageEntry = {
1311
1320
  type: "message",
1312
1321
  id: generateId(this.byId),