@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.
- package/CHANGELOG.md +21 -0
- package/package.json +5 -6
- package/src/core/python-executor.ts +6 -10
- package/src/core/sdk.ts +0 -2
- package/src/core/settings-manager.ts +0 -105
- package/src/core/streaming-output.ts +10 -19
- package/src/core/tools/index.ts +0 -4
- package/src/core/tools/python.ts +3 -3
- package/src/index.ts +0 -2
- package/src/modes/interactive/components/settings-defs.ts +0 -70
- package/src/modes/interactive/components/settings-selector.ts +0 -1
- package/src/modes/interactive/controllers/event-controller.ts +0 -11
- package/src/modes/interactive/controllers/selector-controller.ts +0 -9
- package/src/modes/interactive/interactive-mode.ts +0 -58
- package/src/modes/interactive/types.ts +0 -15
- package/src/core/custom-commands/bundled/wt/index.ts +0 -435
- package/src/core/tools/git.ts +0 -213
- package/src/core/voice-controller.ts +0 -135
- package/src/core/voice-supervisor.ts +0 -976
- package/src/core/voice.ts +0 -314
- package/src/lib/worktree/collapse.ts +0 -180
- package/src/lib/worktree/constants.ts +0 -14
- package/src/lib/worktree/errors.ts +0 -23
- package/src/lib/worktree/git.ts +0 -60
- package/src/lib/worktree/index.ts +0 -15
- package/src/lib/worktree/operations.ts +0 -216
- package/src/lib/worktree/session.ts +0 -114
- package/src/lib/worktree/stats.ts +0 -67
- package/src/modes/interactive/utils/voice-manager.ts +0 -96
- package/src/prompts/tools/git.md +0 -9
- package/src/prompts/voice-summary.md +0 -12
|
@@ -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;
|