@janvitos/pi-plan-build 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +5 -5
  2. package/index.ts +45 -15
  3. package/package.json +2 -2
  4. package/utils.ts +15 -1
package/README.md CHANGED
@@ -25,7 +25,7 @@ A global [Pi coding agent](https://github.com/badlogic/pi-mono) extension that a
25
25
 
26
26
  ## Requirements
27
27
 
28
- - Pi `0.84.1` or newer
28
+ - Pi `0.84.2` or newer
29
29
  - Node.js `22.6` or newer for the test command
30
30
  - TUI or RPC UI support for interactive questions and approval dialogs
31
31
 
@@ -62,7 +62,7 @@ Do not install more than one npm, Git, or local copy at the same time; duplicate
62
62
  | `/plan` | Select Plan mode |
63
63
  | `/build` | Select Build mode |
64
64
  | `pi --plan` | Start a new session in Plan mode |
65
- | `/build-fresh` | Confirm a pending clean-session implementation |
65
+ | `/build-fresh` | Start a pending clean-session implementation manually |
66
66
 
67
67
  The agent may also enter Plan mode with `plan_enter` when planning or investigation is safer than immediate execution.
68
68
 
@@ -71,10 +71,10 @@ The agent may also enter Plan mode with `plan_enter` when planning or investigat
71
71
  When planning is complete, `plan_exit` displays the entire persisted plan and asks whether to:
72
72
 
73
73
  1. implement in the current session;
74
- 2. prepare a clean linked implementation session; or
74
+ 2. start a clean linked implementation session; or
75
75
  3. stay in Plan mode.
76
76
 
77
- Selecting **Start fresh and implement** stops the current run and pre-fills `/build-fresh`. Press Enter to confirm. Pi only exposes session creation to user-invoked command contexts, so this confirmation is required. The command creates a linked child session, copies the approved plan to its canonical plan file, switches it to Build, and starts implementation without transferring the planning conversation.
77
+ Selecting **Start fresh and implement** stops the current run and automatically dispatches `/build-fresh`. Pi 0.84.2 or newer is required for extension command dispatch from an injected user message. The command creates a linked child session, copies the approved plan to its canonical plan file, preserves the model and thinking level selected for the action, switches it to Build, and starts implementation without transferring the planning conversation.
78
78
 
79
79
  Selecting **Stay in Plan mode**, or pressing Escape while the approval dialog is open, displays:
80
80
 
@@ -111,7 +111,7 @@ npm test
111
111
  npm pack --dry-run
112
112
  ```
113
113
 
114
- The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, mode and provider rendering, session-based prompt history restoration, complete plan rendering, approval decisions, stop behavior, fresh-session handoff content, and question formatting.
114
+ The tests cover state decoding, safe plan paths, mutation restrictions, deferred transitions, mode and provider rendering, session-based prompt history restoration, complete plan rendering, approval decisions, stop behavior, fresh-session settings and handoff content, and question formatting.
115
115
 
116
116
  ### Publishing
117
117
 
package/index.ts CHANGED
@@ -14,10 +14,12 @@ import {
14
14
  import {
15
15
  applyManualSelection,
16
16
  buildFreshImplementationHandoff,
17
+ buildFreshImplementationRequest,
17
18
  buildPlanExitFreshResult,
18
19
  buildPlanExitStayResult,
19
20
  buildPlanReviewMessage,
20
21
  classifyPlanExitChoice,
22
+ type FreshImplementationRequest,
21
23
  decodeModeState,
22
24
  extractPromptHistory,
23
25
  formatFooterCwd,
@@ -73,7 +75,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
73
75
  let toolsBeforeModes: string[] = [];
74
76
  let currentContext: ExtensionContext | undefined;
75
77
  let requestEditorRender: (() => void) | undefined;
76
- let freshImplementationPlan: string | undefined;
78
+ let freshImplementationRequest: FreshImplementationRequest | undefined;
77
79
 
78
80
  pi.registerFlag("plan", {
79
81
  description: "Start in Plan mode",
@@ -160,28 +162,47 @@ export default function planBuildModes(pi: ExtensionAPI): void {
160
162
  pi.registerCommand("build-fresh", {
161
163
  description: "Start a clean linked session and implement the plan selected in plan_exit",
162
164
  handler: async (_args, ctx) => {
163
- const plan = freshImplementationPlan;
164
- if (!plan) {
165
+ const request = freshImplementationRequest;
166
+ if (!request) {
165
167
  ctx.ui.notify("No fresh implementation is pending. Choose ‘Start fresh and implement’ from plan_exit first.", "warning");
166
168
  return;
167
169
  }
168
170
  if (selectedMode !== "plan") {
169
- freshImplementationPlan = undefined;
171
+ freshImplementationRequest = undefined;
170
172
  ctx.ui.notify("Fresh implementation is no longer available because Plan mode is not active.", "warning");
171
173
  return;
172
174
  }
173
175
  if (ctx.mode === "print" || ctx.mode === "json") {
174
176
  throw new Error("Fresh implementation requires TUI or RPC mode");
175
177
  }
176
- if (!ctx.model) {
178
+ if (!request.model) {
177
179
  ctx.ui.notify("Cannot start implementation because no model is selected.", "warning");
178
180
  return;
179
181
  }
182
+ const currentModel = ctx.model;
183
+ const implementationModel = ctx.modelRegistry.find(request.model.provider, request.model.id)
184
+ ?? (currentModel?.provider === request.model.provider && currentModel.id === request.model.id ? currentModel : undefined);
185
+ if (!implementationModel) {
186
+ ctx.ui.notify(`Cannot start implementation because ${request.model.provider}/${request.model.id} is unavailable.`, "warning");
187
+ return;
188
+ }
189
+ try {
190
+ const modelSelected = await pi.setModel(implementationModel);
191
+ if (modelSelected === false) {
192
+ ctx.ui.notify(`Cannot start implementation because no API key is available for ${request.model.provider}/${request.model.id}.`, "warning");
193
+ return;
194
+ }
195
+ pi.setThinkingLevel(request.thinkingLevel);
196
+ } catch (error: unknown) {
197
+ const detail = error instanceof Error ? error.message : String(error);
198
+ ctx.ui.notify(`Cannot start implementation with ${request.model.provider}/${request.model.id}: ${detail}`, "warning");
199
+ return;
200
+ }
180
201
 
181
- freshImplementationPlan = undefined;
202
+ freshImplementationRequest = undefined;
182
203
  const parentSession = ctx.sessionManager.getSessionFile();
183
204
  const sourceTools = [...toolsBeforeModes];
184
- const handoff = buildFreshImplementationHandoff(plan);
205
+ const handoff = buildFreshImplementationHandoff(request.plan);
185
206
  let destinationPlanPath = "";
186
207
  let setupError: string | undefined;
187
208
  let kickoffError: string | undefined;
@@ -195,7 +216,9 @@ export default function planBuildModes(pi: ExtensionAPI): void {
195
216
  sessionManager.getSessionId(),
196
217
  );
197
218
  await fs.promises.mkdir(path.dirname(destinationPlanPath), { recursive: true });
198
- await fs.promises.writeFile(destinationPlanPath, plan, "utf8");
219
+ await fs.promises.writeFile(destinationPlanPath, request.plan, "utf8");
220
+ sessionManager.appendModelChange(request.model.provider, request.model.id);
221
+ sessionManager.appendThinkingLevelChange(request.thinkingLevel);
199
222
  sessionManager.appendCustomEntry(STATE_TYPE, {
200
223
  version: 1,
201
224
  selectedMode: "build",
@@ -232,11 +255,11 @@ export default function planBuildModes(pi: ExtensionAPI): void {
232
255
  },
233
256
  });
234
257
  if (result.cancelled) {
235
- freshImplementationPlan = plan;
258
+ freshImplementationRequest = request;
236
259
  ctx.ui.notify("Fresh implementation cancelled; the source plan remains available.", "info");
237
260
  }
238
261
  } catch (error: unknown) {
239
- freshImplementationPlan = plan;
262
+ freshImplementationRequest = request;
240
263
  const detail = error instanceof Error ? error.message : String(error);
241
264
  try {
242
265
  ctx.ui.notify(`Unable to start a fresh implementation session: ${detail}`, "error");
@@ -296,16 +319,23 @@ export default function planBuildModes(pi: ExtensionAPI): void {
296
319
  ));
297
320
  const decision = classifyPlanExitChoice(selection.choice);
298
321
  if (decision === "stay") {
299
- freshImplementationPlan = undefined;
322
+ freshImplementationRequest = undefined;
300
323
  pi.appendEntry(MODE_NOTICE_ENTRY_TYPE, { message: PLAN_EXIT_STAY_ACKNOWLEDGEMENT });
301
324
  return buildPlanExitStayResult(planPath, selection.cancelled);
302
325
  }
303
326
  if (decision === "implement-fresh") {
304
- freshImplementationPlan = plan;
305
- ctx.ui.setEditorText("/build-fresh");
327
+ freshImplementationRequest = buildFreshImplementationRequest(
328
+ plan,
329
+ ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
330
+ pi.getThinkingLevel(),
331
+ );
332
+ pi.sendUserMessage("/build-fresh", {
333
+ deliverAs: "followUp",
334
+ expandPromptTemplates: true,
335
+ });
306
336
  return buildPlanExitFreshResult(planPath);
307
337
  }
308
- freshImplementationPlan = undefined;
338
+ freshImplementationRequest = undefined;
309
339
  await selectMode("build", ctx, "tool");
310
340
  return {
311
341
  content: [
@@ -324,7 +354,7 @@ export default function planBuildModes(pi: ExtensionAPI): void {
324
354
  const details = result.details as { approved?: boolean; action?: string } | undefined;
325
355
  if (details?.action === "implement-fresh" && !context.isError) {
326
356
  return new Text(
327
- theme.fg("success", "Clean-session implementation selected — press Enter to run /build-fresh."),
357
+ theme.fg("success", "Clean-session implementation selected — starting automatically."),
328
358
  0,
329
359
  0,
330
360
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janvitos/pi-plan-build",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Plan safely, approve explicitly, then implement here or in a clean session.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "prepublishOnly": "npm test"
34
34
  },
35
35
  "peerDependencies": {
36
- "@earendil-works/pi-coding-agent": "*",
36
+ "@earendil-works/pi-coding-agent": ">=0.84.2",
37
37
  "@earendil-works/pi-tui": "*",
38
38
  "typebox": "*"
39
39
  },
package/utils.ts CHANGED
@@ -166,7 +166,7 @@ export function buildPlanExitFreshResult(planPath: string) {
166
166
  content: [
167
167
  {
168
168
  type: "text" as const,
169
- text: "The user selected clean-session implementation. Stop now; the /build-fresh command has been prepared for the user to submit.",
169
+ text: "The user selected clean-session implementation. Stop now; /build-fresh is starting automatically.",
170
170
  },
171
171
  ],
172
172
  details: { approved: true, action: "implement-fresh" as const, mode: "plan" as const, planPath },
@@ -174,6 +174,20 @@ export function buildPlanExitFreshResult(planPath: string) {
174
174
  };
175
175
  }
176
176
 
177
+ export interface FreshImplementationRequest {
178
+ plan: string;
179
+ model?: { provider: string; id: string };
180
+ thinkingLevel: string;
181
+ }
182
+
183
+ export function buildFreshImplementationRequest(
184
+ plan: string,
185
+ model: { provider: string; id: string } | undefined,
186
+ thinkingLevel: string,
187
+ ): FreshImplementationRequest {
188
+ return { plan, model, thinkingLevel };
189
+ }
190
+
177
191
  export function buildFreshImplementationHandoff(plan: string): string {
178
192
  return `Plan mode is now disabled. Full tool access is restored. Implement this approved plan now:\n\n${plan}`;
179
193
  }