lsd-pi 1.1.9 → 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 (144) hide show
  1. package/dist/resources/extensions/slash-commands/context.js +15 -8
  2. package/dist/resources/extensions/slash-commands/index.js +2 -0
  3. package/dist/resources/extensions/slash-commands/init.js +47 -0
  4. package/dist/resources/extensions/slash-commands/plan.js +241 -54
  5. package/dist/resources/extensions/slash-commands/tools.js +47 -21
  6. package/dist/resources/extensions/subagent/index.js +5 -10
  7. package/dist/startup-model-validation.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/packages/pi-agent-core/dist/types.d.ts +2 -1
  10. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  11. package/packages/pi-agent-core/dist/types.js.map +1 -1
  12. package/packages/pi-agent-core/src/types.ts +2 -1
  13. package/packages/pi-ai/dist/providers/amazon-bedrock.js +10 -3
  14. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  15. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +1 -1
  16. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  17. package/packages/pi-ai/dist/providers/anthropic-shared.js +4 -1
  18. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  19. package/packages/pi-ai/dist/providers/anthropic.d.ts +2 -2
  20. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  21. package/packages/pi-ai/dist/providers/anthropic.js +2 -2
  22. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  23. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  24. package/packages/pi-ai/dist/providers/azure-openai-responses.js +2 -1
  25. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  26. package/packages/pi-ai/dist/providers/google-gemini-cli.js +2 -0
  27. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  28. package/packages/pi-ai/dist/providers/google-vertex.js +2 -0
  29. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  30. package/packages/pi-ai/dist/providers/google.js +2 -0
  31. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  32. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  33. package/packages/pi-ai/dist/providers/openai-codex-responses.js +2 -1
  34. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  35. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  36. package/packages/pi-ai/dist/providers/openai-completions.js +2 -1
  37. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  38. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  39. package/packages/pi-ai/dist/providers/openai-responses.js +2 -1
  40. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  41. package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
  42. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  43. package/packages/pi-ai/dist/providers/simple-options.js +6 -2
  44. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  45. package/packages/pi-ai/dist/types.d.ts +1 -1
  46. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  47. package/packages/pi-ai/dist/types.js.map +1 -1
  48. package/packages/pi-ai/src/providers/amazon-bedrock.ts +11 -4
  49. package/packages/pi-ai/src/providers/anthropic-shared.ts +5 -2
  50. package/packages/pi-ai/src/providers/anthropic.ts +2 -2
  51. package/packages/pi-ai/src/providers/azure-openai-responses.ts +2 -1
  52. package/packages/pi-ai/src/providers/google-gemini-cli.ts +3 -1
  53. package/packages/pi-ai/src/providers/google-vertex.ts +3 -1
  54. package/packages/pi-ai/src/providers/google.ts +3 -1
  55. package/packages/pi-ai/src/providers/openai-codex-responses.ts +2 -1
  56. package/packages/pi-ai/src/providers/openai-completions.ts +2 -1
  57. package/packages/pi-ai/src/providers/openai-responses.ts +2 -1
  58. package/packages/pi-ai/src/providers/simple-options.ts +5 -3
  59. package/packages/pi-ai/src/types.ts +1 -1
  60. package/packages/pi-coding-agent/dist/cli/args.js +1 -1
  61. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +10 -0
  63. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/agent-session.js +57 -20
  65. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/classifier-service.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/classifier-service.js +34 -61
  68. package/packages/pi-coding-agent/dist/core/classifier-service.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/compaction/utils.d.ts +1 -1
  70. package/packages/pi-coding-agent/dist/core/compaction/utils.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/compaction/utils.js +3 -1
  72. package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/resource-loader-lsd-md.test.js +59 -7
  74. package/packages/pi-coding-agent/dist/core/resource-loader-lsd-md.test.js.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/resource-loader.js +4 -4
  76. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/sdk.d.ts +1 -1
  78. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/sdk.js +19 -20
  80. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/sdk.test.js +80 -0
  82. package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +15 -5
  84. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/settings-manager.js +31 -5
  86. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -1
  88. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/system-prompt.js +28 -68
  90. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js +1 -1
  92. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js +5 -0
  94. package/packages/pi-coding-agent/dist/core/tools/bash-interceptor.test.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +28 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +8 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +44 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.js +1 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +36 -0
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +41 -5
  110. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +2 -0
  114. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  115. package/packages/pi-coding-agent/package.json +1 -1
  116. package/packages/pi-coding-agent/src/cli/args.ts +1 -1
  117. package/packages/pi-coding-agent/src/core/agent-session.ts +62 -19
  118. package/packages/pi-coding-agent/src/core/classifier-service.ts +35 -63
  119. package/packages/pi-coding-agent/src/core/compaction/utils.ts +3 -1
  120. package/packages/pi-coding-agent/src/core/resource-loader-lsd-md.test.ts +67 -7
  121. package/packages/pi-coding-agent/src/core/resource-loader.ts +4 -4
  122. package/packages/pi-coding-agent/src/core/sdk.test.ts +100 -0
  123. package/packages/pi-coding-agent/src/core/sdk.ts +24 -21
  124. package/packages/pi-coding-agent/src/core/settings-manager.ts +42 -8
  125. package/packages/pi-coding-agent/src/core/system-prompt.ts +39 -82
  126. package/packages/pi-coding-agent/src/core/tools/bash-interceptor.test.ts +6 -0
  127. package/packages/pi-coding-agent/src/core/tools/bash-interceptor.ts +1 -1
  128. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +26 -0
  129. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +53 -1
  130. package/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts +1 -0
  131. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +41 -0
  132. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +50 -7
  133. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +3 -1
  134. package/pkg/dist/modes/interactive/theme/theme.d.ts +1 -1
  135. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  136. package/pkg/dist/modes/interactive/theme/theme.js +2 -0
  137. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  138. package/pkg/package.json +1 -1
  139. package/src/resources/extensions/slash-commands/context.ts +15 -8
  140. package/src/resources/extensions/slash-commands/index.ts +2 -0
  141. package/src/resources/extensions/slash-commands/init.ts +55 -0
  142. package/src/resources/extensions/slash-commands/plan.ts +277 -55
  143. package/src/resources/extensions/slash-commands/tools.ts +47 -21
  144. package/src/resources/extensions/subagent/index.ts +5 -10
@@ -60,21 +60,25 @@ export default function contextCommand(pi: ExtensionAPI) {
60
60
  const descLen = t.description?.length ?? 0;
61
61
  const schemaLen = JSON.stringify(t.parameters ?? {}).length;
62
62
  const totalChars = nameLen + descLen + schemaLen;
63
+ const isActive = activeToolNames.has(t.name ?? "");
63
64
  const tokens = Math.ceil(totalChars / 4);
64
65
  return {
65
66
  name: t.name ?? "(unnamed)",
67
+ totalChars,
66
68
  tokens,
67
69
  descTokens: Math.ceil(descLen / 4),
68
70
  schemaTokens: Math.ceil((nameLen + schemaLen) / 4),
69
- isActive: activeToolNames.has(t.name ?? ""),
71
+ isActive,
70
72
  };
71
73
  });
72
74
 
73
- const totalSchemaBytes = allTools.reduce((sum, t) => {
74
- return sum + (t.name?.length ?? 0) + (t.description?.length ?? 0) + JSON.stringify(t.parameters ?? {}).length;
75
- }, 0);
75
+ const activeSchemaBytes = toolSizes
76
+ .filter((t) => t.isActive)
77
+ .reduce((sum, t) => sum + t.totalChars, 0);
78
+ const registeredSchemaBytes = toolSizes.reduce((sum, t) => sum + t.totalChars, 0);
76
79
 
77
- const toolsTokens = Math.ceil(totalSchemaBytes / 4);
80
+ const activeToolsTokens = Math.ceil(activeSchemaBytes / 4);
81
+ const registeredToolsTokens = Math.ceil(registeredSchemaBytes / 4);
78
82
 
79
83
  const largestActiveTools = toolSizes
80
84
  .filter((t) => t.isActive)
@@ -109,7 +113,7 @@ export default function contextCommand(pi: ExtensionAPI) {
109
113
  const usedTokens = contextUsage?.tokens ?? null;
110
114
  const percentUsed = contextUsage?.percent ?? null;
111
115
 
112
- const fallbackUsedTokens = systemPromptTokens + toolsTokens + historyTokens;
116
+ const fallbackUsedTokens = systemPromptTokens + activeToolsTokens + historyTokens;
113
117
  const effectiveUsedTokens = usedTokens ?? fallbackUsedTokens;
114
118
  const estimatedLabel = usedTokens === null ? " (estimated)" : "";
115
119
 
@@ -161,8 +165,11 @@ export default function contextCommand(pi: ExtensionAPI) {
161
165
  lines.push("");
162
166
 
163
167
  lines.push(`Tools ${activeToolsCount} active / ${totalToolsCount} registered`);
164
- lines.push(` Total schema bytes: ${totalSchemaBytes.toLocaleString()}`);
165
- lines.push(` Est. tokens: ~${toolsTokens.toLocaleString()}`);
168
+ lines.push(` Active schema bytes: ${activeSchemaBytes.toLocaleString()}`);
169
+ lines.push(` Est. tokens: ~${activeToolsTokens.toLocaleString()}`);
170
+ if (activeToolsCount !== totalToolsCount) {
171
+ lines.push(` Registered total: ${registeredSchemaBytes.toLocaleString()} bytes · ~${registeredToolsTokens.toLocaleString()} tok`);
172
+ }
166
173
 
167
174
  if (largestActiveTools.length > 0) {
168
175
  lines.push(" Largest active tools:");
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
2
2
  import auditCommand from "./audit.js";
3
3
  import clearCommand from "./clear.js";
4
4
  import contextCommand from "./context.js";
5
+ import initCommand from "./init.js";
5
6
  import planCommand from "./plan.js";
6
7
  import toolSearchExtension from "./tools.js";
7
8
 
@@ -9,6 +10,7 @@ export default function slashCommands(pi: ExtensionAPI) {
9
10
  auditCommand(pi);
10
11
  clearCommand(pi);
11
12
  contextCommand(pi);
13
+ initCommand(pi);
12
14
  planCommand(pi);
13
15
  toolSearchExtension(pi);
14
16
  }
@@ -0,0 +1,55 @@
1
+ import { getAgentDir, type ExtensionAPI, type ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
+ import { existsSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+
5
+ const STARTER_CONTENT = `# Project Context
6
+
7
+ ## Overview
8
+ - Add a short description of this project and its purpose.
9
+
10
+ ## Commands
11
+ - Build:
12
+ - Test:
13
+ - Lint:
14
+
15
+ ## Conventions
16
+ - Add coding conventions, architecture notes, and review expectations.
17
+ `;
18
+
19
+ function ensureFile(filePath: string): "created" | "exists" {
20
+ if (existsSync(filePath)) {
21
+ return "exists";
22
+ }
23
+ writeFileSync(filePath, STARTER_CONTENT, "utf-8");
24
+ return "created";
25
+ }
26
+
27
+ export default function initCommand(pi: ExtensionAPI) {
28
+ pi.registerCommand("init", {
29
+ description: "Initialize global and project LSD.md files if they do not exist",
30
+ async handler(_args: string, ctx: ExtensionCommandContext) {
31
+ const globalPath = resolve(getAgentDir(), "..", "LSD.md");
32
+ const projectPath = join(ctx.cwd, "LSD.md");
33
+
34
+ const globalStatus = ensureFile(globalPath);
35
+ const projectStatus = ensureFile(projectPath);
36
+
37
+ await ctx.reload();
38
+
39
+ const lines = ["Initialized LSD.md files", ""];
40
+ lines.push(`Global: ${globalStatus === "created" ? "created" : "exists"} ${globalPath}`);
41
+ lines.push(`Project: ${projectStatus === "created" ? "created" : "exists"} ${projectPath}`);
42
+
43
+ if (globalStatus === "exists" && projectStatus === "exists") {
44
+ lines.push("");
45
+ lines.push("Nothing changed.");
46
+ }
47
+
48
+ pi.sendMessage({
49
+ customType: "init:report",
50
+ content: lines.join("\n"),
51
+ display: true,
52
+ });
53
+ },
54
+ });
55
+ }
@@ -4,6 +4,7 @@ import {
4
4
  getPermissionMode,
5
5
  isToolCallEventType,
6
6
  setPermissionMode,
7
+ SettingsManager,
7
8
  type ExtensionAPI,
8
9
  type ExtensionCommandContext,
9
10
  type PermissionMode,
@@ -13,6 +14,7 @@ import { join } from "node:path";
13
14
  const PLAN_ENTRY_TYPE = "plan-mode-state";
14
15
  const PLAN_APPROVAL_ACTION_QUESTION_ID = "plan_mode_approval_action";
15
16
  const PLAN_APPROVAL_PERMISSION_QUESTION_ID = "plan_mode_approval_permission";
17
+ const PLAN_SUGGEST_QUESTION_ID = "plan_mode_suggest_switch";
16
18
  const PLAN_DIR_RE = /(^|[/\\])\.(?:lsd|gsd)[/\\]plan([/\\]|$)/;
17
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)/;
18
20
  const SAFE_TOOLS = new Set([
@@ -72,10 +74,12 @@ const APPROVE_AUTO_LABEL = "Auto mode";
72
74
  const APPROVE_BYPASS_LABEL = "Bypass mode";
73
75
  const APPROVE_AUTO_SUBAGENT_LABEL = "Execute with subagent in auto mode";
74
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
75
78
  const REVIEW_LABEL = "Let other agent review";
76
79
  const REVISE_LABEL = "Revise plan";
77
80
  const CANCEL_LABEL = "Cancel";
78
81
  const DEFAULT_PLAN_REVIEW_AGENT = "generic";
82
+ const DEFAULT_PLAN_CODING_AGENT = "worker";
79
83
 
80
84
  type PlanApprovalStatus = "pending" | "approved" | "revising" | "cancelled";
81
85
  type RestorablePermissionMode = Exclude<PermissionMode, "plan">;
@@ -108,6 +112,7 @@ const INITIAL_STATE: PlanModeState = {
108
112
 
109
113
  let state: PlanModeState = { ...INITIAL_STATE };
110
114
  let startedFromFlag = false;
115
+ let reasoningModelSwitchDone = false;
111
116
 
112
117
  function isPlanModeActive(): boolean {
113
118
  return getPermissionMode() === "plan";
@@ -124,7 +129,13 @@ function parseQualifiedModelRef(value: unknown): ModelRef | undefined {
124
129
  return { provider, id };
125
130
  }
126
131
 
127
- 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 } {
128
139
  try {
129
140
  const settingsPath = join(getAgentDir(), "settings.json");
130
141
  if (!existsSync(settingsPath)) return {};
@@ -133,14 +144,19 @@ function readPlanModeSettings(): { reasoningModel?: string; reviewModel?: string
133
144
  planModeReasoningModel?: unknown;
134
145
  planModeReviewModel?: unknown;
135
146
  planModeCodingModel?: unknown;
147
+ planModeCodingSubagent?: unknown;
148
+ planModeCodingAgent?: unknown;
136
149
  };
137
150
  const reasoningModel = parseQualifiedModelRef(parsed.planModeReasoningModel);
138
151
  const reviewModel = parseQualifiedModelRef(parsed.planModeReviewModel);
139
152
  const codingModel = parseQualifiedModelRef(parsed.planModeCodingModel);
153
+ const codingSubagent = parseSubagentName(parsed.planModeCodingSubagent)
154
+ ?? parseSubagentName(parsed.planModeCodingAgent);
140
155
  return {
141
156
  reasoningModel: reasoningModel ? `${reasoningModel.provider}/${reasoningModel.id}` : undefined,
142
157
  reviewModel: reviewModel ? `${reviewModel.provider}/${reviewModel.id}` : undefined,
143
158
  codingModel: codingModel ? `${codingModel.provider}/${codingModel.id}` : undefined,
159
+ codingSubagent,
144
160
  };
145
161
  } catch {
146
162
  return {};
@@ -159,6 +175,42 @@ export function readPlanModeCodingModel(): string | undefined {
159
175
  return readPlanModeSettings().codingModel;
160
176
  }
161
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
+
162
214
  function sameModel(left: ModelRef | undefined, right: ModelRef | undefined): boolean {
163
215
  return !!left && !!right && left.provider === right.provider && left.id === right.id;
164
216
  }
@@ -170,6 +222,12 @@ function resolveModelFromContext(ctx: any, modelRef: ModelRef): any | undefined
170
222
 
171
223
  function setPermissionModeAndEnv(mode: PermissionMode): void {
172
224
  setPermissionMode(mode);
225
+ try {
226
+ const settingsManager = SettingsManager.create();
227
+ settingsManager.setPermissionMode(mode);
228
+ } catch {
229
+ // Best-effort persistence; if settings manager is unavailable, proceed with in-memory only
230
+ }
173
231
  process.env.LUCENT_CODE_PERMISSION_MODE = mode;
174
232
  }
175
233
 
@@ -210,6 +268,24 @@ function restoreStateFromSession(ctx: ExtensionCommandContext | any): void {
210
268
  }
211
269
  }
212
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
+
213
289
  function enablePlanMode(
214
290
  pi: ExtensionAPI,
215
291
  currentModel: ModelRef | undefined,
@@ -239,6 +315,7 @@ function leavePlanMode(
239
315
  nextPermissionMode: RestorablePermissionMode,
240
316
  clearTask = false,
241
317
  ): RestorablePermissionMode {
318
+ reasoningModelSwitchDone = false;
242
319
  setPermissionModeAndEnv(nextPermissionMode);
243
320
  setState(pi, {
244
321
  active: false,
@@ -275,14 +352,14 @@ function buildExecutionKickoffMessage(options: { permissionMode: RestorablePermi
275
352
  }
276
353
 
277
354
  const codingModel = readPlanModeCodingModel();
278
- const modelInstruction = codingModel
279
- ? `Set model to \"${codingModel}\" for that subagent.`
280
- : "Do not pass a model override for that subagent unless needed.";
355
+ const codingSubagent = readPlanModeCodingSubagent() ?? DEFAULT_PLAN_CODING_AGENT;
356
+ const agentInvocationInstruction = codingModel
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.`;
281
359
 
282
360
  const details: string[] = [
283
361
  "Plan approved. Exit plan mode and execute the approved plan with a subagent now.",
284
- "Invoke the subagent tool with agent \"generic\" to implement the plan end-to-end.",
285
- modelInstruction,
362
+ agentInvocationInstruction,
286
363
  `Execution permission mode is now \"${permissionMode}\".`,
287
364
  ];
288
365
  if (task) details.push(`Original task: ${task}`);
@@ -291,6 +368,38 @@ function buildExecutionKickoffMessage(options: { permissionMode: RestorablePermi
291
368
  return details.join(" ");
292
369
  }
293
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
+
294
403
  async function approvePlan(
295
404
  pi: ExtensionAPI,
296
405
  ctx: any,
@@ -307,7 +416,13 @@ async function approvePlan(
307
416
  targetPermissionMode: permissionMode,
308
417
  };
309
418
  leavePlanMode(pi, "approved", permissionMode);
310
- 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" });
311
426
  }
312
427
 
313
428
  async function cancelPlan(pi: ExtensionAPI, ctx: any, clearTask = true): Promise<RestorablePermissionMode> {
@@ -340,35 +455,52 @@ function readPlanArtifact(planPath: string): string | undefined {
340
455
  }
341
456
  }
342
457
 
343
- 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 {
344
469
  return [
345
- "Present approval options now using ask_user_questions with exactly two single-select questions.",
346
- `First question id: \"${PLAN_APPROVAL_ACTION_QUESTION_ID}\". Ask what to do next with the plan.`,
347
- "Use exactly these 3 options for the first question:",
348
- `1. ${APPROVE_LABEL} (Recommended)`,
349
- `2. ${REVIEW_LABEL}`,
350
- `3. ${REVISE_LABEL}`,
351
- `Second question id: \"${PLAN_APPROVAL_PERMISSION_QUESTION_ID}\". Ask which execution mode to use if the plan is approved.`,
352
- "Use exactly these 4 options for the second question:",
353
- `1. ${APPROVE_AUTO_LABEL} (Recommended)`,
354
- `2. ${APPROVE_BYPASS_LABEL}`,
355
- `3. ${APPROVE_AUTO_SUBAGENT_LABEL}`,
356
- `4. ${APPROVE_BYPASS_SUBAGENT_LABEL}`,
357
- `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.`,
358
- `If the user selects \"${REVIEW_LABEL}\" or \"${REVISE_LABEL}\", ignore the second answer for now.`,
359
- "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.",
360
475
  ].join(" ");
361
476
  }
362
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
+
363
499
  function buildApprovalSteeringMessage(planPath: string): string {
364
- const details = [
500
+ return [
365
501
  `Plan artifact saved at ${planPath}.`,
366
- "Do not restate the plan in a normal assistant response.",
367
- "Ask for approval now via ask_user_questions.",
368
- buildApprovalDialogInstructions(),
369
- ];
370
-
371
- return details.join("\n\n");
502
+ buildApprovalActionInstructions(),
503
+ ].join("\n\n");
372
504
  }
373
505
 
374
506
  function buildPlanPreviewMessage(planPath: string, planMarkdown?: string): string {
@@ -414,7 +546,7 @@ function buildReviewSteeringMessage(planPath: string, planMarkdown?: string): st
414
546
 
415
547
  details.push(
416
548
  "After the subagent responds, summarize its feedback for the user, present the current plan again, and then ask for approval again.",
417
- buildApprovalDialogInstructions(),
549
+ buildApprovalActionInstructions(),
418
550
  );
419
551
 
420
552
  return details.join("\n\n");
@@ -472,6 +604,9 @@ export const __testing = {
472
604
  buildApprovalSteeringMessage,
473
605
  buildPlanPreviewMessage,
474
606
  buildReviewSteeringMessage,
607
+ buildAutoSuggestPlanModeSystemPrompt,
608
+ readAutoSuggestPlanModeSetting,
609
+ PLAN_SUGGEST_QUESTION_ID,
475
610
  };
476
611
 
477
612
  export default function planCommand(pi: ExtensionAPI) {
@@ -483,16 +618,28 @@ export default function planCommand(pi: ExtensionAPI) {
483
618
  pi.on("session_start", async (_event, ctx) => {
484
619
  restoreStateFromSession(ctx);
485
620
  startedFromFlag = false;
621
+ reasoningModelSwitchDone = false;
486
622
  if (state.active) {
487
623
  setPermissionModeAndEnv("plan");
488
624
  }
489
625
  });
490
626
 
491
- pi.on("before_agent_start", async () => {
492
- if (!isPlanModeActive()) return;
493
- return {
494
- systemPrompt: buildPlanModeSystemPrompt(),
495
- };
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;
496
643
  });
497
644
 
498
645
  pi.on("input", async (event, ctx) => {
@@ -503,7 +650,7 @@ export default function planCommand(pi: ExtensionAPI) {
503
650
 
504
651
  startedFromFlag = true;
505
652
  ensurePlanDir();
506
- 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, {
507
654
  task: event.text.trim(),
508
655
  approvalStatus: "pending",
509
656
  latestPlanPath: undefined,
@@ -571,6 +718,35 @@ export default function planCommand(pi: ExtensionAPI) {
571
718
  return;
572
719
  }
573
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
+
574
750
  if (!isPlanModeActive() || event.toolName !== "ask_user_questions") return;
575
751
 
576
752
  const details = event.details as {
@@ -584,35 +760,48 @@ export default function planCommand(pi: ExtensionAPI) {
584
760
  const actionValues = getAnswerValues(actionAnswer);
585
761
  const permissionValues = getAnswerValues(permissionAnswer);
586
762
 
587
- if (actionValues.length === 0 && permissionValues.length === 0) return;
588
-
589
- if (selectionRequestsCancel([...actionValues, ...permissionValues])) {
590
- await cancelPlan(pi, ctx, true);
591
- return;
592
- }
763
+ // ── Second question answered (execution mode) ─────────────────────────
764
+ if (permissionValues.length > 0) {
765
+ if (selectionRequestsCancel(permissionValues)) {
766
+ await cancelPlan(pi, ctx, true);
767
+ return;
768
+ }
593
769
 
594
- const actionSelection = actionValues[0];
595
- if (!actionSelection) return;
770
+ if (permissionValues[0]?.includes(APPROVE_NEW_SESSION_LABEL)) {
771
+ scheduleNewSession(pi, ctx);
772
+ return;
773
+ }
596
774
 
597
- if (actionSelection.includes(APPROVE_LABEL)) {
598
775
  const executionMode = approvalSelectionToExecutionMode(permissionValues[0]) ?? {
599
776
  permissionMode: DEFAULT_APPROVAL_PERMISSION_MODE,
600
777
  executeWithSubagent: false,
601
778
  };
602
- state = {
603
- ...state,
604
- targetPermissionMode: executionMode.permissionMode,
605
- };
606
-
779
+ state = { ...state, targetPermissionMode: executionMode.permissionMode };
607
780
  if (executionMode.executeWithSubagent) {
608
781
  const modeLabel = executionMode.permissionMode === "danger-full-access" ? "bypass" : "auto";
609
782
  ctx.ui?.notify?.(`Plan approved: subagent(${modeLabel})`, "info");
610
783
  }
611
-
612
784
  await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent);
613
785
  return;
614
786
  }
615
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
+
616
805
  if (actionSelection.includes(REVIEW_LABEL)) {
617
806
  setState(pi, {
618
807
  ...state,
@@ -654,16 +843,17 @@ export default function planCommand(pi: ExtensionAPI) {
654
843
 
655
844
  ensurePlanDir();
656
845
  const task = args.trim();
657
- 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, {
658
847
  task,
659
848
  latestPlanPath: undefined,
660
849
  approvalStatus: "pending",
661
850
  targetPermissionMode: undefined,
662
851
  });
852
+ const reasoningModel = readAutoSwitchPlanModelSetting() ? readPlanModeReasoningModel() : undefined;
663
853
  ctx.ui.notify(
664
854
  task
665
- ? `Plan mode enabled. Current task: ${task}`
666
- : "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.`,
667
857
  "info",
668
858
  );
669
859
  },
@@ -692,4 +882,36 @@ export default function planCommand(pi: ExtensionAPI) {
692
882
  ctx.ui.notify("Plan mode cancelled.", "info");
693
883
  },
694
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
+ });
695
917
  }