@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
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Component for displaying user-initiated Python execution with streaming output.
3
+ * Shares the same kernel session as the agent's Python tool.
4
+ */
5
+
6
+ import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
7
+ import stripAnsi from "strip-ansi";
8
+ import {
9
+ DEFAULT_MAX_BYTES,
10
+ DEFAULT_MAX_LINES,
11
+ type TruncationResult,
12
+ truncateTail,
13
+ } from "../../../core/tools/truncate";
14
+ import { getSymbolTheme, highlightCode, theme } from "../theme/theme";
15
+ import { DynamicBorder } from "./dynamic-border";
16
+ import { truncateToVisualLines } from "./visual-truncate";
17
+
18
+ const PREVIEW_LINES = 20;
19
+
20
+ export class PythonExecutionComponent extends Container {
21
+ private code: string;
22
+ private outputLines: string[] = [];
23
+ private status: "running" | "complete" | "cancelled" | "error" = "running";
24
+ private exitCode: number | undefined = undefined;
25
+ private loader: Loader;
26
+ private truncationResult?: TruncationResult;
27
+ private fullOutputPath?: string;
28
+ private expanded = false;
29
+ private contentContainer: Container;
30
+ private excludeFromContext: boolean;
31
+
32
+ private formatHeader(colorKey: "dim" | "pythonMode"): Text {
33
+ const prompt = theme.fg(colorKey, theme.bold(">>>"));
34
+ const continuation = theme.fg(colorKey, " ");
35
+ const codeLines = highlightCode(this.code, "python");
36
+ const headerLines = codeLines.map((line, index) =>
37
+ index === 0 ? `${prompt} ${line}` : `${continuation}${line}`,
38
+ );
39
+ return new Text(headerLines.join("\n"), 1, 0);
40
+ }
41
+
42
+ constructor(code: string, ui: TUI, excludeFromContext = false) {
43
+ super();
44
+ this.code = code;
45
+ this.excludeFromContext = excludeFromContext;
46
+
47
+ const colorKey = this.excludeFromContext ? "dim" : "pythonMode";
48
+ const borderColor = (str: string) => theme.fg(colorKey, str);
49
+
50
+ this.addChild(new Spacer(1));
51
+ this.addChild(new DynamicBorder(borderColor));
52
+
53
+ this.contentContainer = new Container();
54
+ this.addChild(this.contentContainer);
55
+ this.contentContainer.addChild(this.formatHeader(colorKey));
56
+
57
+ this.loader = new Loader(
58
+ ui,
59
+ (spinner) => theme.fg(colorKey, spinner),
60
+ (text) => theme.fg("muted", text),
61
+ `Running${theme.format.ellipsis} (esc to cancel)`,
62
+ getSymbolTheme().spinnerFrames,
63
+ );
64
+ this.contentContainer.addChild(this.loader);
65
+
66
+ this.addChild(new DynamicBorder(borderColor));
67
+ }
68
+
69
+ setExpanded(expanded: boolean): void {
70
+ this.expanded = expanded;
71
+ this.updateDisplay();
72
+ }
73
+
74
+ override invalidate(): void {
75
+ super.invalidate();
76
+ this.updateDisplay();
77
+ }
78
+
79
+ appendOutput(chunk: string): void {
80
+ const clean = stripAnsi(chunk).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
81
+
82
+ const newLines = clean.split("\n");
83
+ if (this.outputLines.length > 0 && newLines.length > 0) {
84
+ this.outputLines[this.outputLines.length - 1] += newLines[0];
85
+ this.outputLines.push(...newLines.slice(1));
86
+ } else {
87
+ this.outputLines.push(...newLines);
88
+ }
89
+
90
+ this.updateDisplay();
91
+ }
92
+
93
+ setComplete(
94
+ exitCode: number | undefined,
95
+ cancelled: boolean,
96
+ truncationResult?: TruncationResult,
97
+ fullOutputPath?: string,
98
+ ): void {
99
+ this.exitCode = exitCode;
100
+ this.status = cancelled
101
+ ? "cancelled"
102
+ : exitCode !== 0 && exitCode !== undefined && exitCode !== null
103
+ ? "error"
104
+ : "complete";
105
+ this.truncationResult = truncationResult;
106
+ this.fullOutputPath = fullOutputPath;
107
+
108
+ this.loader.stop();
109
+ this.updateDisplay();
110
+ }
111
+
112
+ private updateDisplay(): void {
113
+ const fullOutput = this.outputLines.join("\n");
114
+ const contextTruncation = truncateTail(fullOutput, {
115
+ maxLines: DEFAULT_MAX_LINES,
116
+ maxBytes: DEFAULT_MAX_BYTES,
117
+ });
118
+
119
+ const availableLines = contextTruncation.content ? contextTruncation.content.split("\n") : [];
120
+ const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
121
+ const hiddenLineCount = availableLines.length - previewLogicalLines.length;
122
+
123
+ this.contentContainer.clear();
124
+
125
+ const colorKey = this.excludeFromContext ? "dim" : "pythonMode";
126
+ this.contentContainer.addChild(this.formatHeader(colorKey));
127
+
128
+ if (availableLines.length > 0) {
129
+ if (this.expanded) {
130
+ const displayText = availableLines.map((line) => theme.fg("muted", line)).join("\n");
131
+ this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
132
+ } else {
133
+ const styledOutput = previewLogicalLines.map((line) => theme.fg("muted", line)).join("\n");
134
+ const previewText = `\n${styledOutput}`;
135
+ this.contentContainer.addChild({
136
+ render: (width: number) => {
137
+ const { visualLines } = truncateToVisualLines(previewText, PREVIEW_LINES, width, 1);
138
+ return visualLines;
139
+ },
140
+ invalidate: () => {},
141
+ });
142
+ }
143
+ }
144
+
145
+ if (this.status === "running") {
146
+ this.contentContainer.addChild(this.loader);
147
+ } else {
148
+ const statusParts: string[] = [];
149
+
150
+ if (hiddenLineCount > 0) {
151
+ statusParts.push(
152
+ theme.fg("dim", `${theme.format.ellipsis} ${hiddenLineCount} more lines (ctrl+o to expand)`),
153
+ );
154
+ }
155
+
156
+ if (this.status === "cancelled") {
157
+ statusParts.push(theme.fg("warning", "(cancelled)"));
158
+ } else if (this.status === "error") {
159
+ statusParts.push(theme.fg("error", `(exit ${this.exitCode})`));
160
+ }
161
+
162
+ const wasTruncated = this.truncationResult?.truncated || contextTruncation.truncated;
163
+ if (wasTruncated && this.fullOutputPath) {
164
+ statusParts.push(theme.fg("warning", `Output truncated. Full output: ${this.fullOutputPath}`));
165
+ }
166
+
167
+ if (statusParts.length > 0) {
168
+ this.contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, 1, 0));
169
+ }
170
+ }
171
+ }
172
+
173
+ getOutput(): string {
174
+ return this.outputLines.join("\n");
175
+ }
176
+
177
+ getCode(): string {
178
+ return this.code;
179
+ }
180
+ }
@@ -83,7 +83,6 @@ const THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {
83
83
  * - behavior: Core agent behavior (compaction, modes, retries, notifications)
84
84
  * - tools: Tool-specific settings (bash, git, python, edit, MCP, skills)
85
85
  * - display: Visual/UI settings (theme, images, thinking)
86
- * - voice: Voice mode and TTS settings
87
86
  * - ttsr: Time Traveling Stream Rules settings
88
87
  * - status: Status line configuration
89
88
  * - lsp: LSP integration settings
@@ -250,15 +249,6 @@ export const SETTINGS_DEFS: SettingDef[] = [
250
249
  get: (sm) => sm.getBashInterceptorSimpleLsEnabled(),
251
250
  set: (sm, v) => sm.setBashInterceptorSimpleLsEnabled(v),
252
251
  },
253
- {
254
- id: "gitTool",
255
- tab: "tools",
256
- type: "boolean",
257
- label: "Git tool",
258
- description: "Enable structured Git tool",
259
- get: (sm) => sm.getGitToolEnabled(),
260
- set: (sm, v) => sm.setGitToolEnabled(v),
261
- },
262
252
  {
263
253
  id: "pythonToolMode",
264
254
  tab: "tools",
@@ -494,66 +484,6 @@ export const SETTINGS_DEFS: SettingDef[] = [
494
484
  set: (sm, v) => sm.setShowHardwareCursor(v),
495
485
  },
496
486
 
497
- // ═══════════════════════════════════════════════════════════════════════════
498
- // Voice tab - Voice mode and TTS settings
499
- // ═══════════════════════════════════════════════════════════════════════════
500
- {
501
- id: "voiceEnabled",
502
- tab: "voice",
503
- type: "boolean",
504
- label: "Voice mode",
505
- description: "Enable realtime voice input/output (Ctrl+Y toggle, auto-send on silence)",
506
- get: (sm) => sm.getVoiceEnabled(),
507
- set: (sm, v) => sm.setVoiceEnabled(v),
508
- },
509
- {
510
- id: "voiceTtsModel",
511
- tab: "voice",
512
- type: "submenu",
513
- label: "TTS model",
514
- description: "Text-to-speech model for voice output",
515
- get: (sm) => sm.getVoiceTtsModel(),
516
- set: (sm, v) => sm.setVoiceTtsModel(v),
517
- getOptions: () => [
518
- { value: "gpt-4o-mini-tts", label: "GPT-4o Mini TTS", description: "Fast and efficient" },
519
- { value: "tts-1", label: "TTS-1", description: "Standard quality" },
520
- { value: "tts-1-hd", label: "TTS-1 HD", description: "Higher quality" },
521
- ],
522
- },
523
- {
524
- id: "voiceTtsVoice",
525
- tab: "voice",
526
- type: "submenu",
527
- label: "TTS voice",
528
- description: "Voice for text-to-speech output",
529
- get: (sm) => sm.getVoiceTtsVoice(),
530
- set: (sm, v) => sm.setVoiceTtsVoice(v),
531
- getOptions: () => [
532
- { value: "alloy", label: "Alloy", description: "Neutral" },
533
- { value: "echo", label: "Echo", description: "Male" },
534
- { value: "fable", label: "Fable", description: "British" },
535
- { value: "onyx", label: "Onyx", description: "Deep male" },
536
- { value: "nova", label: "Nova", description: "Female" },
537
- { value: "shimmer", label: "Shimmer", description: "Female" },
538
- ],
539
- },
540
- {
541
- id: "voiceTtsFormat",
542
- tab: "voice",
543
- type: "submenu",
544
- label: "TTS format",
545
- description: "Audio format for voice output",
546
- get: (sm) => sm.getVoiceTtsFormat(),
547
- set: (sm, v) => sm.setVoiceTtsFormat(v as "wav" | "mp3" | "opus" | "aac" | "flac"),
548
- getOptions: () => [
549
- { value: "wav", label: "WAV", description: "Uncompressed, best quality" },
550
- { value: "mp3", label: "MP3", description: "Compressed, widely compatible" },
551
- { value: "opus", label: "Opus", description: "Efficient compression" },
552
- { value: "aac", label: "AAC", description: "Apple-friendly" },
553
- { value: "flac", label: "FLAC", description: "Lossless compression" },
554
- ],
555
- },
556
-
557
487
  // ═══════════════════════════════════════════════════════════════════════════
558
488
  // TTSR tab - Time Traveling Stream Rules
559
489
  // ═══════════════════════════════════════════════════════════════════════════
@@ -117,7 +117,6 @@ const SETTINGS_TABS: Tab[] = [
117
117
  { id: "behavior", label: "Behavior" },
118
118
  { id: "tools", label: "Tools" },
119
119
  { id: "display", label: "Display" },
120
- { id: "voice", label: "Voice" },
121
120
  { id: "ttsr", label: "TTSR" },
122
121
  { id: "status", label: "Status" },
123
122
  { id: "lsp", label: "LSP" },
@@ -117,6 +117,7 @@ export class WelcomeComponent implements Component {
117
117
  ` ${theme.fg("dim", "?")}${theme.fg("muted", " for keyboard shortcuts")}`,
118
118
  ` ${theme.fg("dim", "/")}${theme.fg("muted", " for commands")}`,
119
119
  ` ${theme.fg("dim", "!")}${theme.fg("muted", " to run bash")}`,
120
+ ` ${theme.fg("dim", "$")}${theme.fg("muted", " to run python")}`,
120
121
  separator,
121
122
  ` ${theme.bold(theme.fg("accent", "LSP Servers"))}`,
122
123
  ...lspLines,
@@ -16,6 +16,7 @@ import { ArminComponent } from "../components/armin";
16
16
  import { BashExecutionComponent } from "../components/bash-execution";
17
17
  import { BorderedLoader } from "../components/bordered-loader";
18
18
  import { DynamicBorder } from "../components/dynamic-border";
19
+ import { PythonExecutionComponent } from "../components/python-execution";
19
20
  import { getMarkdownTheme, getSymbolTheme, theme } from "../theme/theme";
20
21
  import type { InteractiveModeContext } from "../types";
21
22
 
@@ -344,6 +345,8 @@ export class CommandController {
344
345
  | \`/\` | Slash commands |
345
346
  | \`!\` | Run bash command |
346
347
  | \`!!\` | Run bash command (excluded from context) |
348
+ | \`$\` | Run Python in shared kernel |
349
+ | \`$$\` | Run Python (excluded from context) |
347
350
  `;
348
351
  this.ctx.chatContainer.addChild(new Spacer(1));
349
352
  this.ctx.chatContainer.addChild(new DynamicBorder());
@@ -471,6 +474,49 @@ export class CommandController {
471
474
  this.ctx.ui.requestRender();
472
475
  }
473
476
 
477
+ async handlePythonCommand(code: string, excludeFromContext = false): Promise<void> {
478
+ const isDeferred = this.ctx.session.isStreaming;
479
+ this.ctx.pythonComponent = new PythonExecutionComponent(code, this.ctx.ui, excludeFromContext);
480
+
481
+ if (isDeferred) {
482
+ this.ctx.pendingMessagesContainer.addChild(this.ctx.pythonComponent);
483
+ this.ctx.pendingPythonComponents.push(this.ctx.pythonComponent);
484
+ } else {
485
+ this.ctx.chatContainer.addChild(this.ctx.pythonComponent);
486
+ }
487
+ this.ctx.ui.requestRender();
488
+
489
+ try {
490
+ const result = await this.ctx.session.executePython(
491
+ code,
492
+ (chunk) => {
493
+ if (this.ctx.pythonComponent) {
494
+ this.ctx.pythonComponent.appendOutput(chunk);
495
+ this.ctx.ui.requestRender();
496
+ }
497
+ },
498
+ { excludeFromContext },
499
+ );
500
+
501
+ if (this.ctx.pythonComponent) {
502
+ this.ctx.pythonComponent.setComplete(
503
+ result.exitCode,
504
+ result.cancelled,
505
+ result.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,
506
+ result.fullOutputPath,
507
+ );
508
+ }
509
+ } catch (error) {
510
+ if (this.ctx.pythonComponent) {
511
+ this.ctx.pythonComponent.setComplete(undefined, false);
512
+ }
513
+ this.ctx.showError(`Python execution failed: ${error instanceof Error ? error.message : "Unknown error"}`);
514
+ }
515
+
516
+ this.ctx.pythonComponent = undefined;
517
+ this.ctx.ui.requestRender();
518
+ }
519
+
474
520
  async handleCompactCommand(customInstructions?: string): Promise<void> {
475
521
  const entries = this.ctx.sessionManager.getEntries();
476
522
  const messageCount = entries.filter((e) => e.type === "message").length;
@@ -68,7 +68,6 @@ export class EventController {
68
68
  getSymbolTheme().spinnerFrames,
69
69
  );
70
70
  this.ctx.statusContainer.addChild(this.ctx.loadingAnimation);
71
- this.ctx.startVoiceProgressTimer();
72
71
  this.ctx.ui.requestRender();
73
72
  break;
74
73
 
@@ -250,7 +249,6 @@ export class EventController {
250
249
  }
251
250
 
252
251
  case "agent_end":
253
- this.ctx.stopVoiceProgressTimer();
254
252
  if (this.ctx.loadingAnimation) {
255
253
  this.ctx.loadingAnimation.stop();
256
254
  this.ctx.loadingAnimation = undefined;
@@ -262,15 +260,6 @@ export class EventController {
262
260
  this.ctx.streamingMessage = undefined;
263
261
  }
264
262
  this.ctx.pendingTools.clear();
265
- if (this.ctx.settingsManager.getVoiceEnabled() && this.ctx.voiceAutoModeEnabled) {
266
- const lastAssistant = this.ctx.findLastAssistantMessage();
267
- if (lastAssistant && lastAssistant.stopReason !== "aborted" && lastAssistant.stopReason !== "error") {
268
- const text = this.ctx.extractAssistantText(lastAssistant);
269
- if (text) {
270
- this.ctx.voiceSupervisor.notifyResult(text);
271
- }
272
- }
273
- }
274
263
  this.ctx.ui.requestRender();
275
264
  this.sendCompletionNotification();
276
265
  break;
@@ -31,6 +31,12 @@ export class InputController {
31
31
  this.ctx.editor.setText("");
32
32
  this.ctx.isBashMode = false;
33
33
  this.ctx.updateEditorBorderColor();
34
+ } else if (this.ctx.isPythonMode) {
35
+ this.ctx.editor.setText("");
36
+ this.ctx.isPythonMode = false;
37
+ this.ctx.updateEditorBorderColor();
38
+ } else if (this.ctx.session.isPythonRunning) {
39
+ this.ctx.session.abortPython();
34
40
  } else if (!this.ctx.editor.getText().trim()) {
35
41
  // Double-escape with empty editor triggers /tree or /branch based on setting
36
42
  const now = Date.now();
@@ -83,8 +89,11 @@ export class InputController {
83
89
 
84
90
  this.ctx.editor.onChange = (text: string) => {
85
91
  const wasBashMode = this.ctx.isBashMode;
92
+ const wasPythonMode = this.ctx.isPythonMode;
93
+ const trimmed = text.trimStart();
86
94
  this.ctx.isBashMode = text.trimStart().startsWith("!");
87
- if (wasBashMode !== this.ctx.isBashMode) {
95
+ this.ctx.isPythonMode = trimmed.startsWith("$") && !trimmed.startsWith("${");
96
+ if (wasBashMode !== this.ctx.isBashMode || wasPythonMode !== this.ctx.isPythonMode) {
88
97
  this.ctx.updateEditorBorderColor();
89
98
  }
90
99
  };
@@ -309,6 +318,24 @@ export class InputController {
309
318
  }
310
319
  }
311
320
 
321
+ // Handle python command ($ for normal, $$ for excluded from context)
322
+ if (text.startsWith("$")) {
323
+ const isExcluded = text.startsWith("$$");
324
+ const code = isExcluded ? text.slice(2).trim() : text.slice(1).trim();
325
+ if (code) {
326
+ if (this.ctx.session.isPythonRunning) {
327
+ this.ctx.showWarning("A Python execution is already running. Press Esc to cancel it first.");
328
+ this.ctx.editor.setText(text);
329
+ return;
330
+ }
331
+ this.ctx.editor.addToHistory(text);
332
+ await this.ctx.handlePythonCommand(code, isExcluded);
333
+ this.ctx.isPythonMode = false;
334
+ this.ctx.updateEditorBorderColor();
335
+ return;
336
+ }
337
+ }
338
+
312
339
  // Queue input during compaction
313
340
  if (this.ctx.session.isCompacting) {
314
341
  if (this.ctx.pendingImages.length > 0) {
@@ -199,15 +199,6 @@ export class SelectorController {
199
199
  this.ctx.ui.invalidate();
200
200
  break;
201
201
  }
202
- case "voiceEnabled": {
203
- if (!value) {
204
- this.ctx.voiceAutoModeEnabled = false;
205
- this.ctx.stopVoiceProgressTimer();
206
- void this.ctx.voiceSupervisor.stop();
207
- this.ctx.setVoiceStatus(undefined);
208
- }
209
- break;
210
- }
211
202
  case "statusLinePreset":
212
203
  case "statusLineSeparator":
213
204
  case "statusLineShowHooks":
@@ -28,7 +28,6 @@ import { getRecentSessions } from "../../core/session-manager";
28
28
  import type { SettingsManager } from "../../core/settings-manager";
29
29
  import { loadSlashCommands } from "../../core/slash-commands";
30
30
  import { setTerminalTitle } from "../../core/title-generator";
31
- import { VoiceSupervisor } from "../../core/voice-supervisor";
32
31
  import type { AssistantMessageComponent } from "./components/assistant-message";
33
32
  import type { BashExecutionComponent } from "./components/bash-execution";
34
33
  import { CustomEditor } from "./components/custom-editor";
@@ -36,6 +35,7 @@ import { DynamicBorder } from "./components/dynamic-border";
36
35
  import type { HookEditorComponent } from "./components/hook-editor";
37
36
  import type { HookInputComponent } from "./components/hook-input";
38
37
  import type { HookSelectorComponent } from "./components/hook-selector";
38
+ import type { PythonExecutionComponent } from "./components/python-execution";
39
39
  import { StatusLineComponent } from "./components/status-line";
40
40
  import type { ToolExecutionHandle } from "./components/tool-execution";
41
41
  import { WelcomeComponent } from "./components/welcome";
@@ -48,7 +48,6 @@ import type { Theme } from "./theme/theme";
48
48
  import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/theme";
49
49
  import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem } from "./types";
50
50
  import { UiHelpers } from "./utils/ui-helpers";
51
- import { VoiceManager } from "./utils/voice-manager";
52
51
 
53
52
  const TODO_FILE_NAME = "todos.json";
54
53
 
@@ -72,7 +71,6 @@ export class InteractiveMode implements InteractiveModeContext {
72
71
  public settingsManager: SettingsManager;
73
72
  public keybindings: KeybindingsManager;
74
73
  public agent: AgentSession["agent"];
75
- public voiceSupervisor: VoiceSupervisor;
76
74
  public historyStorage?: HistoryStorage;
77
75
 
78
76
  public ui: TUI;
@@ -96,6 +94,9 @@ export class InteractiveMode implements InteractiveModeContext {
96
94
  public pendingTools = new Map<string, ToolExecutionHandle>();
97
95
  public pendingBashComponents: BashExecutionComponent[] = [];
98
96
  public bashComponent: BashExecutionComponent | undefined = undefined;
97
+ public pendingPythonComponents: PythonExecutionComponent[] = [];
98
+ public pythonComponent: PythonExecutionComponent | undefined = undefined;
99
+ public isPythonMode = false;
99
100
  public streamingComponent: AssistantMessageComponent | undefined = undefined;
100
101
  public streamingMessage: AssistantMessage | undefined = undefined;
101
102
  public loadingAnimation: Loader | undefined = undefined;
@@ -107,13 +108,8 @@ export class InteractiveMode implements InteractiveModeContext {
107
108
  public onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
108
109
  public lastSigintTime = 0;
109
110
  public lastEscapeTime = 0;
110
- public lastVoiceInterruptAt = 0;
111
- public voiceAutoModeEnabled = false;
112
111
  public shutdownRequested = false;
113
112
  private isShuttingDown = false;
114
- public voiceProgressTimer: ReturnType<typeof setTimeout> | undefined = undefined;
115
- public voiceProgressSpoken = false;
116
- public voiceProgressLastLength = 0;
117
113
  public hookSelector: HookSelectorComponent | undefined = undefined;
118
114
  public hookInput: HookInputComponent | undefined = undefined;
119
115
  public hookEditor: HookEditorComponent | undefined = undefined;
@@ -137,7 +133,6 @@ export class InteractiveMode implements InteractiveModeContext {
137
133
  private readonly inputController: InputController;
138
134
  private readonly selectorController: SelectorController;
139
135
  private readonly uiHelpers: UiHelpers;
140
- private readonly voiceManager: VoiceManager;
141
136
 
142
137
  constructor(
143
138
  session: AgentSession,
@@ -180,26 +175,6 @@ export class InteractiveMode implements InteractiveModeContext {
180
175
  this.editorContainer.addChild(this.editor);
181
176
  this.statusLine = new StatusLineComponent(session);
182
177
  this.statusLine.setAutoCompactEnabled(session.autoCompactionEnabled);
183
- this.voiceSupervisor = new VoiceSupervisor(this.session.modelRegistry, {
184
- onSendToAgent: async (text) => {
185
- await this.submitVoiceText(text);
186
- },
187
- onInterruptAgent: async (reason) => {
188
- await this.handleVoiceInterrupt(reason);
189
- },
190
- onStatus: (status) => {
191
- this.setVoiceStatus(status);
192
- },
193
- onError: (error) => {
194
- this.showError(error.message);
195
- this.voiceAutoModeEnabled = false;
196
- void this.voiceSupervisor.stop();
197
- this.setVoiceStatus(undefined);
198
- },
199
- onWarning: (message) => {
200
- this.showWarning(message);
201
- },
202
- });
203
178
 
204
179
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
205
180
 
@@ -255,7 +230,6 @@ export class InteractiveMode implements InteractiveModeContext {
255
230
  this.pendingSlashCommands = [...slashCommands, ...hookCommands, ...customCommands, ...skillCommandList];
256
231
 
257
232
  this.uiHelpers = new UiHelpers(this);
258
- this.voiceManager = new VoiceManager(this);
259
233
  this.extensionUiController = new ExtensionUiController(this);
260
234
  this.eventController = new EventController(this);
261
235
  this.commandController = new CommandController(this);
@@ -397,6 +371,8 @@ export class InteractiveMode implements InteractiveModeContext {
397
371
  updateEditorBorderColor(): void {
398
372
  if (this.isBashMode) {
399
373
  this.editor.borderColor = theme.getBashModeBorderColor();
374
+ } else if (this.isPythonMode) {
375
+ this.editor.borderColor = theme.getPythonModeBorderColor();
400
376
  } else {
401
377
  const level = this.session.thinkingLevel || "off";
402
378
  this.editor.borderColor = theme.getThinkingBorderColor(level);
@@ -511,9 +487,6 @@ export class InteractiveMode implements InteractiveModeContext {
511
487
  if (this.isShuttingDown) return;
512
488
  this.isShuttingDown = true;
513
489
 
514
- this.voiceAutoModeEnabled = false;
515
- await this.voiceSupervisor.stop();
516
-
517
490
  // Flush pending session writes before shutdown
518
491
  await this.sessionManager.flush();
519
492
 
@@ -664,6 +637,10 @@ export class InteractiveMode implements InteractiveModeContext {
664
637
  return this.commandController.handleBashCommand(command, excludeFromContext);
665
638
  }
666
639
 
640
+ handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void> {
641
+ return this.commandController.handlePythonCommand(code, excludeFromContext);
642
+ }
643
+
667
644
  handleCompactCommand(customInstructions?: string): Promise<void> {
668
645
  return this.commandController.handleCompactCommand(customInstructions);
669
646
  }
@@ -783,31 +760,6 @@ export class InteractiveMode implements InteractiveModeContext {
783
760
  this.inputController.registerExtensionShortcuts();
784
761
  }
785
762
 
786
- // Voice handling
787
- setVoiceStatus(text: string | undefined): void {
788
- this.voiceManager.setVoiceStatus(text);
789
- }
790
-
791
- handleVoiceInterrupt(reason?: string): Promise<void> {
792
- return this.voiceManager.handleVoiceInterrupt(reason);
793
- }
794
-
795
- startVoiceProgressTimer(): void {
796
- this.voiceManager.startVoiceProgressTimer();
797
- }
798
-
799
- stopVoiceProgressTimer(): void {
800
- this.voiceManager.stopVoiceProgressTimer();
801
- }
802
-
803
- maybeSpeakProgress(): Promise<void> {
804
- return this.voiceManager.maybeSpeakProgress();
805
- }
806
-
807
- submitVoiceText(text: string): Promise<void> {
808
- return this.voiceManager.submitVoiceText(text);
809
- }
810
-
811
763
  // Hook UI methods
812
764
  initHooksAndCustomTools(): Promise<void> {
813
765
  return this.extensionUiController.initHooksAndCustomTools();
@@ -30,7 +30,6 @@
30
30
  "dim": "dimGray",
31
31
  "text": "",
32
32
  "thinkingText": "gray",
33
-
34
33
  "selectedBg": "selectedBg",
35
34
  "userMessageBg": "userMsgBg",
36
35
  "userMessageText": "",
@@ -42,7 +41,6 @@
42
41
  "toolErrorBg": "toolErrorBg",
43
42
  "toolTitle": "",
44
43
  "toolOutput": "gray",
45
-
46
44
  "mdHeading": "#febc38",
47
45
  "mdLink": "#0088fa",
48
46
  "mdLinkUrl": "dimGray",
@@ -53,13 +51,10 @@
53
51
  "mdQuoteBorder": "darkGray",
54
52
  "mdHr": "darkGray",
55
53
  "mdListBullet": "accent",
56
-
57
54
  "toolDiffAdded": "green",
58
55
  "toolDiffRemoved": "red",
59
56
  "toolDiffContext": "gray",
60
-
61
57
  "link": "#0088fa",
62
-
63
58
  "syntaxComment": "#6A9955",
64
59
  "syntaxKeyword": "#569CD6",
65
60
  "syntaxFunction": "#DCDCAA",
@@ -69,16 +64,13 @@
69
64
  "syntaxType": "#4EC9B0",
70
65
  "syntaxOperator": "#D4D4D4",
71
66
  "syntaxPunctuation": "#D4D4D4",
72
-
73
67
  "thinkingOff": "darkGray",
74
68
  "thinkingMinimal": "dimGray",
75
69
  "thinkingLow": "#178fb9",
76
70
  "thinkingMedium": "#0088fa",
77
71
  "thinkingHigh": "#b281d6",
78
72
  "thinkingXhigh": "#e5c1ff",
79
-
80
73
  "bashMode": "cyan",
81
-
82
74
  "statusLineBg": "#121212",
83
75
  "statusLineSep": 244,
84
76
  "statusLineModel": "#d787af",
@@ -92,7 +84,8 @@
92
84
  "statusLineUntracked": 39,
93
85
  "statusLineOutput": 205,
94
86
  "statusLineCost": 205,
95
- "statusLineSubagents": "accent"
87
+ "statusLineSubagents": "accent",
88
+ "pythonMode": "yellow"
96
89
  },
97
90
  "export": {
98
91
  "pageBg": "#18181e",