lsd-pi 1.1.10 → 1.2.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.
Files changed (49) hide show
  1. package/dist/resources/extensions/slash-commands/index.js +2 -0
  2. package/dist/resources/extensions/slash-commands/init.js +47 -0
  3. package/dist/resources/extensions/slash-commands/plan.js +231 -50
  4. package/dist/resources/extensions/slash-commands/tools.js +14 -27
  5. package/dist/resources/extensions/subagent/index.js +5 -10
  6. package/package.json +1 -1
  7. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  8. package/packages/pi-coding-agent/dist/core/agent-session.js +11 -5
  9. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  10. package/packages/pi-coding-agent/dist/core/resource-loader-lsd-md.test.js +59 -7
  11. package/packages/pi-coding-agent/dist/core/resource-loader-lsd-md.test.js.map +1 -1
  12. package/packages/pi-coding-agent/dist/core/resource-loader.js +4 -4
  13. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  14. package/packages/pi-coding-agent/dist/core/sdk.d.ts +1 -1
  15. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  16. package/packages/pi-coding-agent/dist/core/sdk.js +18 -7
  17. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  18. package/packages/pi-coding-agent/dist/core/sdk.test.js +80 -0
  19. package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -1
  20. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +12 -5
  21. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  22. package/packages/pi-coding-agent/dist/core/settings-manager.js +23 -9
  23. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  24. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +8 -4
  25. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  26. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +32 -5
  27. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  28. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  29. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +8 -0
  30. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  31. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  32. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +34 -25
  33. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  34. package/packages/pi-coding-agent/package.json +1 -1
  35. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -5
  36. package/packages/pi-coding-agent/src/core/resource-loader-lsd-md.test.ts +67 -7
  37. package/packages/pi-coding-agent/src/core/resource-loader.ts +4 -4
  38. package/packages/pi-coding-agent/src/core/sdk.test.ts +100 -0
  39. package/packages/pi-coding-agent/src/core/sdk.ts +23 -8
  40. package/packages/pi-coding-agent/src/core/settings-manager.ts +36 -15
  41. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +41 -10
  42. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +11 -0
  43. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +43 -27
  44. package/pkg/package.json +1 -1
  45. package/src/resources/extensions/slash-commands/index.ts +2 -0
  46. package/src/resources/extensions/slash-commands/init.ts +55 -0
  47. package/src/resources/extensions/slash-commands/plan.ts +268 -52
  48. package/src/resources/extensions/slash-commands/tools.ts +15 -29
  49. package/src/resources/extensions/subagent/index.ts +5 -10
@@ -14,6 +14,7 @@ import { join } from "node:path";
14
14
  const PLAN_ENTRY_TYPE = "plan-mode-state";
15
15
  const PLAN_APPROVAL_ACTION_QUESTION_ID = "plan_mode_approval_action";
16
16
  const PLAN_APPROVAL_PERMISSION_QUESTION_ID = "plan_mode_approval_permission";
17
+ const PLAN_SUGGEST_QUESTION_ID = "plan_mode_suggest_switch";
17
18
  const PLAN_DIR_RE = /(^|[/\\])\.(?:lsd|gsd)[/\\]plan([/\\]|$)/;
18
19
  const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.(?:lsd|gsd)(?:[\\/]+plan)?|rtk\s)/;
19
20
  const SAFE_TOOLS = new Set([
@@ -73,10 +74,12 @@ const APPROVE_AUTO_LABEL = "Auto mode";
73
74
  const APPROVE_BYPASS_LABEL = "Bypass mode";
74
75
  const APPROVE_AUTO_SUBAGENT_LABEL = "Execute with subagent in auto mode";
75
76
  const APPROVE_BYPASS_SUBAGENT_LABEL = "Execute with subagent in bypass mode";
77
+ const APPROVE_NEW_SESSION_LABEL = "New session with coding model"; // shown in second question when autoSwitchPlanModel is on
76
78
  const REVIEW_LABEL = "Let other agent review";
77
79
  const REVISE_LABEL = "Revise plan";
78
80
  const CANCEL_LABEL = "Cancel";
79
81
  const DEFAULT_PLAN_REVIEW_AGENT = "generic";
82
+ const DEFAULT_PLAN_CODING_AGENT = "worker";
80
83
 
81
84
  type PlanApprovalStatus = "pending" | "approved" | "revising" | "cancelled";
82
85
  type RestorablePermissionMode = Exclude<PermissionMode, "plan">;
@@ -109,6 +112,7 @@ const INITIAL_STATE: PlanModeState = {
109
112
 
110
113
  let state: PlanModeState = { ...INITIAL_STATE };
111
114
  let startedFromFlag = false;
115
+ let reasoningModelSwitchDone = false;
112
116
 
113
117
  function isPlanModeActive(): boolean {
114
118
  return getPermissionMode() === "plan";
@@ -125,7 +129,13 @@ function parseQualifiedModelRef(value: unknown): ModelRef | undefined {
125
129
  return { provider, id };
126
130
  }
127
131
 
128
- function readPlanModeSettings(): { reasoningModel?: string; reviewModel?: string; codingModel?: string } {
132
+ function parseSubagentName(value: unknown): string | undefined {
133
+ if (typeof value !== "string") return undefined;
134
+ const trimmed = value.trim();
135
+ return trimmed.length > 0 ? trimmed : undefined;
136
+ }
137
+
138
+ function readPlanModeSettings(): { reasoningModel?: string; reviewModel?: string; codingModel?: string; codingSubagent?: string } {
129
139
  try {
130
140
  const settingsPath = join(getAgentDir(), "settings.json");
131
141
  if (!existsSync(settingsPath)) return {};
@@ -134,14 +144,19 @@ function readPlanModeSettings(): { reasoningModel?: string; reviewModel?: string
134
144
  planModeReasoningModel?: unknown;
135
145
  planModeReviewModel?: unknown;
136
146
  planModeCodingModel?: unknown;
147
+ planModeCodingSubagent?: unknown;
148
+ planModeCodingAgent?: unknown;
137
149
  };
138
150
  const reasoningModel = parseQualifiedModelRef(parsed.planModeReasoningModel);
139
151
  const reviewModel = parseQualifiedModelRef(parsed.planModeReviewModel);
140
152
  const codingModel = parseQualifiedModelRef(parsed.planModeCodingModel);
153
+ const codingSubagent = parseSubagentName(parsed.planModeCodingSubagent)
154
+ ?? parseSubagentName(parsed.planModeCodingAgent);
141
155
  return {
142
156
  reasoningModel: reasoningModel ? `${reasoningModel.provider}/${reasoningModel.id}` : undefined,
143
157
  reviewModel: reviewModel ? `${reviewModel.provider}/${reviewModel.id}` : undefined,
144
158
  codingModel: codingModel ? `${codingModel.provider}/${codingModel.id}` : undefined,
159
+ codingSubagent,
145
160
  };
146
161
  } catch {
147
162
  return {};
@@ -160,6 +175,42 @@ export function readPlanModeCodingModel(): string | undefined {
160
175
  return readPlanModeSettings().codingModel;
161
176
  }
162
177
 
178
+ export function readPlanModeCodingSubagent(): string | undefined {
179
+ return readPlanModeSettings().codingSubagent;
180
+ }
181
+
182
+ function readAutoSuggestPlanModeSetting(): boolean {
183
+ try {
184
+ const settingsPath = join(getAgentDir(), "settings.json");
185
+ if (!existsSync(settingsPath)) return false;
186
+ const raw = readFileSync(settingsPath, "utf-8");
187
+ const parsed = JSON.parse(raw) as { autoSuggestPlanMode?: unknown };
188
+ return parsed.autoSuggestPlanMode === true;
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+
194
+ function readAutoSwitchPlanModelSetting(): boolean {
195
+ try {
196
+ const settingsPath = join(getAgentDir(), "settings.json");
197
+ if (!existsSync(settingsPath)) return false;
198
+ const raw = readFileSync(settingsPath, "utf-8");
199
+ const parsed = JSON.parse(raw) as { autoSwitchPlanModel?: unknown };
200
+ return parsed.autoSwitchPlanModel === true;
201
+ } catch {
202
+ return false;
203
+ }
204
+ }
205
+
206
+ function buildAutoSuggestPlanModeSystemPrompt(): string {
207
+ return [
208
+ `Plan-mode suggestion: if the user's latest request describes a large, multi-step, or ambiguous task — e.g. a refactor, multi-file change, new feature, migration, or anything that benefits from upfront investigation — proactively ask whether to switch to plan mode before making any edits.`,
209
+ `How to suggest: call ask_user_questions with a single question. Set the question id to exactly "${PLAN_SUGGEST_QUESTION_ID}". Ask: "This looks like a complex task. Would you like to switch to plan mode first?". Provide exactly two options: "Yes, switch to plan mode" (recommended) and "No, proceed directly". Do NOT call /plan yourself — wait for the user answer and the system will handle switching automatically.`,
210
+ "Do not suggest plan mode for simple, single-file, or read-only tasks. Do not suggest it if the user is already in plan mode or in the middle of an implementation. Only suggest it once per distinct task.",
211
+ ].join(" ");
212
+ }
213
+
163
214
  function sameModel(left: ModelRef | undefined, right: ModelRef | undefined): boolean {
164
215
  return !!left && !!right && left.provider === right.provider && left.id === right.id;
165
216
  }
@@ -217,6 +268,24 @@ function restoreStateFromSession(ctx: ExtensionCommandContext | any): void {
217
268
  }
218
269
  }
219
270
 
271
+ async function enablePlanModeWithModelSwitch(
272
+ pi: ExtensionAPI,
273
+ ctx: any,
274
+ currentModel: ModelRef | undefined,
275
+ next: Partial<Pick<PlanModeState, "task" | "latestPlanPath" | "approvalStatus" | "previousMode" | "preplanModel" | "targetPermissionMode">> = {},
276
+ ): Promise<void> {
277
+ enablePlanMode(pi, currentModel, next);
278
+ // Signal that before_agent_start should switch to the reasoning model on next turn
279
+ reasoningModelSwitchDone = false;
280
+ if (!readAutoSwitchPlanModelSetting()) return;
281
+ if (!readPlanModeReasoningModel()) {
282
+ ctx.ui?.notify?.(
283
+ "OpusPlan: set a Plan reasoning model in /settings to auto-switch on entry",
284
+ "info",
285
+ );
286
+ }
287
+ }
288
+
220
289
  function enablePlanMode(
221
290
  pi: ExtensionAPI,
222
291
  currentModel: ModelRef | undefined,
@@ -246,6 +315,7 @@ function leavePlanMode(
246
315
  nextPermissionMode: RestorablePermissionMode,
247
316
  clearTask = false,
248
317
  ): RestorablePermissionMode {
318
+ reasoningModelSwitchDone = false;
249
319
  setPermissionModeAndEnv(nextPermissionMode);
250
320
  setState(pi, {
251
321
  active: false,
@@ -282,9 +352,10 @@ function buildExecutionKickoffMessage(options: { permissionMode: RestorablePermi
282
352
  }
283
353
 
284
354
  const codingModel = readPlanModeCodingModel();
355
+ const codingSubagent = readPlanModeCodingSubagent() ?? DEFAULT_PLAN_CODING_AGENT;
285
356
  const agentInvocationInstruction = codingModel
286
- ? `Invoke the subagent tool with agent "worker" and model="${codingModel}" to implement the plan end-to-end.`
287
- : `Invoke the subagent tool with agent "worker" to implement the plan end-to-end.`;
357
+ ? `Invoke the subagent tool with exact parameters agent "${codingSubagent}" and model="${codingModel}" to implement the plan end-to-end.`
358
+ : `Invoke the subagent tool with exact parameter agent "${codingSubagent}" to implement the plan end-to-end.`;
288
359
 
289
360
  const details: string[] = [
290
361
  "Plan approved. Exit plan mode and execute the approved plan with a subagent now.",
@@ -297,6 +368,38 @@ function buildExecutionKickoffMessage(options: { permissionMode: RestorablePermi
297
368
  return details.join(" ");
298
369
  }
299
370
 
371
+ // Pending new-session payload — set before triggering the internal command
372
+ interface PendingNewSession {
373
+ codingModelRef: ModelRef | undefined;
374
+ codingSubagent: string;
375
+ planPath: string | undefined;
376
+ planContent: string | undefined;
377
+ task: string;
378
+ }
379
+ let pendingNewSession: PendingNewSession | null = null;
380
+
381
+ function scheduleNewSession(pi: ExtensionAPI, ctx: any): void {
382
+ const codingModelRef = parseQualifiedModelRef(readPlanModeCodingModel());
383
+ const codingSubagent = readPlanModeCodingSubagent() ?? DEFAULT_PLAN_CODING_AGENT;
384
+ const planPath = state.latestPlanPath;
385
+ const planContent = planPath ? readPlanArtifact(planPath) : undefined;
386
+
387
+ pendingNewSession = {
388
+ codingModelRef,
389
+ codingSubagent,
390
+ planPath,
391
+ planContent,
392
+ task: state.task,
393
+ };
394
+
395
+ leavePlanMode(pi, "approved", "auto");
396
+ ctx.ui?.notify?.("Plan approved. Starting new session…", "info");
397
+
398
+ // Trigger the internal command which has ExtensionCommandContext (ctx.newSession available).
399
+ // Must use the /prefix so tryExecuteExtensionCommand parses the name correctly.
400
+ pi.executeSlashCommand("/plan-execute-new-session");
401
+ }
402
+
300
403
  async function approvePlan(
301
404
  pi: ExtensionAPI,
302
405
  ctx: any,
@@ -313,7 +416,13 @@ async function approvePlan(
313
416
  targetPermissionMode: permissionMode,
314
417
  };
315
418
  leavePlanMode(pi, "approved", permissionMode);
316
- await pi.sendUserMessage(buildExecutionKickoffMessage({ permissionMode, executeWithSubagent }), { deliverAs: "followUp" });
419
+ // Deliver the kickoff as a steering message so it is injected BEFORE the LLM
420
+ // produces its next assistant turn. Using "followUp" would defer delivery
421
+ // until the agent has no more tool calls, which lets the LLM call the
422
+ // subagent tool with the default session model BEFORE it ever sees the
423
+ // explicit model="<planModeCodingModel>" instruction. Steering ensures the
424
+ // configured plan-mode coding model reaches the subagent invocation.
425
+ await pi.sendUserMessage(buildExecutionKickoffMessage({ permissionMode, executeWithSubagent }), { deliverAs: "steer" });
317
426
  }
318
427
 
319
428
  async function cancelPlan(pi: ExtensionAPI, ctx: any, clearTask = true): Promise<RestorablePermissionMode> {
@@ -346,35 +455,52 @@ function readPlanArtifact(planPath: string): string | undefined {
346
455
  }
347
456
  }
348
457
 
349
- function buildApprovalDialogInstructions(): string {
458
+ function buildNewSessionOptionLabel(): string {
459
+ const codingModel = readPlanModeCodingModel();
460
+ const codingSubagent = readPlanModeCodingSubagent() ?? DEFAULT_PLAN_CODING_AGENT;
461
+ const modelSuffix = codingModel ? codingModel.split("/")[1] ?? codingModel : null;
462
+ // e.g. "Approve plan — new session (worker · claude-sonnet-4-6)"
463
+ // "Approve plan — new session (worker)"
464
+ const suffix = modelSuffix ? `${codingSubagent} · ${modelSuffix}` : codingSubagent;
465
+ return `${APPROVE_LABEL} — ${APPROVE_NEW_SESSION_LABEL} (${suffix})`;
466
+ }
467
+
468
+ function buildApprovalActionInstructions(): string {
350
469
  return [
351
- "Present approval options now using ask_user_questions with exactly two single-select questions.",
352
- `First question id: \"${PLAN_APPROVAL_ACTION_QUESTION_ID}\". Ask what to do next with the plan.`,
353
- "Use exactly these 3 options for the first question:",
354
- `1. ${APPROVE_LABEL} (Recommended)`,
355
- `2. ${REVIEW_LABEL}`,
356
- `3. ${REVISE_LABEL}`,
357
- `Second question id: \"${PLAN_APPROVAL_PERMISSION_QUESTION_ID}\". Ask which execution mode to use if the plan is approved.`,
358
- "Use exactly these 4 options for the second question:",
359
- `1. ${APPROVE_AUTO_LABEL} (Recommended)`,
360
- `2. ${APPROVE_BYPASS_LABEL}`,
361
- `3. ${APPROVE_AUTO_SUBAGENT_LABEL}`,
362
- `4. ${APPROVE_BYPASS_SUBAGENT_LABEL}`,
363
- `Do not include \"${CANCEL_LABEL}\" as an explicit option. If the user wants to cancel, they should choose \"None of the above\" on the first question and type \"${CANCEL_LABEL}\" in the free-text note.`,
364
- `If the user selects \"${REVIEW_LABEL}\" or \"${REVISE_LABEL}\", ignore the second answer for now.`,
365
- "If the dialog is dismissed or the user gives no answer, continue planning.",
470
+ "Ask for plan approval now using ask_user_questions.",
471
+ `One single-select question with id \"${PLAN_APPROVAL_ACTION_QUESTION_ID}\". Ask what to do next with the plan.`,
472
+ `Options: ${APPROVE_LABEL}, ${REVIEW_LABEL}, ${REVISE_LABEL}.`,
473
+ `Do not include \"${CANCEL_LABEL}\" as an explicit option — if the user wants to cancel they should choose \"None of the above\" and type \"${CANCEL_LABEL}\" in the note.`,
474
+ "Do not restate the plan. Just show the question.",
366
475
  ].join(" ");
367
476
  }
368
477
 
478
+ function buildApprovalModeInstructions(): string {
479
+ const autoSwitchEnabled = readAutoSwitchPlanModelSetting();
480
+ const showNewSessionOption = autoSwitchEnabled;
481
+ const newSessionLabel = buildNewSessionOptionLabel();
482
+
483
+ const options = showNewSessionOption
484
+ ? `${APPROVE_AUTO_LABEL}, ${APPROVE_BYPASS_LABEL}, ${APPROVE_AUTO_SUBAGENT_LABEL}, ${APPROVE_BYPASS_SUBAGENT_LABEL}, ${newSessionLabel}`
485
+ : `${APPROVE_AUTO_LABEL}, ${APPROVE_BYPASS_LABEL}, ${APPROVE_AUTO_SUBAGENT_LABEL}, ${APPROVE_BYPASS_SUBAGENT_LABEL}`;
486
+
487
+ return [
488
+ "Plan approved. Now ask which execution mode to use via ask_user_questions.",
489
+ `One single-select question with id \"${PLAN_APPROVAL_PERMISSION_QUESTION_ID}\".`,
490
+ `Options: ${options}.`,
491
+ ].join(" ");
492
+ }
493
+
494
+ // Keep for external callers that reference the combined form (headless path)
495
+ function buildApprovalDialogInstructions(): string {
496
+ return buildApprovalActionInstructions();
497
+ }
498
+
369
499
  function buildApprovalSteeringMessage(planPath: string): string {
370
- const details = [
500
+ return [
371
501
  `Plan artifact saved at ${planPath}.`,
372
- "Do not restate the plan in a normal assistant response.",
373
- "Ask for approval now via ask_user_questions.",
374
- buildApprovalDialogInstructions(),
375
- ];
376
-
377
- return details.join("\n\n");
502
+ buildApprovalActionInstructions(),
503
+ ].join("\n\n");
378
504
  }
379
505
 
380
506
  function buildPlanPreviewMessage(planPath: string, planMarkdown?: string): string {
@@ -420,7 +546,7 @@ function buildReviewSteeringMessage(planPath: string, planMarkdown?: string): st
420
546
 
421
547
  details.push(
422
548
  "After the subagent responds, summarize its feedback for the user, present the current plan again, and then ask for approval again.",
423
- buildApprovalDialogInstructions(),
549
+ buildApprovalActionInstructions(),
424
550
  );
425
551
 
426
552
  return details.join("\n\n");
@@ -478,6 +604,9 @@ export const __testing = {
478
604
  buildApprovalSteeringMessage,
479
605
  buildPlanPreviewMessage,
480
606
  buildReviewSteeringMessage,
607
+ buildAutoSuggestPlanModeSystemPrompt,
608
+ readAutoSuggestPlanModeSetting,
609
+ PLAN_SUGGEST_QUESTION_ID,
481
610
  };
482
611
 
483
612
  export default function planCommand(pi: ExtensionAPI) {
@@ -489,16 +618,28 @@ export default function planCommand(pi: ExtensionAPI) {
489
618
  pi.on("session_start", async (_event, ctx) => {
490
619
  restoreStateFromSession(ctx);
491
620
  startedFromFlag = false;
621
+ reasoningModelSwitchDone = false;
492
622
  if (state.active) {
493
623
  setPermissionModeAndEnv("plan");
494
624
  }
495
625
  });
496
626
 
497
- pi.on("before_agent_start", async () => {
498
- if (!isPlanModeActive()) return;
499
- return {
500
- systemPrompt: buildPlanModeSystemPrompt(),
501
- };
627
+ pi.on("before_agent_start", async (_event, ctx) => {
628
+ if (isPlanModeActive()) {
629
+ // Switch to reasoning model once per plan mode activation
630
+ if (!reasoningModelSwitchDone && readAutoSwitchPlanModelSetting()) {
631
+ const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
632
+ if (reasoningModel) {
633
+ reasoningModelSwitchDone = true;
634
+ await setModelIfNeeded(pi, ctx, reasoningModel);
635
+ }
636
+ }
637
+ return { systemPrompt: buildPlanModeSystemPrompt() };
638
+ }
639
+ if (readAutoSuggestPlanModeSetting()) {
640
+ return { systemPrompt: buildAutoSuggestPlanModeSystemPrompt() };
641
+ }
642
+ return;
502
643
  });
503
644
 
504
645
  pi.on("input", async (event, ctx) => {
@@ -509,7 +650,7 @@ export default function planCommand(pi: ExtensionAPI) {
509
650
 
510
651
  startedFromFlag = true;
511
652
  ensurePlanDir();
512
- enablePlanMode(pi, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
653
+ await enablePlanModeWithModelSwitch(pi, ctx, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
513
654
  task: event.text.trim(),
514
655
  approvalStatus: "pending",
515
656
  latestPlanPath: undefined,
@@ -577,6 +718,35 @@ export default function planCommand(pi: ExtensionAPI) {
577
718
  return;
578
719
  }
579
720
 
721
+ if (event.toolName === "ask_user_questions" && !isPlanModeActive()) {
722
+ const details = event.details as {
723
+ cancelled?: boolean;
724
+ response?: { answers?: Record<string, AskUserAnswer> };
725
+ } | undefined;
726
+ if (!details?.cancelled && details?.response?.answers) {
727
+ const suggestAnswer = details.response.answers[PLAN_SUGGEST_QUESTION_ID];
728
+ if (suggestAnswer) {
729
+ const selected = Array.isArray(suggestAnswer.selected) ? suggestAnswer.selected[0] : suggestAnswer.selected;
730
+ if (typeof selected === "string" && selected.toLowerCase().includes("yes")) {
731
+ ensurePlanDir();
732
+ await enablePlanModeWithModelSwitch(pi, ctx, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
733
+ task: state.task,
734
+ latestPlanPath: undefined,
735
+ approvalStatus: "pending",
736
+ targetPermissionMode: undefined,
737
+ });
738
+ ctx.ui?.notify?.("Plan mode enabled. Investigate and produce a plan before making changes.", "info");
739
+ pi.sendUserMessage(
740
+ "The user confirmed switching to plan mode. You are now in plan mode. Investigate the task and produce a persisted execution plan under .lsd/plan/ before making any source changes.",
741
+ { deliverAs: "steer" },
742
+ );
743
+ }
744
+ return;
745
+ }
746
+ }
747
+ return;
748
+ }
749
+
580
750
  if (!isPlanModeActive() || event.toolName !== "ask_user_questions") return;
581
751
 
582
752
  const details = event.details as {
@@ -590,35 +760,48 @@ export default function planCommand(pi: ExtensionAPI) {
590
760
  const actionValues = getAnswerValues(actionAnswer);
591
761
  const permissionValues = getAnswerValues(permissionAnswer);
592
762
 
593
- if (actionValues.length === 0 && permissionValues.length === 0) return;
594
-
595
- if (selectionRequestsCancel([...actionValues, ...permissionValues])) {
596
- await cancelPlan(pi, ctx, true);
597
- return;
598
- }
763
+ // ── Second question answered (execution mode) ─────────────────────────
764
+ if (permissionValues.length > 0) {
765
+ if (selectionRequestsCancel(permissionValues)) {
766
+ await cancelPlan(pi, ctx, true);
767
+ return;
768
+ }
599
769
 
600
- const actionSelection = actionValues[0];
601
- if (!actionSelection) return;
770
+ if (permissionValues[0]?.includes(APPROVE_NEW_SESSION_LABEL)) {
771
+ scheduleNewSession(pi, ctx);
772
+ return;
773
+ }
602
774
 
603
- if (actionSelection.includes(APPROVE_LABEL)) {
604
775
  const executionMode = approvalSelectionToExecutionMode(permissionValues[0]) ?? {
605
776
  permissionMode: DEFAULT_APPROVAL_PERMISSION_MODE,
606
777
  executeWithSubagent: false,
607
778
  };
608
- state = {
609
- ...state,
610
- targetPermissionMode: executionMode.permissionMode,
611
- };
612
-
779
+ state = { ...state, targetPermissionMode: executionMode.permissionMode };
613
780
  if (executionMode.executeWithSubagent) {
614
781
  const modeLabel = executionMode.permissionMode === "danger-full-access" ? "bypass" : "auto";
615
782
  ctx.ui?.notify?.(`Plan approved: subagent(${modeLabel})`, "info");
616
783
  }
617
-
618
784
  await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent);
619
785
  return;
620
786
  }
621
787
 
788
+ // ── First question answered (action) ──────────────────────────────────
789
+ if (actionValues.length === 0) return;
790
+
791
+ if (selectionRequestsCancel(actionValues)) {
792
+ await cancelPlan(pi, ctx, true);
793
+ return;
794
+ }
795
+
796
+ const actionSelection = actionValues[0];
797
+ if (!actionSelection) return;
798
+
799
+ if (actionSelection.includes(APPROVE_LABEL)) {
800
+ // Steer the second question — handle in the next tool_result cycle
801
+ pi.sendUserMessage(buildApprovalModeInstructions(), { deliverAs: "steer" });
802
+ return;
803
+ }
804
+
622
805
  if (actionSelection.includes(REVIEW_LABEL)) {
623
806
  setState(pi, {
624
807
  ...state,
@@ -660,16 +843,17 @@ export default function planCommand(pi: ExtensionAPI) {
660
843
 
661
844
  ensurePlanDir();
662
845
  const task = args.trim();
663
- enablePlanMode(pi, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
846
+ await enablePlanModeWithModelSwitch(pi, ctx, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
664
847
  task,
665
848
  latestPlanPath: undefined,
666
849
  approvalStatus: "pending",
667
850
  targetPermissionMode: undefined,
668
851
  });
852
+ const reasoningModel = readAutoSwitchPlanModelSetting() ? readPlanModeReasoningModel() : undefined;
669
853
  ctx.ui.notify(
670
854
  task
671
- ? `Plan mode enabled. Current task: ${task}`
672
- : "Plan mode enabled. Investigation is allowed; source changes stay blocked until you exit plan mode.",
855
+ ? `Plan mode enabled${reasoningModel ? ` · ${reasoningModel.split("/")[1] ?? reasoningModel}` : ""}. Current task: ${task}`
856
+ : `Plan mode enabled${reasoningModel ? ` · ${reasoningModel.split("/")[1] ?? reasoningModel}` : ""}. Investigation is allowed; source changes stay blocked until you exit plan mode.`,
673
857
  "info",
674
858
  );
675
859
  },
@@ -698,4 +882,36 @@ export default function planCommand(pi: ExtensionAPI) {
698
882
  ctx.ui.notify("Plan mode cancelled.", "info");
699
883
  },
700
884
  });
885
+
886
+ // Internal command — called by scheduleNewSession() via pi.executeSlashCommand().
887
+ // Runs in ExtensionCommandContext so ctx.newSession() is available.
888
+ pi.registerCommand("plan-execute-new-session", {
889
+ description: "Internal: execute approved plan in a new session with the coding model",
890
+ async handler(_args: string, ctx: ExtensionCommandContext) {
891
+ const payload = pendingNewSession;
892
+ pendingNewSession = null;
893
+ if (!payload) return;
894
+
895
+ // Switch to coding model first
896
+ if (payload.codingModelRef) {
897
+ await setModelIfNeeded(pi, ctx, payload.codingModelRef);
898
+ }
899
+
900
+ const result = await ctx.newSession();
901
+ if (result.cancelled) return;
902
+
903
+ // Inject plan into the new session as a steer message
904
+ const parts: string[] = [
905
+ `Plan approved. You are acting as the ${payload.codingSubagent} agent. Implement the following plan now without re-investigating or re-planning.`,
906
+ ];
907
+ if (payload.task) parts.push(`Original task: ${payload.task}`);
908
+ if (payload.planPath) parts.push(`Plan artifact: ${payload.planPath}`);
909
+ if (payload.planContent) {
910
+ parts.push(`Full plan:\n\`\`\`markdown\n${payload.planContent}\n\`\`\``);
911
+ } else if (payload.planPath) {
912
+ parts.push(`Read the plan from ${payload.planPath} before starting.`);
913
+ }
914
+ pi.sendUserMessage(parts.join("\n\n"), { deliverAs: "steer" });
915
+ },
916
+ });
701
917
  }
@@ -16,12 +16,6 @@ function isHashlineMode(activeToolNames: string[]): boolean {
16
16
  return activeToolNames.includes("hashline_read") || activeToolNames.includes("hashline_edit");
17
17
  }
18
18
 
19
- function getCoreToolNames(activeToolNames: string[]): string[] {
20
- return isHashlineMode(activeToolNames)
21
- ? ["hashline_read", "bash", "lsp", "tool_search", "tool_enable"]
22
- : ["read", "bash", "lsp", "tool_search", "tool_enable"];
23
- }
24
-
25
19
  function getBalancedToolNames(activeToolNames: string[]): string[] {
26
20
  return isHashlineMode(activeToolNames)
27
21
  ? [
@@ -36,6 +30,7 @@ function getBalancedToolNames(activeToolNames: string[]): string[] {
36
30
  "Skill",
37
31
  "subagent",
38
32
  "await_subagent",
33
+ "ask_user_questions",
39
34
  ]
40
35
  : [
41
36
  "read",
@@ -49,9 +44,14 @@ function getBalancedToolNames(activeToolNames: string[]): string[] {
49
44
  "Skill",
50
45
  "subagent",
51
46
  "await_subagent",
47
+ "ask_user_questions",
52
48
  ];
53
49
  }
54
50
 
51
+ function getFullToolNames(pi: ExtensionAPI): string[] {
52
+ return pi.getAllTools().map((tool) => tool.name).filter((name): name is string => Boolean(name));
53
+ }
54
+
55
55
  function scoreTool(query: string, tool: { name?: string; description?: string }): number {
56
56
  const name = (tool.name ?? "").toLowerCase();
57
57
  const description = (tool.description ?? "").toLowerCase();
@@ -164,46 +164,32 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
164
164
  });
165
165
 
166
166
  pi.registerCommand("tools", {
167
- description: "Toggle lazy tool-search mode",
167
+ description: "Manage default tool profiles",
168
168
  handler: async (args: string, _ctx: ExtensionCommandContext) => {
169
169
  const input = args.trim();
170
170
  const settings = getSettingsManager();
171
171
  const currentActive = pi.getActiveTools();
172
172
  const toolProfile = settings.getToolProfile();
173
- const toolSearchEnabled = toolProfile === "minimal";
174
173
 
175
174
  if (!input) {
176
175
  pi.sendMessage({
177
176
  customType: "tools:status",
178
177
  content: [
179
178
  `Tool profile: ${toolProfile}`,
180
- `Tool search mode: ${toolSearchEnabled ? "on" : "off"}`,
181
179
  `Active tools: ${currentActive.length}`,
182
180
  currentActive.length > 0 ? currentActive.join(", ") : "(none)",
183
181
  "",
184
182
  "Usage:",
185
- " /tools on Enable lazy tool-search mode and switch to a small core tool set",
186
- " /tools off Disable lazy tool-search mode and restore the balanced tool profile",
187
183
  " /tools balanced Switch to the balanced tool profile",
188
- " /tools minimal Switch to the minimal core tool profile",
184
+ " /tools full Switch to the full tool profile (all available tools)",
185
+ " /tools on Alias for /tools full",
186
+ " /tools off Alias for /tools balanced",
189
187
  ].join("\n"),
190
188
  display: true,
191
189
  });
192
190
  return;
193
191
  }
194
192
 
195
- if (["on", "enable", "mode on"].includes(input)) {
196
- settings.setToolProfile("minimal");
197
- const nextActive = getCoreToolNames(currentActive);
198
- pi.setActiveTools(nextActive);
199
- pi.sendMessage({
200
- customType: "tools:mode",
201
- content: `Tool search mode enabled. Active tools reduced to: ${pi.getActiveTools().join(", ")}`,
202
- display: true,
203
- });
204
- return;
205
- }
206
-
207
193
  if (["off", "disable", "mode off", "balanced", "default"].includes(input)) {
208
194
  settings.setToolProfile("balanced");
209
195
  const nextActive = getBalancedToolNames(currentActive);
@@ -216,13 +202,13 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
216
202
  return;
217
203
  }
218
204
 
219
- if (["minimal", "core"].includes(input)) {
220
- settings.setToolProfile("minimal");
221
- const nextActive = getCoreToolNames(currentActive);
205
+ if (["on", "enable", "mode on", "full", "all"].includes(input)) {
206
+ settings.setToolProfile("full");
207
+ const nextActive = getFullToolNames(pi);
222
208
  pi.setActiveTools(nextActive);
223
209
  pi.sendMessage({
224
210
  customType: "tools:mode",
225
- content: `Minimal tool profile active: ${pi.getActiveTools().join(", ")}`,
211
+ content: `Full tool profile active: ${pi.getActiveTools().join(", ")}`,
226
212
  display: true,
227
213
  });
228
214
  return;
@@ -230,7 +216,7 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
230
216
 
231
217
  pi.sendMessage({
232
218
  customType: "tools:help",
233
- content: `Unknown /tools subcommand: ${input}\n\nTry /tools, /tools on, /tools off, /tools balanced, or /tools minimal.`,
219
+ content: `Unknown /tools subcommand: ${input}\n\nTry /tools, /tools balanced, /tools full, /tools on, or /tools off.`,
234
220
  display: true,
235
221
  });
236
222
  },
@@ -710,7 +710,7 @@ const ChainItem = Type.Object({
710
710
  });
711
711
 
712
712
  const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
713
- description: 'Which agent directories to use. Default: "both" (user + project-local).',
713
+ description: 'Which agent directories to use. Default: "both".',
714
714
  default: "both",
715
715
  });
716
716
 
@@ -950,13 +950,11 @@ export default function(pi: ExtensionAPI) {
950
950
  "Modes: single ({ agent, task }), parallel ({ tasks: [{agent, task},...] }), chain ({ chain: [{agent, task},...] } with {previous} placeholder).",
951
951
  "Agents are defined as .md files in the configured user agent directory (for LSD this is typically ~/.lsd/agent/agents/) or project-local .lsd/agents/, with legacy support for .gsd/agents/ and .pi/agents/.",
952
952
  "Use the /subagent command to list available agents and their descriptions.",
953
- "Use chain mode to pipeline: scout finds context, planner designs, worker implements.",
954
953
  "Set background: true (single mode only) to run detached — returns immediately with a sa_xxxx job ID. Completion is announced back into the session. Use await_subagent or /subagents to manage background jobs.",
955
954
  ].join(" "),
956
955
  promptGuidelines: [
957
956
  "Use subagent to delegate self-contained tasks that benefit from an isolated context window.",
958
- "Use scout agent first when you need codebase context before implementing.",
959
- "Use chain mode for scout→planner→worker or worker→reviewer→worker pipelines.",
957
+ "Use scout when a task requires reading and understanding many files to build architectural context not for targeted lookups where LSP or a single file read is enough.",
960
958
  "Use parallel mode when tasks are independent and don't need each other's output.",
961
959
  "Always check available agents with /subagent before choosing one.",
962
960
  "Use background: true when the user wants to keep chatting while a long-running agent works in parallel.",
@@ -1344,8 +1342,7 @@ export default function(pi: ExtensionAPI) {
1344
1342
  if (args.chain && args.chain.length > 0) {
1345
1343
  let text =
1346
1344
  theme.fg("toolTitle", theme.bold("subagent ")) +
1347
- theme.fg("accent", `chain (${args.chain.length} steps)`) +
1348
- theme.fg("muted", ` [${scope}]`);
1345
+ theme.fg("accent", `chain (${args.chain.length} steps)`);
1349
1346
  for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
1350
1347
  const step = args.chain[i];
1351
1348
  // Clean up {previous} placeholder for display
@@ -1364,8 +1361,7 @@ export default function(pi: ExtensionAPI) {
1364
1361
  if (args.tasks && args.tasks.length > 0) {
1365
1362
  let text =
1366
1363
  theme.fg("toolTitle", theme.bold("subagent ")) +
1367
- theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
1368
- theme.fg("muted", ` [${scope}]`);
1364
+ theme.fg("accent", `parallel (${args.tasks.length} tasks)`);
1369
1365
  for (const t of args.tasks.slice(0, 3)) {
1370
1366
  const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
1371
1367
  text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
@@ -1377,8 +1373,7 @@ export default function(pi: ExtensionAPI) {
1377
1373
  const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "...";
1378
1374
  let text =
1379
1375
  theme.fg("toolTitle", theme.bold("subagent ")) +
1380
- theme.fg("accent", agentName) +
1381
- theme.fg("muted", ` [${scope}]`);
1376
+ theme.fg("accent", agentName);
1382
1377
  text += `\n ${theme.fg("dim", preview)}`;
1383
1378
  return new Text(text, 0, 0);
1384
1379
  },