@oh-my-pi/pi-coding-agent 6.8.4 → 6.9.0

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.
@@ -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";
@@ -48,7 +47,6 @@ import type { Theme } from "./theme/theme";
48
47
  import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/theme";
49
48
  import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem } from "./types";
50
49
  import { UiHelpers } from "./utils/ui-helpers";
51
- import { VoiceManager } from "./utils/voice-manager";
52
50
 
53
51
  const TODO_FILE_NAME = "todos.json";
54
52
 
@@ -72,7 +70,6 @@ export class InteractiveMode implements InteractiveModeContext {
72
70
  public settingsManager: SettingsManager;
73
71
  public keybindings: KeybindingsManager;
74
72
  public agent: AgentSession["agent"];
75
- public voiceSupervisor: VoiceSupervisor;
76
73
  public historyStorage?: HistoryStorage;
77
74
 
78
75
  public ui: TUI;
@@ -107,13 +104,8 @@ export class InteractiveMode implements InteractiveModeContext {
107
104
  public onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
108
105
  public lastSigintTime = 0;
109
106
  public lastEscapeTime = 0;
110
- public lastVoiceInterruptAt = 0;
111
- public voiceAutoModeEnabled = false;
112
107
  public shutdownRequested = false;
113
108
  private isShuttingDown = false;
114
- public voiceProgressTimer: ReturnType<typeof setTimeout> | undefined = undefined;
115
- public voiceProgressSpoken = false;
116
- public voiceProgressLastLength = 0;
117
109
  public hookSelector: HookSelectorComponent | undefined = undefined;
118
110
  public hookInput: HookInputComponent | undefined = undefined;
119
111
  public hookEditor: HookEditorComponent | undefined = undefined;
@@ -137,7 +129,6 @@ export class InteractiveMode implements InteractiveModeContext {
137
129
  private readonly inputController: InputController;
138
130
  private readonly selectorController: SelectorController;
139
131
  private readonly uiHelpers: UiHelpers;
140
- private readonly voiceManager: VoiceManager;
141
132
 
142
133
  constructor(
143
134
  session: AgentSession,
@@ -180,26 +171,6 @@ export class InteractiveMode implements InteractiveModeContext {
180
171
  this.editorContainer.addChild(this.editor);
181
172
  this.statusLine = new StatusLineComponent(session);
182
173
  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
174
 
204
175
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
205
176
 
@@ -255,7 +226,6 @@ export class InteractiveMode implements InteractiveModeContext {
255
226
  this.pendingSlashCommands = [...slashCommands, ...hookCommands, ...customCommands, ...skillCommandList];
256
227
 
257
228
  this.uiHelpers = new UiHelpers(this);
258
- this.voiceManager = new VoiceManager(this);
259
229
  this.extensionUiController = new ExtensionUiController(this);
260
230
  this.eventController = new EventController(this);
261
231
  this.commandController = new CommandController(this);
@@ -511,9 +481,6 @@ export class InteractiveMode implements InteractiveModeContext {
511
481
  if (this.isShuttingDown) return;
512
482
  this.isShuttingDown = true;
513
483
 
514
- this.voiceAutoModeEnabled = false;
515
- await this.voiceSupervisor.stop();
516
-
517
484
  // Flush pending session writes before shutdown
518
485
  await this.sessionManager.flush();
519
486
 
@@ -783,31 +750,6 @@ export class InteractiveMode implements InteractiveModeContext {
783
750
  this.inputController.registerExtensionShortcuts();
784
751
  }
785
752
 
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
753
  // Hook UI methods
812
754
  initHooksAndCustomTools(): Promise<void> {
813
755
  return this.extensionUiController.initHooksAndCustomTools();
@@ -9,7 +9,6 @@ import type { KeybindingsManager } from "../../core/keybindings";
9
9
  import type { MCPManager } from "../../core/mcp/index";
10
10
  import type { SessionContext, SessionManager } from "../../core/session-manager";
11
11
  import type { SettingsManager } from "../../core/settings-manager";
12
- import type { VoiceSupervisor } from "../../core/voice-supervisor";
13
12
  import type { AssistantMessageComponent } from "./components/assistant-message";
14
13
  import type { BashExecutionComponent } from "./components/bash-execution";
15
14
  import type { CustomEditor } from "./components/custom-editor";
@@ -49,7 +48,6 @@ export interface InteractiveModeContext {
49
48
  settingsManager: SettingsManager;
50
49
  keybindings: KeybindingsManager;
51
50
  agent: AgentSession["agent"];
52
- voiceSupervisor: VoiceSupervisor;
53
51
  historyStorage?: HistoryStorage;
54
52
  mcpManager?: MCPManager;
55
53
  lspServers?: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }>;
@@ -77,12 +75,7 @@ export interface InteractiveModeContext {
77
75
  onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
78
76
  lastSigintTime: number;
79
77
  lastEscapeTime: number;
80
- lastVoiceInterruptAt: number;
81
- voiceAutoModeEnabled: boolean;
82
78
  shutdownRequested: boolean;
83
- voiceProgressTimer: ReturnType<typeof setTimeout> | undefined;
84
- voiceProgressSpoken: boolean;
85
- voiceProgressLastLength: number;
86
79
  hookSelector: HookSelectorComponent | undefined;
87
80
  hookInput: HookInputComponent | undefined;
88
81
  hookEditor: HookEditorComponent | undefined;
@@ -174,14 +167,6 @@ export interface InteractiveModeContext {
174
167
  openExternalEditor(): void;
175
168
  registerExtensionShortcuts(): void;
176
169
 
177
- // Voice handling
178
- setVoiceStatus(text: string | undefined): void;
179
- handleVoiceInterrupt(reason?: string): Promise<void>;
180
- startVoiceProgressTimer(): void;
181
- stopVoiceProgressTimer(): void;
182
- maybeSpeakProgress(): Promise<void>;
183
- submitVoiceText(text: string): Promise<void>;
184
-
185
170
  // Hook UI methods
186
171
  initHooksAndCustomTools(): Promise<void>;
187
172
  emitCustomToolSessionEvent(
@@ -1,435 +0,0 @@
1
- import * as path from "node:path";
2
- import { nanoid } from "nanoid";
3
- import { type CollapseStrategy, collapse } from "../../../../lib/worktree/collapse";
4
- import { WorktreeError, WorktreeErrorCode } from "../../../../lib/worktree/errors";
5
- import { getRepoRoot, git } from "../../../../lib/worktree/git";
6
- import * as worktree from "../../../../lib/worktree/index";
7
- import { createSession, updateSession } from "../../../../lib/worktree/session";
8
- import { formatStats, getStats } from "../../../../lib/worktree/stats";
9
- import type { HookCommandContext } from "../../../hooks/types";
10
- import { discoverAgents, getAgent } from "../../../tools/task/discovery";
11
- import { runSubprocess } from "../../../tools/task/executor";
12
- import { generateTaskName } from "../../../tools/task/name-generator";
13
- import type { AgentDefinition } from "../../../tools/task/types";
14
- import type { CustomCommand, CustomCommandAPI } from "../../types";
15
-
16
- interface FlagParseResult {
17
- positionals: string[];
18
- flags: Map<string, string | boolean>;
19
- }
20
-
21
- interface NewArgs {
22
- branch: string;
23
- base?: string;
24
- }
25
-
26
- interface MergeArgs {
27
- source: string;
28
- target?: string;
29
- strategy?: CollapseStrategy;
30
- keep?: boolean;
31
- }
32
-
33
- interface RmArgs {
34
- name: string;
35
- force?: boolean;
36
- }
37
-
38
- interface SpawnArgs {
39
- task: string;
40
- scope?: string;
41
- name?: string;
42
- }
43
-
44
- interface ParallelTask {
45
- task: string;
46
- scope: string;
47
- }
48
-
49
- function parseFlags(args: string[]): FlagParseResult {
50
- const flags = new Map<string, string | boolean>();
51
- const positionals: string[] = [];
52
-
53
- for (let i = 0; i < args.length; i++) {
54
- const arg = args[i];
55
- if (arg.startsWith("--")) {
56
- const name = arg.slice(2);
57
- const next = args[i + 1];
58
- if (next && !next.startsWith("--")) {
59
- flags.set(name, next);
60
- i += 1;
61
- } else {
62
- flags.set(name, true);
63
- }
64
- } else {
65
- positionals.push(arg);
66
- }
67
- }
68
-
69
- return { positionals, flags };
70
- }
71
-
72
- function getFlagValue(flags: Map<string, string | boolean>, name: string): string | undefined {
73
- const value = flags.get(name);
74
- if (typeof value === "string") return value;
75
- return undefined;
76
- }
77
-
78
- function getFlagBoolean(flags: Map<string, string | boolean>, name: string): boolean {
79
- return flags.get(name) === true;
80
- }
81
-
82
- function formatUsage(): string {
83
- return [
84
- "Usage:",
85
- " /wt new <branch> [--base <ref>]",
86
- " /wt list",
87
- " /wt merge <src> [dst] [--strategy simple|merge-base|rebase] [--keep]",
88
- " /wt rm <name> [--force]",
89
- " /wt status",
90
- ' /wt spawn "<task>" [--scope <glob>] [--name <branch>]',
91
- " /wt parallel --task <t> --scope <s> [--task <t> --scope <s>]...",
92
- ].join("\n");
93
- }
94
-
95
- function formatError(err: unknown): string {
96
- if (err instanceof WorktreeError) {
97
- return `${err.code}: ${err.message}`;
98
- }
99
- if (err instanceof Error) return err.message;
100
- return String(err);
101
- }
102
-
103
- async function pickAgent(cwd: string): Promise<AgentDefinition> {
104
- const { agents } = await discoverAgents(cwd);
105
- // Use the bundled "task" agent as the general-purpose default.
106
- const agent = getAgent(agents, "task") ?? agents[0];
107
- if (!agent) {
108
- throw new Error("No agents available");
109
- }
110
- return agent;
111
- }
112
-
113
- function parseParallelTasks(args: string[]): ParallelTask[] {
114
- const tasks: ParallelTask[] = [];
115
- let current: Partial<ParallelTask> = {};
116
-
117
- for (let i = 0; i < args.length; i++) {
118
- const arg = args[i];
119
- if (arg === "--task") {
120
- const value = args[i + 1];
121
- if (!value || value.startsWith("--")) {
122
- throw new Error("Missing value for --task");
123
- }
124
- current.task = value;
125
- i += 1;
126
- } else if (arg === "--scope") {
127
- const value = args[i + 1];
128
- if (!value || value.startsWith("--")) {
129
- throw new Error("Missing value for --scope");
130
- }
131
- current.scope = value;
132
- i += 1;
133
- } else {
134
- throw new Error(`Unknown argument: ${arg}`);
135
- }
136
-
137
- if (current.task && current.scope) {
138
- tasks.push({ task: current.task, scope: current.scope });
139
- current = {};
140
- }
141
- }
142
-
143
- if (current.task || current.scope) {
144
- throw new Error("Each --task must be paired with a --scope");
145
- }
146
-
147
- return tasks;
148
- }
149
-
150
- function validateDisjointScopes(scopes: string[]): void {
151
- for (let i = 0; i < scopes.length; i++) {
152
- for (let j = i + 1; j < scopes.length; j++) {
153
- const a = scopes[i].replace(/\*.*$/, "");
154
- const b = scopes[j].replace(/\*.*$/, "");
155
- if (a.startsWith(b) || b.startsWith(a)) {
156
- throw new WorktreeError(
157
- `Overlapping scopes: "${scopes[i]}" and "${scopes[j]}"`,
158
- WorktreeErrorCode.OVERLAPPING_SCOPES,
159
- );
160
- }
161
- }
162
- }
163
- }
164
-
165
- async function handleNew(args: NewArgs): Promise<string> {
166
- const wt = await worktree.create(args.branch, { base: args.base });
167
-
168
- return [`Created worktree: ${wt.path}`, `Branch: ${wt.branch ?? "detached"}`, "", `To switch: cd ${wt.path}`].join(
169
- "\n",
170
- );
171
- }
172
-
173
- async function handleList(ctx: HookCommandContext): Promise<string> {
174
- const worktrees = await worktree.list();
175
- const cwd = path.resolve(ctx.cwd);
176
- const mainPath = await getRepoRoot();
177
-
178
- const lines: string[] = [];
179
-
180
- for (const wt of worktrees) {
181
- const stats = await getStats(wt.path);
182
- const isCurrent = cwd === wt.path || cwd.startsWith(wt.path + path.sep);
183
- const isMain = wt.path === mainPath;
184
-
185
- const marker = isCurrent ? "->" : " ";
186
- const mainTag = isMain ? " [main]" : "";
187
- const branch = wt.branch ?? "detached";
188
- const statsStr = formatStats(stats);
189
-
190
- lines.push(`${marker} ${branch}${mainTag} (${statsStr})`);
191
- }
192
-
193
- return lines.join("\n") || "No worktrees found";
194
- }
195
-
196
- async function handleMerge(args: MergeArgs): Promise<string> {
197
- const target = args.target ?? "main";
198
- const strategy = args.strategy ?? "rebase";
199
-
200
- const result = await collapse(args.source, target, {
201
- strategy,
202
- keepSource: args.keep,
203
- });
204
-
205
- const lines = [
206
- `Collapsed ${args.source} -> ${target}`,
207
- `Strategy: ${strategy}`,
208
- `Changes: +${result.insertions} -${result.deletions} in ${result.filesChanged} files`,
209
- ];
210
-
211
- if (!args.keep) {
212
- lines.push("Source worktree removed");
213
- }
214
-
215
- return lines.join("\n");
216
- }
217
-
218
- async function handleRm(args: RmArgs): Promise<string> {
219
- const wt = await worktree.find(args.name);
220
- await worktree.remove(args.name, { force: args.force });
221
-
222
- const mainPath = await getRepoRoot();
223
- if (wt.branch) {
224
- await git(["branch", "-D", wt.branch], mainPath);
225
- return `Removed worktree and branch: ${wt.branch}`;
226
- }
227
-
228
- return `Removed worktree: ${wt.path}`;
229
- }
230
-
231
- async function handleStatus(): Promise<string> {
232
- const worktrees = await worktree.list();
233
- const sections: string[] = [];
234
-
235
- for (const wt of worktrees) {
236
- const branch = wt.branch ?? "detached";
237
- const name = path.basename(wt.path);
238
-
239
- const statusResult = await git(["status", "--short"], wt.path);
240
- const status = statusResult.stdout.trim() || "(clean)";
241
-
242
- sections.push(`${name} (${branch})\n${"-".repeat(40)}\n${status}`);
243
- }
244
-
245
- return sections.join("\n\n");
246
- }
247
-
248
- async function handleSpawn(args: SpawnArgs, ctx: HookCommandContext): Promise<string> {
249
- const branch = args.name ?? `wt-agent-${nanoid(6)}`;
250
- const wt = await worktree.create(branch);
251
-
252
- const session = await createSession({
253
- branch,
254
- path: wt.path,
255
- scope: args.scope ? [args.scope] : undefined,
256
- task: args.task,
257
- });
258
- await updateSession(session.id, { status: "active" });
259
-
260
- const agent = await pickAgent(ctx.cwd);
261
- const context = args.scope ? `Scope: ${args.scope}` : undefined;
262
-
263
- // Command context doesn't expose a spawn API, so run the task subprocess directly.
264
- const result = await runSubprocess({
265
- cwd: wt.path,
266
- agent,
267
- task: args.task,
268
- index: 0,
269
- taskId: generateTaskName(),
270
- context,
271
- });
272
-
273
- await updateSession(session.id, {
274
- status: result.exitCode === 0 ? "completed" : "failed",
275
- completedAt: Date.now(),
276
- });
277
-
278
- if (result.exitCode !== 0) {
279
- return [
280
- `Agent failed in worktree: ${branch}`,
281
- result.stderr.trim() ? `Error: ${result.stderr.trim()}` : "Error: agent execution failed",
282
- "",
283
- "Actions:",
284
- ` /wt merge ${branch} - Apply changes to main`,
285
- " /wt status - Inspect changes",
286
- ` /wt rm ${branch} - Discard changes`,
287
- ].join("\n");
288
- }
289
-
290
- return [
291
- `Agent completed in worktree: ${branch}`,
292
- "",
293
- "Actions:",
294
- ` /wt merge ${branch} - Apply changes to main`,
295
- " /wt status - Inspect changes",
296
- ` /wt rm ${branch} - Discard changes`,
297
- ].join("\n");
298
- }
299
-
300
- async function handleParallel(args: ParallelTask[], ctx: HookCommandContext): Promise<string> {
301
- validateDisjointScopes(args.map((t) => t.scope));
302
-
303
- const sessionId = `parallel-${nanoid()}`;
304
- const agent = await pickAgent(ctx.cwd);
305
-
306
- const worktrees: Array<{ task: ParallelTask; wt: worktree.Worktree; session: worktree.WorktreeSession }> = [];
307
- for (let i = 0; i < args.length; i++) {
308
- const task = args[i];
309
- const branch = `wt-parallel-${sessionId}-${i}`;
310
- const wt = await worktree.create(branch);
311
- const session = await createSession({
312
- branch,
313
- path: wt.path,
314
- scope: [task.scope],
315
- task: task.task,
316
- });
317
- worktrees.push({ task, wt, session });
318
- }
319
-
320
- const agentPromises = worktrees.map(async ({ task, wt, session }, index) => {
321
- await updateSession(session.id, { status: "active" });
322
- const result = await runSubprocess({
323
- cwd: wt.path,
324
- agent,
325
- task: task.task,
326
- index,
327
- taskId: generateTaskName(),
328
- context: `Scope: ${task.scope}`,
329
- });
330
- await updateSession(session.id, {
331
- status: result.exitCode === 0 ? "completed" : "failed",
332
- completedAt: Date.now(),
333
- });
334
- return { wt, session, result };
335
- });
336
-
337
- const results = await Promise.all(agentPromises);
338
-
339
- const mergeResults: string[] = [];
340
-
341
- for (const { wt, session } of results) {
342
- try {
343
- await updateSession(session.id, { status: "merging" });
344
- const collapseResult = await collapse(wt.branch ?? wt.path, "main", {
345
- strategy: "simple",
346
- keepSource: false,
347
- });
348
- await updateSession(session.id, { status: "merged" });
349
- mergeResults.push(
350
- `ok ${wt.branch ?? path.basename(wt.path)}: +${collapseResult.insertions} -${collapseResult.deletions}`,
351
- );
352
- } catch (err) {
353
- await updateSession(session.id, { status: "failed" });
354
- mergeResults.push(`err ${wt.branch ?? path.basename(wt.path)}: ${formatError(err)}`);
355
- }
356
- }
357
-
358
- return [`Parallel execution complete (${args.length} agents)`, "", "Results:", ...mergeResults].join("\n");
359
- }
360
-
361
- export class WorktreeCommand implements CustomCommand {
362
- name = "wt";
363
- description = "Git worktree management";
364
-
365
- // biome-ignore lint/complexity/noUselessConstructor: interface conformance - loader passes API to all commands
366
- constructor(_api: CustomCommandAPI) {}
367
-
368
- async execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> {
369
- if (args.length === 0) return formatUsage();
370
-
371
- const subcommand = args[0];
372
- const rest = args.slice(1);
373
-
374
- try {
375
- switch (subcommand) {
376
- case "new": {
377
- const parsed = parseFlags(rest);
378
- const branch = parsed.positionals[0];
379
- if (!branch) return formatUsage();
380
- const base = getFlagValue(parsed.flags, "base");
381
- if (parsed.flags.get("base") === true) {
382
- return "Missing value for --base";
383
- }
384
- return await handleNew({ branch, base });
385
- }
386
- case "list":
387
- return await handleList(ctx);
388
- case "merge": {
389
- const parsed = parseFlags(rest);
390
- const source = parsed.positionals[0];
391
- const target = parsed.positionals[1];
392
- if (!source) return formatUsage();
393
- const strategyRaw = getFlagValue(parsed.flags, "strategy");
394
- if (parsed.flags.get("strategy") === true) {
395
- return "Missing value for --strategy";
396
- }
397
- const strategy = strategyRaw as CollapseStrategy | undefined;
398
- const keep = getFlagBoolean(parsed.flags, "keep");
399
- return await handleMerge({ source, target, strategy, keep });
400
- }
401
- case "rm": {
402
- const parsed = parseFlags(rest);
403
- const name = parsed.positionals[0];
404
- if (!name) return formatUsage();
405
- const force = getFlagBoolean(parsed.flags, "force");
406
- return await handleRm({ name, force });
407
- }
408
- case "status":
409
- return await handleStatus();
410
- case "spawn": {
411
- const parsed = parseFlags(rest);
412
- const task = parsed.positionals[0];
413
- if (!task) return formatUsage();
414
- const scope = getFlagValue(parsed.flags, "scope");
415
- if (parsed.flags.get("scope") === true) {
416
- return "Missing value for --scope";
417
- }
418
- const name = getFlagValue(parsed.flags, "name");
419
- return await handleSpawn({ task, scope, name }, ctx);
420
- }
421
- case "parallel": {
422
- const tasks = parseParallelTasks(rest);
423
- if (tasks.length === 0) return formatUsage();
424
- return await handleParallel(tasks, ctx);
425
- }
426
- default:
427
- return formatUsage();
428
- }
429
- } catch (err) {
430
- return formatError(err);
431
- }
432
- }
433
- }
434
-
435
- export default WorktreeCommand;