@milanglacier/pi-plan-mode 0.5.1

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/flow.ts ADDED
@@ -0,0 +1,665 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { BorderedLoader } from "@earendil-works/pi-coding-agent";
4
+ import path from "node:path";
5
+
6
+ import type { PlanModeState } from "./types";
7
+
8
+ import {
9
+ createFreshPlanFilePath,
10
+ ensurePlanFileExists,
11
+ movePlanFile,
12
+ pathExists,
13
+ readPlanFile,
14
+ resetPlanFile,
15
+ resolveActivePlanFilePath,
16
+ resolvePlanLocationInput,
17
+ } from "./plan-files";
18
+ import { getFirstUserMessageId, hasEntryInSession } from "./state";
19
+ import {
20
+ buildImplementationPrefill,
21
+ PLAN_MODE_END_OPTIONS,
22
+ PLAN_MODE_START_OPTIONS,
23
+ PLAN_MODE_SUMMARY_PROMPT,
24
+ } from "./utils";
25
+
26
+ interface PlanModeStateManager {
27
+ getState: () => PlanModeState;
28
+ setState: (ctx: ExtensionContext, nextState: PlanModeState) => void;
29
+ startPlanMode: (
30
+ ctx: ExtensionContext,
31
+ options: {
32
+ originLeafId?: string;
33
+ planFilePath: string;
34
+ },
35
+ ) => void;
36
+ }
37
+
38
+ interface PlanModeExitSummary {
39
+ planFilePath: string;
40
+ planText?: string;
41
+ }
42
+
43
+ async function navigateToFreshPlanningBranch(ctx: ExtensionContext, cancelMessage: string): Promise<boolean> {
44
+ const firstUserMessageId = getFirstUserMessageId(ctx);
45
+ if (!firstUserMessageId) {
46
+ ctx.ui.notify("No user message found to branch planning from.", "error");
47
+ return false;
48
+ }
49
+
50
+ try {
51
+ const navigateResult = await ctx.navigateTree(firstUserMessageId, {
52
+ label: "plan",
53
+ summarize: false,
54
+ });
55
+ if (navigateResult.cancelled) {
56
+ ctx.ui.notify(cancelMessage, "info");
57
+ return false;
58
+ }
59
+ } catch (error) {
60
+ ctx.ui.notify(
61
+ `Failed to create a fresh planning branch: ${error instanceof Error ? error.message : String(error)}`,
62
+ "error",
63
+ );
64
+ return false;
65
+ }
66
+
67
+ if (ctx.hasUI) {
68
+ ctx.ui.setEditorText("");
69
+ }
70
+ return true;
71
+ }
72
+
73
+ async function navigateToSavedPlanningBranch(
74
+ ctx: ExtensionContext,
75
+ options: {
76
+ savedLeafId?: string;
77
+ currentLeafId?: string;
78
+ cancelMessage: string;
79
+ },
80
+ ): Promise<boolean> {
81
+ if (!options.savedLeafId || options.savedLeafId === options.currentLeafId) {
82
+ return true;
83
+ }
84
+
85
+ if (!hasEntryInSession(ctx, options.savedLeafId)) {
86
+ ctx.ui.notify("Saved planning branch is unavailable. Continuing from the current branch tip.", "warning");
87
+ return true;
88
+ }
89
+
90
+ try {
91
+ const navigateResult = await ctx.navigateTree(options.savedLeafId, {
92
+ label: "plan",
93
+ summarize: false,
94
+ });
95
+ if (navigateResult.cancelled) {
96
+ ctx.ui.notify(options.cancelMessage, "info");
97
+ return false;
98
+ }
99
+ if (ctx.hasUI) {
100
+ ctx.ui.notify("Resumed previous planning branch.", "info");
101
+ }
102
+ } catch (error) {
103
+ ctx.ui.notify(
104
+ `Failed to resume the saved planning branch: ${error instanceof Error ? error.message : String(error)}`,
105
+ "error",
106
+ );
107
+ return false;
108
+ }
109
+
110
+ return true;
111
+ }
112
+
113
+ async function confirmMoveOverwriteIfNeeded(
114
+ ctx: ExtensionContext,
115
+ sourcePath: string | undefined,
116
+ targetPath: string,
117
+ ): Promise<boolean> {
118
+ if (!sourcePath || sourcePath === targetPath) {
119
+ return true;
120
+ }
121
+
122
+ const [sourceExists, targetExists] = await Promise.all([pathExists(sourcePath), pathExists(targetPath)]);
123
+ if (!sourceExists || !targetExists) {
124
+ return true;
125
+ }
126
+
127
+ if (!ctx.hasUI) {
128
+ ctx.ui.notify(`Refusing to overwrite existing plan file without interactive confirmation: ${targetPath}`, "error");
129
+ return false;
130
+ }
131
+
132
+ const shouldOverwrite = await ctx.ui.confirm(
133
+ "Overwrite existing plan file?",
134
+ `Target already exists:\n${targetPath}\n\nMove current plan file and overwrite target contents?`,
135
+ );
136
+ if (!shouldOverwrite) {
137
+ ctx.ui.notify("Plan file move cancelled.", "info");
138
+ return false;
139
+ }
140
+
141
+ return true;
142
+ }
143
+
144
+ async function updateActivePlanFileLocation(
145
+ ctx: ExtensionContext,
146
+ stateManager: PlanModeStateManager,
147
+ rawLocation: string,
148
+ ): Promise<{ previousPath: string; nextPath: string } | undefined> {
149
+ const previousPath = resolveActivePlanFilePath(ctx, stateManager.getState().planFilePath);
150
+
151
+ let nextPath: string | null;
152
+ try {
153
+ nextPath = await resolvePlanLocationInput(ctx, rawLocation);
154
+ } catch (error) {
155
+ ctx.ui.notify(
156
+ `Failed to resolve plan file location: ${error instanceof Error ? error.message : String(error)}`,
157
+ "error",
158
+ );
159
+ return undefined;
160
+ }
161
+
162
+ if (!nextPath) {
163
+ ctx.ui.notify("Please enter a valid plan file location.", "warning");
164
+ return undefined;
165
+ }
166
+
167
+ let shouldMove: boolean;
168
+ try {
169
+ shouldMove = await confirmMoveOverwriteIfNeeded(ctx, previousPath, nextPath);
170
+ } catch (error) {
171
+ ctx.ui.notify(`Failed to check target path: ${error instanceof Error ? error.message : String(error)}`, "error");
172
+ return undefined;
173
+ }
174
+ if (!shouldMove) {
175
+ return undefined;
176
+ }
177
+
178
+ try {
179
+ await movePlanFile(previousPath, nextPath);
180
+ } catch (error) {
181
+ ctx.ui.notify(`Failed to move plan file: ${error instanceof Error ? error.message : String(error)}`, "error");
182
+ return undefined;
183
+ }
184
+
185
+ const state = stateManager.getState();
186
+ if (state.planFilePath !== nextPath) {
187
+ stateManager.setState(ctx, {
188
+ ...state,
189
+ planFilePath: nextPath,
190
+ });
191
+ }
192
+
193
+ return {
194
+ nextPath,
195
+ previousPath,
196
+ };
197
+ }
198
+
199
+ async function exitPlanMode(
200
+ ctx: ExtensionContext,
201
+ stateManager: PlanModeStateManager,
202
+ wantsSummary: boolean,
203
+ onPlanModeExited?: (summary: PlanModeExitSummary) => void,
204
+ ): Promise<boolean> {
205
+ const state = stateManager.getState();
206
+ if (!state.active) {
207
+ ctx.ui.notify("Plan mode is not active.", "info");
208
+ return false;
209
+ }
210
+ if (!ctx.hasUI) {
211
+ ctx.ui.notify("Exiting plan mode requires interactive mode.", "error");
212
+ return false;
213
+ }
214
+
215
+ const activeState = state;
216
+ const planningLeafId = ctx.sessionManager.getLeafId();
217
+ const { originLeafId } = activeState;
218
+ const planFilePath = resolveActivePlanFilePath(ctx, activeState.planFilePath);
219
+
220
+ const canNavigateToOrigin = Boolean(originLeafId && hasEntryInSession(ctx, originLeafId));
221
+ if (canNavigateToOrigin && originLeafId) {
222
+ if (wantsSummary) {
223
+ const result = await ctx.ui.custom<{ cancelled: boolean; error?: string } | null>((tui, theme, _kb, done) => {
224
+ const loader = new BorderedLoader(tui, theme, "Summarizing planning branch...");
225
+ loader.onAbort = () => done(null);
226
+
227
+ ctx
228
+ .navigateTree(originLeafId, {
229
+ customInstructions: PLAN_MODE_SUMMARY_PROMPT,
230
+ replaceInstructions: true,
231
+ summarize: true,
232
+ })
233
+ .then(done)
234
+ .catch((error) =>
235
+ done({
236
+ cancelled: false,
237
+ error: error instanceof Error ? error.message : String(error),
238
+ }),
239
+ );
240
+
241
+ return loader;
242
+ });
243
+
244
+ if (result === null) {
245
+ ctx.ui.notify("Summarization cancelled. Use /plan to try again.", "info");
246
+ return false;
247
+ }
248
+ if (result.error) {
249
+ ctx.ui.notify(`Summarization failed: ${result.error}`, "error");
250
+ return false;
251
+ }
252
+ if (result.cancelled) {
253
+ ctx.ui.notify("Returning from plan mode was cancelled. Use /plan to try again.", "info");
254
+ return false;
255
+ }
256
+ } else {
257
+ try {
258
+ const navigateResult = await ctx.navigateTree(originLeafId, {
259
+ summarize: false,
260
+ });
261
+ if (navigateResult.cancelled) {
262
+ ctx.ui.notify("Returning from plan mode was cancelled. Use /plan to try again.", "info");
263
+ return false;
264
+ }
265
+ } catch (error) {
266
+ ctx.ui.notify(
267
+ `Failed to restore origin point: ${error instanceof Error ? error.message : String(error)}`,
268
+ "error",
269
+ );
270
+ return false;
271
+ }
272
+ }
273
+ } else if (originLeafId) {
274
+ ctx.ui.notify("Origin point is unavailable. Ended planning at the current branch tip.", "warning");
275
+ }
276
+
277
+ stateManager.setState(ctx, {
278
+ active: false,
279
+ lastPlanLeafId: planningLeafId ?? activeState.lastPlanLeafId,
280
+ planFilePath,
281
+ version: activeState.version,
282
+ });
283
+ const planText = (await readPlanFile(planFilePath))?.trim();
284
+ if (planText) {
285
+ ctx.ui.setEditorText(buildImplementationPrefill(planFilePath));
286
+ }
287
+
288
+ onPlanModeExited?.({
289
+ planFilePath,
290
+ planText,
291
+ });
292
+ return true;
293
+ }
294
+
295
+ async function endPlanMode(
296
+ ctx: ExtensionContext,
297
+ stateManager: PlanModeStateManager,
298
+ onPlanModeExited?: (summary: PlanModeExitSummary) => void,
299
+ ) {
300
+ const state = stateManager.getState();
301
+ if (!state.active) {
302
+ ctx.ui.notify("Plan mode is not active.", "info");
303
+ return;
304
+ }
305
+ if (!ctx.hasUI) {
306
+ ctx.ui.notify("Exiting plan mode requires interactive mode.", "error");
307
+ return;
308
+ }
309
+
310
+ await ctx.waitForIdle();
311
+
312
+ const choice = await ctx.ui.select("Plan mode action (Esc stays in Plan mode)", [...PLAN_MODE_END_OPTIONS]);
313
+ if (choice === undefined) {
314
+ ctx.ui.notify("Continuing in Plan mode (Esc).", "info");
315
+ return;
316
+ }
317
+
318
+ const wantsSummary = choice === PLAN_MODE_END_OPTIONS[1];
319
+ await exitPlanMode(ctx, stateManager, wantsSummary, onPlanModeExited);
320
+ }
321
+
322
+ function canOfferEmptyBranchStart(ctx: ExtensionContext, originLeafId: string | undefined): boolean {
323
+ const firstUserMessageId = getFirstUserMessageId(ctx);
324
+ return Boolean(originLeafId && firstUserMessageId && firstUserMessageId !== originLeafId);
325
+ }
326
+
327
+ async function waitForIdleInShortcutContext(ctx: ExtensionContext): Promise<void> {
328
+ while (!ctx.isIdle()) {
329
+ await new Promise<void>((resolve) => {
330
+ setTimeout(resolve, 25);
331
+ });
332
+ }
333
+ }
334
+
335
+ function extractTextFromMessageContent(content: unknown): string {
336
+ if (typeof content === "string") {
337
+ return content;
338
+ }
339
+ if (!Array.isArray(content)) {
340
+ return "";
341
+ }
342
+
343
+ let text = "";
344
+ for (const part of content) {
345
+ if (!part || typeof part !== "object") {
346
+ continue;
347
+ }
348
+ const typedPart = part as { type?: unknown; text?: unknown };
349
+ if (typedPart.type === "text" && typeof typedPart.text === "string") {
350
+ text += typedPart.text;
351
+ }
352
+ }
353
+ return text;
354
+ }
355
+
356
+ async function navigateTreeInShortcutContext(
357
+ ctx: ExtensionContext,
358
+ targetId: string,
359
+ options?: {
360
+ summarize?: boolean;
361
+ label?: string;
362
+ },
363
+ ): Promise<{ cancelled: boolean }> {
364
+ if (options?.summarize) {
365
+ ctx.ui.notify("Alt+P exited plan mode without branch summarization. Use /plan for summarize-on-exit.", "warning");
366
+ }
367
+
368
+ const sessionManager = ctx.sessionManager as ExtensionContext["sessionManager"] & {
369
+ getEntry?: (entryId: string) =>
370
+ | {
371
+ type?: string;
372
+ parentId?: string | null;
373
+ message?: {
374
+ role?: string;
375
+ content?: unknown;
376
+ };
377
+ content?: unknown;
378
+ }
379
+ | undefined;
380
+ branch?: (entryId: string) => void;
381
+ resetLeaf?: () => void;
382
+ appendLabelChange?: (targetId: string, label: string | undefined) => void;
383
+ };
384
+
385
+ if (typeof sessionManager.getEntry !== "function") {
386
+ return { cancelled: true };
387
+ }
388
+
389
+ const targetEntry = sessionManager.getEntry(targetId);
390
+ if (!targetEntry) {
391
+ return { cancelled: true };
392
+ }
393
+
394
+ let newLeafId: string | null = targetId;
395
+ let editorText: string | undefined;
396
+
397
+ if (targetEntry.type === "message" && targetEntry.message?.role === "user") {
398
+ newLeafId = targetEntry.parentId ?? null;
399
+ editorText = extractTextFromMessageContent(targetEntry.message.content);
400
+ } else if (targetEntry.type === "custom_message") {
401
+ newLeafId = targetEntry.parentId ?? null;
402
+ editorText = extractTextFromMessageContent(targetEntry.content);
403
+ }
404
+
405
+ if (newLeafId === null) {
406
+ if (typeof sessionManager.resetLeaf !== "function") {
407
+ return { cancelled: true };
408
+ }
409
+ sessionManager.resetLeaf();
410
+ } else {
411
+ if (typeof sessionManager.branch !== "function") {
412
+ return { cancelled: true };
413
+ }
414
+ sessionManager.branch(newLeafId);
415
+ }
416
+
417
+ if (options?.label && typeof sessionManager.appendLabelChange === "function") {
418
+ sessionManager.appendLabelChange(targetId, options.label);
419
+ }
420
+
421
+ if (editorText && ctx.hasUI && !ctx.ui.getEditorText().trim()) {
422
+ ctx.ui.setEditorText(editorText);
423
+ }
424
+
425
+ return { cancelled: false };
426
+ }
427
+
428
+ function createShortcutCommandContext(ctx: ExtensionContext): ExtensionCommandContext {
429
+ return {
430
+ ...ctx,
431
+ fork: async () => ({ cancelled: true }),
432
+ navigateTree: async (targetId, options) => navigateTreeInShortcutContext(ctx, targetId, options),
433
+ newSession: async () => ({ cancelled: true }),
434
+ reload: async () => {},
435
+ switchSession: async () => ({ cancelled: true }),
436
+ waitForIdle: async () => {
437
+ await waitForIdleInShortcutContext(ctx);
438
+ },
439
+ };
440
+ }
441
+
442
+ export function registerPlanModeCommand(
443
+ pi: ExtensionAPI,
444
+ dependencies: {
445
+ stateManager: PlanModeStateManager;
446
+ onPlanModeExited?: (summary: PlanModeExitSummary) => void;
447
+ },
448
+ ) {
449
+ const handlePlanModeCommand = async (args: string, ctx: ExtensionCommandContext) => {
450
+ const rawLocation = args.trim();
451
+ const state = dependencies.stateManager.getState();
452
+
453
+ if (state.active) {
454
+ if (rawLocation.length > 0) {
455
+ const moved = await updateActivePlanFileLocation(ctx, dependencies.stateManager, rawLocation);
456
+ if (!moved) {
457
+ return;
458
+ }
459
+ if (moved.previousPath === moved.nextPath) {
460
+ ctx.ui.notify("Plan file location unchanged.", "info");
461
+ } else {
462
+ ctx.ui.notify(`Plan file moved to ${moved.nextPath}.`, "info");
463
+ }
464
+ return;
465
+ }
466
+
467
+ await endPlanMode(ctx, dependencies.stateManager, dependencies.onPlanModeExited);
468
+ return;
469
+ }
470
+
471
+ await ctx.waitForIdle();
472
+
473
+ let requestedPlanFilePath: string | undefined;
474
+ if (rawLocation.length > 0) {
475
+ try {
476
+ requestedPlanFilePath = (await resolvePlanLocationInput(ctx, rawLocation)) ?? undefined;
477
+ } catch (error) {
478
+ ctx.ui.notify(
479
+ `Failed to resolve plan file location: ${error instanceof Error ? error.message : String(error)}`,
480
+ "error",
481
+ );
482
+ return;
483
+ }
484
+ if (!requestedPlanFilePath) {
485
+ ctx.ui.notify("Please provide a valid plan file location.", "warning");
486
+ return;
487
+ }
488
+ }
489
+
490
+ const originLeafId = ctx.sessionManager.getLeafId();
491
+ const canStartFromEmptyBranch = canOfferEmptyBranchStart(ctx, originLeafId);
492
+ const currentState = dependencies.stateManager.getState();
493
+ const sessionPlanFilePath = resolveActivePlanFilePath(ctx, currentState.planFilePath);
494
+ const existingSessionPlanText = (await readPlanFile(sessionPlanFilePath))?.trim();
495
+ const savedPlanLeafId = currentState.lastPlanLeafId;
496
+ let planFilePath = requestedPlanFilePath ?? sessionPlanFilePath;
497
+
498
+ type StartIntent = "continue" | "empty-branch" | "current-branch";
499
+ let startIntent: StartIntent = existingSessionPlanText ? "continue" : "current-branch";
500
+
501
+ if (ctx.hasUI) {
502
+ if (existingSessionPlanText) {
503
+ const continueOption = "Continue planning";
504
+ const startFreshOption = "Start fresh";
505
+ const choices = canStartFromEmptyBranch
506
+ ? [continueOption, ...PLAN_MODE_START_OPTIONS]
507
+ : [continueOption, startFreshOption];
508
+ const choice = await ctx.ui.select(`Start planning:\nPlan file: ${sessionPlanFilePath}`, choices);
509
+ if (choice === undefined) {
510
+ ctx.ui.notify("Plan mode activation cancelled.", "info");
511
+ return;
512
+ }
513
+ if (choice === continueOption) {
514
+ startIntent = "continue";
515
+ } else if (choice === PLAN_MODE_START_OPTIONS[0]) {
516
+ startIntent = "empty-branch";
517
+ } else {
518
+ startIntent = "current-branch";
519
+ }
520
+ } else if (canStartFromEmptyBranch) {
521
+ const choice = await ctx.ui.select("Start planning in:", [...PLAN_MODE_START_OPTIONS]);
522
+ if (choice === undefined) {
523
+ ctx.ui.notify("Plan mode activation cancelled.", "info");
524
+ return;
525
+ }
526
+ startIntent = choice === PLAN_MODE_START_OPTIONS[0] ? "empty-branch" : "current-branch";
527
+ }
528
+ }
529
+
530
+ if (startIntent === "continue") {
531
+ const resumedSavedPlanningBranch = await navigateToSavedPlanningBranch(ctx, {
532
+ cancelMessage: "Plan mode activation cancelled.",
533
+ currentLeafId: originLeafId,
534
+ savedLeafId: savedPlanLeafId,
535
+ });
536
+ if (!resumedSavedPlanningBranch) {
537
+ return;
538
+ }
539
+
540
+ if (requestedPlanFilePath && requestedPlanFilePath !== sessionPlanFilePath) {
541
+ let shouldMove: boolean;
542
+ try {
543
+ shouldMove = await confirmMoveOverwriteIfNeeded(ctx, sessionPlanFilePath, requestedPlanFilePath);
544
+ } catch (error) {
545
+ ctx.ui.notify(
546
+ `Failed to check target path: ${error instanceof Error ? error.message : String(error)}`,
547
+ "error",
548
+ );
549
+ return;
550
+ }
551
+ if (!shouldMove) {
552
+ return;
553
+ }
554
+
555
+ try {
556
+ await movePlanFile(sessionPlanFilePath, requestedPlanFilePath);
557
+ planFilePath = requestedPlanFilePath;
558
+ } catch (error) {
559
+ ctx.ui.notify(
560
+ `Failed to move existing plan file: ${error instanceof Error ? error.message : String(error)}`,
561
+ "error",
562
+ );
563
+ return;
564
+ }
565
+ } else {
566
+ planFilePath = sessionPlanFilePath;
567
+ }
568
+ } else {
569
+ if (startIntent === "empty-branch") {
570
+ if (!originLeafId) {
571
+ ctx.ui.notify("Could not determine origin point for returning from planning.", "error");
572
+ return;
573
+ }
574
+
575
+ const movedToFreshBranch = await navigateToFreshPlanningBranch(ctx, "Plan mode activation cancelled.");
576
+ if (!movedToFreshBranch) {
577
+ return;
578
+ }
579
+ }
580
+
581
+ if (requestedPlanFilePath) {
582
+ planFilePath = requestedPlanFilePath;
583
+ } else if (existingSessionPlanText) {
584
+ try {
585
+ planFilePath = await createFreshPlanFilePath(ctx, path.dirname(sessionPlanFilePath));
586
+ } catch (error) {
587
+ ctx.ui.notify(
588
+ `Failed to allocate a fresh plan file path: ${error instanceof Error ? error.message : String(error)}`,
589
+ "error",
590
+ );
591
+ return;
592
+ }
593
+ } else {
594
+ planFilePath = sessionPlanFilePath;
595
+ }
596
+
597
+ if (requestedPlanFilePath) {
598
+ let requestedPathExists = false;
599
+ try {
600
+ requestedPathExists = await pathExists(planFilePath);
601
+ } catch (error) {
602
+ ctx.ui.notify(
603
+ `Failed to check requested plan path: ${error instanceof Error ? error.message : String(error)}`,
604
+ "error",
605
+ );
606
+ return;
607
+ }
608
+
609
+ if (requestedPathExists) {
610
+ if (!ctx.hasUI) {
611
+ ctx.ui.notify(
612
+ `Refusing to overwrite existing plan file without interactive confirmation: ${planFilePath}`,
613
+ "error",
614
+ );
615
+ return;
616
+ }
617
+
618
+ const shouldOverwriteRequestedPath = await ctx.ui.confirm(
619
+ "Overwrite existing plan file?",
620
+ `Plan file already exists:\n${planFilePath}\n\nStart fresh planning and overwrite this file?`,
621
+ );
622
+ if (!shouldOverwriteRequestedPath) {
623
+ ctx.ui.notify("Plan mode activation cancelled.", "info");
624
+ return;
625
+ }
626
+ }
627
+ }
628
+
629
+ try {
630
+ await resetPlanFile(planFilePath);
631
+ } catch (error) {
632
+ ctx.ui.notify(`Failed to reset plan file: ${error instanceof Error ? error.message : String(error)}`, "error");
633
+ return;
634
+ }
635
+ }
636
+
637
+ try {
638
+ await ensurePlanFileExists(planFilePath);
639
+ } catch (error) {
640
+ ctx.ui.notify(
641
+ `Failed to initialize plan file: ${error instanceof Error ? error.message : String(error)}`,
642
+ "error",
643
+ );
644
+ return;
645
+ }
646
+
647
+ dependencies.stateManager.startPlanMode(ctx, {
648
+ originLeafId,
649
+ planFilePath,
650
+ });
651
+ };
652
+
653
+ pi.registerCommand("plan", {
654
+ description: "Start /plan, end it, or pass a plan file location.",
655
+ handler: handlePlanModeCommand,
656
+ });
657
+
658
+ pi.registerShortcut("alt+p", {
659
+ description: "Toggle /plan",
660
+ handler: async (ctx) => {
661
+ const shortcutCommandContext = createShortcutCommandContext(ctx);
662
+ await handlePlanModeCommand("", shortcutCommandContext);
663
+ },
664
+ });
665
+ }