@dreki-gg/pi-plan-mode 0.7.2 → 0.13.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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Phase transitions — enter/exit plan mode, start execution, switch models.
3
+ */
4
+
5
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
6
+ import type { PlanModeState } from './state.js';
7
+ import type { ThinkingLevel } from './types.js';
8
+ import { PLAN_TOOLS, EXEC_TOOLS, PLAN_MODEL, PLAN_THINKING, EXEC_MODEL, EXEC_THINKING } from './constants.js';
9
+ import { updateUI } from './ui.js';
10
+
11
+ export async function switchModel(
12
+ pi: ExtensionAPI,
13
+ ctx: ExtensionContext,
14
+ preset: { provider: string; id: string },
15
+ ): Promise<boolean> {
16
+ const model = ctx.modelRegistry.find(preset.provider, preset.id);
17
+ if (!model) {
18
+ ctx.ui.notify(`Model ${preset.provider}/${preset.id} not found`, 'error');
19
+ return false;
20
+ }
21
+ const ok = await pi.setModel(model);
22
+ if (!ok) {
23
+ ctx.ui.notify(`No API key for ${preset.provider}/${preset.id}`, 'error');
24
+ return false;
25
+ }
26
+ return true;
27
+ }
28
+
29
+ export async function enterPlanMode(
30
+ state: PlanModeState,
31
+ pi: ExtensionAPI,
32
+ ctx: ExtensionContext,
33
+ ): Promise<void> {
34
+ state.planEnabled = true;
35
+ state.executing = false;
36
+ state.planDir = undefined;
37
+ state.plan = undefined;
38
+ state.previousThinking = pi.getThinkingLevel() as ThinkingLevel;
39
+ state.previousModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
40
+ pi.setActiveTools(PLAN_TOOLS);
41
+ await switchModel(pi, ctx, PLAN_MODEL);
42
+ pi.setThinkingLevel(PLAN_THINKING);
43
+ ctx.ui.notify(
44
+ `Plan mode ON — ${PLAN_MODEL.provider}/${PLAN_MODEL.id}:${PLAN_THINKING}`,
45
+ 'info',
46
+ );
47
+ updateUI(state, ctx);
48
+ state.persist(pi);
49
+ }
50
+
51
+ export async function exitPlanMode(
52
+ state: PlanModeState,
53
+ pi: ExtensionAPI,
54
+ ctx: ExtensionContext,
55
+ ): Promise<void> {
56
+ const { previousModel, previousThinking } = state;
57
+ state.reset();
58
+ pi.setActiveTools(EXEC_TOOLS);
59
+ if (previousModel) {
60
+ await switchModel(pi, ctx, previousModel);
61
+ }
62
+ if (previousThinking) {
63
+ pi.setThinkingLevel(previousThinking);
64
+ }
65
+ ctx.ui.notify('Plan mode OFF — original model restored', 'info');
66
+ updateUI(state, ctx);
67
+ state.persist(pi);
68
+ }
69
+
70
+ export async function startExecution(
71
+ state: PlanModeState,
72
+ pi: ExtensionAPI,
73
+ ctx: ExtensionContext,
74
+ ): Promise<void> {
75
+ state.planEnabled = false;
76
+ state.executing = true;
77
+ state.executionStartIdx = ctx.sessionManager.getEntries().length;
78
+ pi.setActiveTools(EXEC_TOOLS);
79
+ await switchModel(pi, ctx, EXEC_MODEL);
80
+ pi.setThinkingLevel(EXEC_THINKING);
81
+ ctx.ui.notify(
82
+ `Executing plan — ${EXEC_MODEL.provider}/${EXEC_MODEL.id}:${EXEC_THINKING}`,
83
+ 'info',
84
+ );
85
+ updateUI(state, ctx);
86
+ state.persist(pi);
87
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Plan disk I/O — save/load plans, exec-pending markers, and manifest updates.
3
+ */
4
+
5
+ import { mkdir, readFile, readdir, unlink, writeFile } from 'node:fs/promises';
6
+ import { readPlansJson, serializePlansJson } from './plans-json.js';
7
+ import type { PlanData, ExecPendingConfig } from './types.js';
8
+ import { EXEC_PENDING_FILE } from './constants.js';
9
+
10
+ export async function savePlanToDisk(planDir: string, plan: PlanData): Promise<void> {
11
+ await mkdir(planDir, { recursive: true });
12
+ await writeFile(`${planDir}/plan.json`, JSON.stringify(plan, null, 2) + '\n', 'utf-8');
13
+ }
14
+
15
+ export async function loadPlanFromDisk(dir: string): Promise<PlanData | undefined> {
16
+ try {
17
+ const text = await readFile(`${dir}/plan.json`, 'utf-8');
18
+ return JSON.parse(text) as PlanData;
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ }
23
+
24
+ export async function writeExecPending(dir: string, config: ExecPendingConfig): Promise<void> {
25
+ await mkdir(dir, { recursive: true });
26
+ await writeFile(`${dir}/${EXEC_PENDING_FILE}`, JSON.stringify(config, null, 2) + '\n', 'utf-8');
27
+ }
28
+
29
+ export async function readAndClearExecPending(): Promise<{ planDir: string; config: ExecPendingConfig } | undefined> {
30
+ try {
31
+ const entries = await readdir('.plans', { withFileTypes: true });
32
+ for (const entry of entries) {
33
+ if (!entry.isDirectory()) continue;
34
+ const dir = `.plans/${entry.name}`;
35
+ const markerPath = `${dir}/${EXEC_PENDING_FILE}`;
36
+ try {
37
+ const text = await readFile(markerPath, 'utf-8');
38
+ const config = JSON.parse(text) as ExecPendingConfig;
39
+ await unlink(markerPath);
40
+ return { planDir: dir, config };
41
+ } catch {
42
+ // No marker in this directory
43
+ }
44
+ }
45
+ } catch {
46
+ // .plans/ doesn't exist
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ export async function saveHandoff(planDir: string, content: string): Promise<void> {
52
+ await mkdir(planDir, { recursive: true });
53
+ await writeFile(`${planDir}/HANDOFF.md`, content, 'utf-8');
54
+ }
55
+
56
+ export async function loadHandoff(planDir: string): Promise<string | undefined> {
57
+ try {
58
+ return await readFile(`${planDir}/HANDOFF.md`, 'utf-8');
59
+ } catch {
60
+ return undefined;
61
+ }
62
+ }
63
+
64
+ export async function updatePlansManifest(
65
+ planName: string,
66
+ status: 'in-progress' | 'done',
67
+ title?: string,
68
+ ): Promise<void> {
69
+ const manifest = await readPlansJson();
70
+ const existing = manifest[planName];
71
+ const now = new Date().toISOString();
72
+ manifest[planName] = {
73
+ status,
74
+ title: title ?? existing?.title ?? 'Untitled plan',
75
+ created: existing?.created ?? now,
76
+ completed: status === 'done' ? now : null,
77
+ };
78
+ await mkdir('.plans', { recursive: true });
79
+ await writeFile('.plans/plans.json', serializePlansJson(manifest), 'utf-8');
80
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Prompt builders for plan and execution phases.
3
+ */
4
+
5
+ import type { PlanData } from './types.js';
6
+ import { PLAN_TOOLS } from './constants.js';
7
+
8
+ export function buildPlanModePrompt(): string {
9
+ return `[PLAN MODE ACTIVE]
10
+ You are in plan mode — a planning phase with strict bash restrictions.
11
+
12
+ Restrictions:
13
+ - Available tools: ${PLAN_TOOLS.join(', ')}
14
+ - Bash is restricted to read-only commands (ls, grep, git status, etc.)
15
+
16
+ Your task:
17
+ 1. Analyze the codebase thoroughly using the available read-only tools
18
+ 2. Ask clarifying questions if needed (use the questionnaire tool)
19
+ 3. Produce a detailed, concrete plan
20
+
21
+ When you are ready to finalize the plan, call the submit_plan tool with:
22
+ - name: a short kebab-case name (e.g. "add-auth-middleware")
23
+ - title: a human-readable plan title
24
+ - handoff: a markdown document that explains:
25
+ 1. **What** change is being made and **why** it matters
26
+ 2. The approach and key design decisions
27
+ 3. Relevant codebase context: file paths, APIs, patterns, constraints, and gotchas
28
+ This document is saved as HANDOFF.md and must be clear to both human reviewers and executor agents.
29
+ - steps: an array of steps, each with a short description (≤60 chars for display) and detailed implementation instructions
30
+
31
+ Do NOT attempt to make product code changes — only analyze and plan.
32
+ Do NOT write files manually — use submit_plan to finalize the plan.
33
+
34
+ When facing a significant technical decision with multiple viable approaches (architecture, API design, implementation strategy), use the technical-options skill: you generate the competing proposals yourself, then use the subagent tool to fan out voting agents for evaluation. Do not delegate the entire workflow to a subagent — you are the planner, you drive the process.`;
35
+ }
36
+
37
+ export function buildExecutionPrompt(plan: PlanData): string | undefined {
38
+ const remaining = plan.steps
39
+ .map((s, i) => ({ ...s, num: i + 1 }))
40
+ .filter((s) => s.status === 'pending');
41
+
42
+ if (remaining.length === 0) return undefined;
43
+
44
+ const stepList = remaining
45
+ .map((s) => `${s.num}. ${s.description}\n Details: ${s.details}`)
46
+ .join('\n\n');
47
+
48
+ const currentStep = remaining[0];
49
+
50
+ return `[EXECUTING PLAN — FOLLOW THE PLAN EXACTLY]
51
+
52
+ You are executing a structured plan. Your ONLY job is to implement the plan steps below, one at a time.
53
+
54
+ Rules:
55
+ - Work on ONE step at a time, starting with step ${currentStep.num}
56
+ - After completing each step, IMMEDIATELY call update_step to mark it done with notes summarizing what you changed (files modified, key decisions)
57
+ - Do NOT run diagnostics, linters, test suites, or skills unless a step explicitly asks for it
58
+ - Do NOT explore the codebase beyond what the current step requires
59
+ - Do NOT deviate from the plan — if something seems wrong, call update_step with status "blocked"
60
+
61
+ ## Current step
62
+ Step ${currentStep.num}: ${currentStep.description}
63
+ Details: ${currentStep.details}
64
+
65
+ ## Handoff
66
+ ${plan.handoff}
67
+
68
+ ## All remaining steps
69
+ ${stepList}
70
+
71
+ Start with step ${currentStep.num} NOW. When done, call update_step(step=${currentStep.num}, status="done", notes="<brief summary of what you did>").`;
72
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Resume and execution handoff — pick up in-progress plans, model picker, new session handoff.
3
+ */
4
+
5
+ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
6
+ import type { PlanModeState } from './state.js';
7
+ import type { PlanData } from './types.js';
8
+ import { EXEC_THINKING, EXEC_MODEL_OPTIONS } from './constants.js';
9
+ import { readPlansJson } from './plans-json.js';
10
+ import { loadPlanFromDisk, writeExecPending, savePlanToDisk } from './plan-storage.js';
11
+ import { enterPlanMode } from './phase-transitions.js';
12
+
13
+ export async function pickExecutionModel(
14
+ ctx: ExtensionContext,
15
+ ): Promise<{ provider: string; id: string } | undefined> {
16
+ const labels = EXEC_MODEL_OPTIONS.map((o) => o.label);
17
+ const choice = await ctx.ui.select('Execute with:', labels);
18
+ if (!choice) return undefined;
19
+ return EXEC_MODEL_OPTIONS.find((o) => o.label === choice)?.model;
20
+ }
21
+
22
+ export async function executeInNewSession(
23
+ ctx: ExtensionCommandContext,
24
+ dir: string,
25
+ _planData: PlanData,
26
+ kickoff: string,
27
+ ): Promise<void> {
28
+ const selectedModel = await pickExecutionModel(ctx);
29
+ if (!selectedModel) return;
30
+
31
+ await writeExecPending(dir, { model: selectedModel, thinking: EXEC_THINKING });
32
+ const parentSession = ctx.sessionManager.getSessionFile();
33
+
34
+ await ctx.newSession({
35
+ parentSession,
36
+ withSession: async (newCtx) => {
37
+ await newCtx.sendUserMessage(kickoff);
38
+ },
39
+ });
40
+ }
41
+
42
+ export async function resumePlan(
43
+ state: PlanModeState,
44
+ pi: ExtensionAPI,
45
+ ctx: ExtensionCommandContext,
46
+ ): Promise<void> {
47
+ const manifest = await readPlansJson();
48
+ const inProgress = Object.entries(manifest).filter(([, e]) => e.status === 'in-progress');
49
+
50
+ if (inProgress.length === 0) {
51
+ ctx.ui.notify('No in-progress plans found in .plans/plans.json', 'info');
52
+ return;
53
+ }
54
+
55
+ const options = inProgress.map(([name, entry]) => `${name} — ${entry.title}`);
56
+ options.push('Cancel');
57
+
58
+ const choice = await ctx.ui.select('Resume which plan?', options);
59
+ if (!choice || choice === 'Cancel') return;
60
+
61
+ const planName = choice.split(' — ')[0];
62
+ const dir = `.plans/${planName}`;
63
+ const loaded = await loadPlanFromDisk(dir);
64
+
65
+ if (!loaded) {
66
+ ctx.ui.notify(`Could not load ${dir}/plan.json`, 'error');
67
+ return;
68
+ }
69
+
70
+ state.planDir = dir;
71
+ state.plan = loaded;
72
+
73
+ const doneCount = state.plan.steps.filter((s) => s.status === 'done' || s.status === 'skipped').length;
74
+ const pendingCount = state.plan.steps.filter((s) => s.status === 'pending').length;
75
+ const blockedCount = state.plan.steps.filter((s) => s.status === 'blocked').length;
76
+
77
+ if (pendingCount === 0 && blockedCount === 0) {
78
+ ctx.ui.notify(`Plan "${state.plan.title}" is already complete (${doneCount}/${state.plan.steps.length} done).`, 'info');
79
+ state.plan = undefined;
80
+ state.planDir = undefined;
81
+ return;
82
+ }
83
+
84
+ const summary = `${doneCount}/${state.plan.steps.length} done, ${pendingCount} pending${blockedCount ? `, ${blockedCount} blocked` : ''}`;
85
+ const action = await ctx.ui.select(
86
+ `Resume "${state.plan.title}" (${summary}) — what next?`,
87
+ ['Continue execution', 'Re-plan from scratch', 'Cancel'],
88
+ );
89
+
90
+ if (!action || action === 'Cancel') {
91
+ state.plan = undefined;
92
+ state.planDir = undefined;
93
+ return;
94
+ }
95
+
96
+ if (action === 'Re-plan from scratch') {
97
+ const planTitle = state.plan.title;
98
+ const planDirPath = state.planDir;
99
+ await enterPlanMode(state, pi, ctx);
100
+ pi.sendUserMessage(
101
+ `There is an existing plan "${planTitle}" at ${planDirPath}/plan.json. Review it and create a revised plan using submit_plan. Keep the same plan name ("${planName}").`,
102
+ );
103
+ return;
104
+ }
105
+
106
+ // Unblock any blocked steps
107
+ if (blockedCount > 0) {
108
+ for (const step of state.plan.steps) {
109
+ if (step.status === 'blocked') step.status = 'pending';
110
+ }
111
+ await savePlanToDisk(dir, state.plan);
112
+ }
113
+
114
+ const remaining = state.plan.steps
115
+ .map((s, i) => ({ ...s, num: i + 1 }))
116
+ .filter((s) => s.status === 'pending');
117
+ const stepList = remaining.map((s) => `${s.num}. ${s.description}`).join('\n');
118
+ const kickoff = `Resuming plan: "${state.plan.title}"\n\nCompleted: ${doneCount}/${state.plan.steps.length} steps\n\nRemaining steps:\n${stepList}\n\nContinue from step ${remaining[0].num}. Call update_step after completing each step.`;
119
+
120
+ await executeInNewSession(ctx, dir, state.plan, kickoff);
121
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Encapsulates all mutable plan-mode state with persistence helpers.
3
+ */
4
+
5
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
6
+ import type { PlanData, PersistedState, ThinkingLevel } from './types.js';
7
+
8
+ export class PlanModeState {
9
+ planEnabled = false;
10
+ executing = false;
11
+ planDir: string | undefined;
12
+ plan: PlanData | undefined;
13
+ executionStartIdx: number | undefined;
14
+ previousThinking: ThinkingLevel | undefined;
15
+ previousModel: { provider: string; id: string } | undefined;
16
+
17
+ persist(pi: ExtensionAPI): void {
18
+ pi.appendEntry<PersistedState>('plan-mode', {
19
+ planEnabled: this.planEnabled,
20
+ executing: this.executing,
21
+ planDir: this.planDir,
22
+ plan: this.plan,
23
+ executionStartIdx: this.executionStartIdx,
24
+ });
25
+ }
26
+
27
+ restore(entries: Array<{ type: string; customType?: string; data?: PersistedState }>): void {
28
+ const saved = entries
29
+ .filter((e) => e.type === 'custom' && e.customType === 'plan-mode')
30
+ .pop();
31
+ if (saved?.data) {
32
+ this.planEnabled = saved.data.planEnabled ?? this.planEnabled;
33
+ this.executing = saved.data.executing ?? this.executing;
34
+ this.planDir = saved.data.planDir ?? this.planDir;
35
+ this.plan = saved.data.plan ?? this.plan;
36
+ this.executionStartIdx = saved.data.executionStartIdx ?? this.executionStartIdx;
37
+ }
38
+ }
39
+
40
+ reset(): void {
41
+ this.planEnabled = false;
42
+ this.executing = false;
43
+ this.planDir = undefined;
44
+ this.plan = undefined;
45
+ this.executionStartIdx = undefined;
46
+ }
47
+ }
@@ -10,6 +10,7 @@ import { Text } from '@earendil-works/pi-tui';
10
10
  import { Type } from 'typebox';
11
11
  import { mkdir, writeFile } from 'node:fs/promises';
12
12
  import { readPlansJson, serializePlansJson } from '../plans-json.js';
13
+ import { saveHandoff } from '../plan-storage.js';
13
14
  import { toKebabCase } from '../utils.js';
14
15
  import type { PlanData, PlanStep } from '../types.js';
15
16
 
@@ -22,24 +23,24 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
22
23
  name: 'submit_plan',
23
24
  label: 'Submit Plan',
24
25
  description:
25
- 'Submit a structured plan with a title, context, numbered steps, and risks. ' +
26
+ 'Submit a structured plan with a title, handoff document, and numbered steps. ' +
26
27
  'Each step has a short description (for progress display) and detailed implementation instructions. ' +
27
- 'This finalizes the plan and writes it to .plans/<name>/plan.json.',
28
+ 'This finalizes the plan, writes .plans/<name>/plan.json and .plans/<name>/HANDOFF.md.',
28
29
  promptSnippet:
29
- 'Submit a structured plan with title, context, steps (description + details), and risks',
30
+ 'Submit a structured plan with title, handoff (what/why/context), and steps',
30
31
  promptGuidelines: [
31
32
  'Call submit_plan once when you have finished analyzing the codebase and are ready to finalize your plan.',
32
33
  'Each submit_plan step description should be a short label (≤60 chars). Put full implementation instructions in the details field.',
33
- 'The submit_plan context field must include all codebase findings, relevant file paths, APIs, patterns, and constraints so an executor with zero prior context can implement the plan.',
34
+ 'The submit_plan handoff field is a markdown document explaining what change is being made, why it matters, and all relevant codebase context (file paths, APIs, patterns, constraints). It must be thorough enough that both a human reviewer and an executor agent with zero prior context can understand the plan.',
34
35
  ],
35
36
  parameters: Type.Object({
36
37
  name: Type.String({
37
38
  description: 'Short kebab-case name for the plan (e.g. "add-auth-middleware")',
38
39
  }),
39
40
  title: Type.String({ description: 'Human-readable plan title' }),
40
- context: Type.String({
41
+ handoff: Type.String({
41
42
  description:
42
- 'Complete codebase context: relevant file paths, APIs, patterns, constraints, and gotchas',
43
+ 'Markdown content for HANDOFF.md explains what change is being made, why, and includes relevant codebase context for the executor',
43
44
  }),
44
45
  steps: Type.Array(
45
46
  Type.Object({
@@ -52,7 +53,6 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
52
53
  }),
53
54
  { minItems: 1 },
54
55
  ),
55
- risks: Type.String({ description: 'Open questions, assumptions, and risks' }),
56
56
  }),
57
57
 
58
58
  async execute(_toolCallId, params) {
@@ -67,14 +67,14 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
67
67
 
68
68
  const plan: PlanData = {
69
69
  title: params.title,
70
- context: params.context,
70
+ handoff: params.handoff,
71
71
  steps,
72
- risks: params.risks,
73
72
  };
74
73
 
75
- // Write plan.json
74
+ // Write plan.json and HANDOFF.md
76
75
  await mkdir(planDir, { recursive: true });
77
76
  await writeFile(`${planDir}/plan.json`, JSON.stringify(plan, null, 2) + '\n', 'utf-8');
77
+ await saveHandoff(planDir, params.handoff);
78
78
 
79
79
  // Update plans.json manifest
80
80
  const manifest = await readPlansJson();
@@ -132,12 +132,6 @@ export function registerSubmitPlanTool(pi: ExtensionAPI, callbacks: SubmitPlanCa
132
132
  lines.push(` ${num} ${step.description}`);
133
133
  }
134
134
 
135
- // Risks
136
- if (plan.risks) {
137
- lines.push('');
138
- lines.push(theme.fg('warning', '⚠ Risks: ') + theme.fg('dim', plan.risks));
139
- }
140
-
141
135
  return new Text(lines.join('\n'), 0, 0);
142
136
  },
143
137
  });
@@ -27,6 +27,7 @@ export function registerUpdateStepTool(pi: ExtensionAPI, callbacks: UpdateStepCa
27
27
  promptSnippet: 'Mark a plan step as done, skipped, or blocked',
28
28
  promptGuidelines: [
29
29
  'Call update_step after completing each plan step to mark it done before moving to the next.',
30
+ 'Always include notes when calling update_step — summarize what was done (files changed, key decisions made) for done steps, why it was unnecessary for skipped steps, or what went wrong for blocked steps.',
30
31
  'Use update_step with status "skipped" if a step is unnecessary after inspecting the code.',
31
32
  'Use update_step with status "blocked" and explain the reason in notes if a step cannot be completed — execution will pause for user input.',
32
33
  ],
@@ -103,6 +104,8 @@ export function registerUpdateStepTool(pi: ExtensionAPI, callbacks: UpdateStepCa
103
104
  return {
104
105
  content: [{ type: 'text' as const, text }],
105
106
  details,
107
+ // Stop the agent when all steps are done so agent_end fires immediately
108
+ terminate: !next,
106
109
  };
107
110
  },
108
111
 
@@ -11,9 +11,15 @@ export interface PlanStep {
11
11
 
12
12
  export interface PlanData {
13
13
  title: string;
14
- context: string;
14
+ handoff: string;
15
15
  steps: PlanStep[];
16
- risks: string;
16
+ }
17
+
18
+ export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
19
+
20
+ export interface ExecPendingConfig {
21
+ model: { provider: string; id: string };
22
+ thinking: string;
17
23
  }
18
24
 
19
25
  export interface PersistedState {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Plan mode UI — status bar and step widget rendering.
3
+ */
4
+
5
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
6
+ import type { PlanModeState } from './state.js';
7
+
8
+ export function updateUI(state: PlanModeState, ctx: ExtensionContext): void {
9
+ const { theme } = ctx.ui;
10
+
11
+ if (state.executing && state.plan) {
12
+ const done = state.plan.steps.filter((s) => s.status === 'done').length;
13
+ const total = state.plan.steps.length;
14
+ ctx.ui.setStatus('plan-mode', theme.fg('accent', `📋 exec ${done}/${total}`));
15
+ } else if (state.planEnabled) {
16
+ ctx.ui.setStatus('plan-mode', theme.fg('warning', '📝 plan'));
17
+ } else {
18
+ ctx.ui.setStatus('plan-mode', undefined);
19
+ }
20
+
21
+ if (state.executing && state.plan) {
22
+ const lines = state.plan.steps.map((step, i) => {
23
+ const num = `${i + 1}. `;
24
+ switch (step.status) {
25
+ case 'done':
26
+ return theme.fg('success', '✓ ') + theme.fg('muted', theme.strikethrough(num + step.description));
27
+ case 'skipped':
28
+ return theme.fg('warning', '⊘ ') + theme.fg('muted', theme.strikethrough(num + step.description));
29
+ case 'blocked':
30
+ return theme.fg('error', '✗ ') + theme.fg('error', num + step.description);
31
+ default:
32
+ return theme.fg('muted', '☐ ') + (num + step.description);
33
+ }
34
+ });
35
+ ctx.ui.setWidget('plan-todos', lines);
36
+ } else {
37
+ ctx.ui.setWidget('plan-todos', undefined);
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreki-gg/pi-plan-mode",
3
- "version": "0.7.2",
3
+ "version": "0.13.0",
4
4
  "description": "Two-phase planning workflow for pi — plan with claude-opus-4-6:medium, execute with gpt-5.5:low, with .plans/ file-based handoff",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -18,6 +18,7 @@
18
18
  },
19
19
  "files": [
20
20
  "extensions",
21
+ "skills",
21
22
  "bin",
22
23
  "README.md",
23
24
  "CHANGELOG.md",
@@ -27,11 +28,15 @@
27
28
  "typecheck": "tsc --noEmit",
28
29
  "lint": "oxlint extensions bin",
29
30
  "format": "oxfmt --write extensions bin",
30
- "format:check": "oxfmt --check extensions bin"
31
+ "format:check": "oxfmt --check extensions bin",
32
+ "test": "bun test extensions/plan-mode/__tests__"
31
33
  },
32
34
  "pi": {
33
35
  "extensions": [
34
36
  "./extensions/plan-mode"
37
+ ],
38
+ "skills": [
39
+ "./skills"
35
40
  ]
36
41
  },
37
42
  "dependencies": {