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
@@ -48,19 +48,23 @@ export default function contextCommand(pi) {
48
48
  const descLen = t.description?.length ?? 0;
49
49
  const schemaLen = JSON.stringify(t.parameters ?? {}).length;
50
50
  const totalChars = nameLen + descLen + schemaLen;
51
+ const isActive = activeToolNames.has(t.name ?? "");
51
52
  const tokens = Math.ceil(totalChars / 4);
52
53
  return {
53
54
  name: t.name ?? "(unnamed)",
55
+ totalChars,
54
56
  tokens,
55
57
  descTokens: Math.ceil(descLen / 4),
56
58
  schemaTokens: Math.ceil((nameLen + schemaLen) / 4),
57
- isActive: activeToolNames.has(t.name ?? ""),
59
+ isActive,
58
60
  };
59
61
  });
60
- const totalSchemaBytes = allTools.reduce((sum, t) => {
61
- return sum + (t.name?.length ?? 0) + (t.description?.length ?? 0) + JSON.stringify(t.parameters ?? {}).length;
62
- }, 0);
63
- const toolsTokens = Math.ceil(totalSchemaBytes / 4);
62
+ const activeSchemaBytes = toolSizes
63
+ .filter((t) => t.isActive)
64
+ .reduce((sum, t) => sum + t.totalChars, 0);
65
+ const registeredSchemaBytes = toolSizes.reduce((sum, t) => sum + t.totalChars, 0);
66
+ const activeToolsTokens = Math.ceil(activeSchemaBytes / 4);
67
+ const registeredToolsTokens = Math.ceil(registeredSchemaBytes / 4);
64
68
  const largestActiveTools = toolSizes
65
69
  .filter((t) => t.isActive)
66
70
  .sort((a, b) => b.tokens - a.tokens)
@@ -84,7 +88,7 @@ export default function contextCommand(pi) {
84
88
  const windowSize = model?.contextWindow ?? null;
85
89
  const usedTokens = contextUsage?.tokens ?? null;
86
90
  const percentUsed = contextUsage?.percent ?? null;
87
- const fallbackUsedTokens = systemPromptTokens + toolsTokens + historyTokens;
91
+ const fallbackUsedTokens = systemPromptTokens + activeToolsTokens + historyTokens;
88
92
  const effectiveUsedTokens = usedTokens ?? fallbackUsedTokens;
89
93
  const estimatedLabel = usedTokens === null ? " (estimated)" : "";
90
94
  const freeTokens = windowSize !== null ? windowSize - effectiveUsedTokens : null;
@@ -129,8 +133,11 @@ export default function contextCommand(pi) {
129
133
  }
130
134
  lines.push("");
131
135
  lines.push(`Tools ${activeToolsCount} active / ${totalToolsCount} registered`);
132
- lines.push(` Total schema bytes: ${totalSchemaBytes.toLocaleString()}`);
133
- lines.push(` Est. tokens: ~${toolsTokens.toLocaleString()}`);
136
+ lines.push(` Active schema bytes: ${activeSchemaBytes.toLocaleString()}`);
137
+ lines.push(` Est. tokens: ~${activeToolsTokens.toLocaleString()}`);
138
+ if (activeToolsCount !== totalToolsCount) {
139
+ lines.push(` Registered total: ${registeredSchemaBytes.toLocaleString()} bytes · ~${registeredToolsTokens.toLocaleString()} tok`);
140
+ }
134
141
  if (largestActiveTools.length > 0) {
135
142
  lines.push(" Largest active tools:");
136
143
  for (const tool of largestActiveTools) {
@@ -1,12 +1,14 @@
1
1
  import auditCommand from "./audit.js";
2
2
  import clearCommand from "./clear.js";
3
3
  import contextCommand from "./context.js";
4
+ import initCommand from "./init.js";
4
5
  import planCommand from "./plan.js";
5
6
  import toolSearchExtension from "./tools.js";
6
7
  export default function slashCommands(pi) {
7
8
  auditCommand(pi);
8
9
  clearCommand(pi);
9
10
  contextCommand(pi);
11
+ initCommand(pi);
10
12
  planCommand(pi);
11
13
  toolSearchExtension(pi);
12
14
  }
@@ -0,0 +1,47 @@
1
+ import { getAgentDir } from "@gsd/pi-coding-agent";
2
+ import { existsSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ const STARTER_CONTENT = `# Project Context
5
+
6
+ ## Overview
7
+ - Add a short description of this project and its purpose.
8
+
9
+ ## Commands
10
+ - Build:
11
+ - Test:
12
+ - Lint:
13
+
14
+ ## Conventions
15
+ - Add coding conventions, architecture notes, and review expectations.
16
+ `;
17
+ function ensureFile(filePath) {
18
+ if (existsSync(filePath)) {
19
+ return "exists";
20
+ }
21
+ writeFileSync(filePath, STARTER_CONTENT, "utf-8");
22
+ return "created";
23
+ }
24
+ export default function initCommand(pi) {
25
+ pi.registerCommand("init", {
26
+ description: "Initialize global and project LSD.md files if they do not exist",
27
+ async handler(_args, ctx) {
28
+ const globalPath = resolve(getAgentDir(), "..", "LSD.md");
29
+ const projectPath = join(ctx.cwd, "LSD.md");
30
+ const globalStatus = ensureFile(globalPath);
31
+ const projectStatus = ensureFile(projectPath);
32
+ await ctx.reload();
33
+ const lines = ["Initialized LSD.md files", ""];
34
+ lines.push(`Global: ${globalStatus === "created" ? "created" : "exists"} ${globalPath}`);
35
+ lines.push(`Project: ${projectStatus === "created" ? "created" : "exists"} ${projectPath}`);
36
+ if (globalStatus === "exists" && projectStatus === "exists") {
37
+ lines.push("");
38
+ lines.push("Nothing changed.");
39
+ }
40
+ pi.sendMessage({
41
+ customType: "init:report",
42
+ content: lines.join("\n"),
43
+ display: true,
44
+ });
45
+ },
46
+ });
47
+ }
@@ -1,9 +1,10 @@
1
1
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
- import { getAgentDir, getPermissionMode, isToolCallEventType, setPermissionMode, } from "@gsd/pi-coding-agent";
2
+ import { getAgentDir, getPermissionMode, isToolCallEventType, setPermissionMode, SettingsManager, } from "@gsd/pi-coding-agent";
3
3
  import { join } from "node:path";
4
4
  const PLAN_ENTRY_TYPE = "plan-mode-state";
5
5
  const PLAN_APPROVAL_ACTION_QUESTION_ID = "plan_mode_approval_action";
6
6
  const PLAN_APPROVAL_PERMISSION_QUESTION_ID = "plan_mode_approval_permission";
7
+ const PLAN_SUGGEST_QUESTION_ID = "plan_mode_suggest_switch";
7
8
  const PLAN_DIR_RE = /(^|[/\\])\.(?:lsd|gsd)[/\\]plan([/\\]|$)/;
8
9
  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)/;
9
10
  const SAFE_TOOLS = new Set([
@@ -63,10 +64,12 @@ const APPROVE_AUTO_LABEL = "Auto mode";
63
64
  const APPROVE_BYPASS_LABEL = "Bypass mode";
64
65
  const APPROVE_AUTO_SUBAGENT_LABEL = "Execute with subagent in auto mode";
65
66
  const APPROVE_BYPASS_SUBAGENT_LABEL = "Execute with subagent in bypass mode";
67
+ const APPROVE_NEW_SESSION_LABEL = "New session with coding model"; // shown in second question when autoSwitchPlanModel is on
66
68
  const REVIEW_LABEL = "Let other agent review";
67
69
  const REVISE_LABEL = "Revise plan";
68
70
  const CANCEL_LABEL = "Cancel";
69
71
  const DEFAULT_PLAN_REVIEW_AGENT = "generic";
72
+ const DEFAULT_PLAN_CODING_AGENT = "worker";
70
73
  const INITIAL_STATE = {
71
74
  active: false,
72
75
  task: "",
@@ -78,6 +81,7 @@ const INITIAL_STATE = {
78
81
  };
79
82
  let state = { ...INITIAL_STATE };
80
83
  let startedFromFlag = false;
84
+ let reasoningModelSwitchDone = false;
81
85
  function isPlanModeActive() {
82
86
  return getPermissionMode() === "plan";
83
87
  }
@@ -95,6 +99,12 @@ function parseQualifiedModelRef(value) {
95
99
  return undefined;
96
100
  return { provider, id };
97
101
  }
102
+ function parseSubagentName(value) {
103
+ if (typeof value !== "string")
104
+ return undefined;
105
+ const trimmed = value.trim();
106
+ return trimmed.length > 0 ? trimmed : undefined;
107
+ }
98
108
  function readPlanModeSettings() {
99
109
  try {
100
110
  const settingsPath = join(getAgentDir(), "settings.json");
@@ -105,10 +115,13 @@ function readPlanModeSettings() {
105
115
  const reasoningModel = parseQualifiedModelRef(parsed.planModeReasoningModel);
106
116
  const reviewModel = parseQualifiedModelRef(parsed.planModeReviewModel);
107
117
  const codingModel = parseQualifiedModelRef(parsed.planModeCodingModel);
118
+ const codingSubagent = parseSubagentName(parsed.planModeCodingSubagent)
119
+ ?? parseSubagentName(parsed.planModeCodingAgent);
108
120
  return {
109
121
  reasoningModel: reasoningModel ? `${reasoningModel.provider}/${reasoningModel.id}` : undefined,
110
122
  reviewModel: reviewModel ? `${reviewModel.provider}/${reviewModel.id}` : undefined,
111
123
  codingModel: codingModel ? `${codingModel.provider}/${codingModel.id}` : undefined,
124
+ codingSubagent,
112
125
  };
113
126
  }
114
127
  catch {
@@ -124,6 +137,42 @@ export function readPlanModeReviewModel() {
124
137
  export function readPlanModeCodingModel() {
125
138
  return readPlanModeSettings().codingModel;
126
139
  }
140
+ export function readPlanModeCodingSubagent() {
141
+ return readPlanModeSettings().codingSubagent;
142
+ }
143
+ function readAutoSuggestPlanModeSetting() {
144
+ try {
145
+ const settingsPath = join(getAgentDir(), "settings.json");
146
+ if (!existsSync(settingsPath))
147
+ return false;
148
+ const raw = readFileSync(settingsPath, "utf-8");
149
+ const parsed = JSON.parse(raw);
150
+ return parsed.autoSuggestPlanMode === true;
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ function readAutoSwitchPlanModelSetting() {
157
+ try {
158
+ const settingsPath = join(getAgentDir(), "settings.json");
159
+ if (!existsSync(settingsPath))
160
+ return false;
161
+ const raw = readFileSync(settingsPath, "utf-8");
162
+ const parsed = JSON.parse(raw);
163
+ return parsed.autoSwitchPlanModel === true;
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ }
169
+ function buildAutoSuggestPlanModeSystemPrompt() {
170
+ return [
171
+ `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.`,
172
+ `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.`,
173
+ "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.",
174
+ ].join(" ");
175
+ }
127
176
  function sameModel(left, right) {
128
177
  return !!left && !!right && left.provider === right.provider && left.id === right.id;
129
178
  }
@@ -133,6 +182,13 @@ function resolveModelFromContext(ctx, modelRef) {
133
182
  }
134
183
  function setPermissionModeAndEnv(mode) {
135
184
  setPermissionMode(mode);
185
+ try {
186
+ const settingsManager = SettingsManager.create();
187
+ settingsManager.setPermissionMode(mode);
188
+ }
189
+ catch {
190
+ // Best-effort persistence; if settings manager is unavailable, proceed with in-memory only
191
+ }
136
192
  process.env.LUCENT_CODE_PERMISSION_MODE = mode;
137
193
  }
138
194
  function saveState(pi) {
@@ -168,6 +224,16 @@ function restoreStateFromSession(ctx) {
168
224
  // Best-effort restore only.
169
225
  }
170
226
  }
227
+ async function enablePlanModeWithModelSwitch(pi, ctx, currentModel, next = {}) {
228
+ enablePlanMode(pi, currentModel, next);
229
+ // Signal that before_agent_start should switch to the reasoning model on next turn
230
+ reasoningModelSwitchDone = false;
231
+ if (!readAutoSwitchPlanModelSetting())
232
+ return;
233
+ if (!readPlanModeReasoningModel()) {
234
+ ctx.ui?.notify?.("OpusPlan: set a Plan reasoning model in /settings to auto-switch on entry", "info");
235
+ }
236
+ }
171
237
  function enablePlanMode(pi, currentModel, next = {}) {
172
238
  const currentMode = getPermissionMode();
173
239
  const enteringPlanMode = currentMode !== "plan";
@@ -186,6 +252,7 @@ function enablePlanMode(pi, currentModel, next = {}) {
186
252
  });
187
253
  }
188
254
  function leavePlanMode(pi, approvalStatus, nextPermissionMode, clearTask = false) {
255
+ reasoningModelSwitchDone = false;
189
256
  setPermissionModeAndEnv(nextPermissionMode);
190
257
  setState(pi, {
191
258
  active: false,
@@ -223,13 +290,13 @@ function buildExecutionKickoffMessage(options) {
223
290
  return details.join(" ");
224
291
  }
225
292
  const codingModel = readPlanModeCodingModel();
226
- const modelInstruction = codingModel
227
- ? `Set model to \"${codingModel}\" for that subagent.`
228
- : "Do not pass a model override for that subagent unless needed.";
293
+ const codingSubagent = readPlanModeCodingSubagent() ?? DEFAULT_PLAN_CODING_AGENT;
294
+ const agentInvocationInstruction = codingModel
295
+ ? `Invoke the subagent tool with exact parameters agent "${codingSubagent}" and model="${codingModel}" to implement the plan end-to-end.`
296
+ : `Invoke the subagent tool with exact parameter agent "${codingSubagent}" to implement the plan end-to-end.`;
229
297
  const details = [
230
298
  "Plan approved. Exit plan mode and execute the approved plan with a subagent now.",
231
- "Invoke the subagent tool with agent \"generic\" to implement the plan end-to-end.",
232
- modelInstruction,
299
+ agentInvocationInstruction,
233
300
  `Execution permission mode is now \"${permissionMode}\".`,
234
301
  ];
235
302
  if (task)
@@ -239,6 +306,25 @@ function buildExecutionKickoffMessage(options) {
239
306
  details.push("After subagent completion, summarize the result and any remaining follow-ups.");
240
307
  return details.join(" ");
241
308
  }
309
+ let pendingNewSession = null;
310
+ function scheduleNewSession(pi, ctx) {
311
+ const codingModelRef = parseQualifiedModelRef(readPlanModeCodingModel());
312
+ const codingSubagent = readPlanModeCodingSubagent() ?? DEFAULT_PLAN_CODING_AGENT;
313
+ const planPath = state.latestPlanPath;
314
+ const planContent = planPath ? readPlanArtifact(planPath) : undefined;
315
+ pendingNewSession = {
316
+ codingModelRef,
317
+ codingSubagent,
318
+ planPath,
319
+ planContent,
320
+ task: state.task,
321
+ };
322
+ leavePlanMode(pi, "approved", "auto");
323
+ ctx.ui?.notify?.("Plan approved. Starting new session…", "info");
324
+ // Trigger the internal command which has ExtensionCommandContext (ctx.newSession available).
325
+ // Must use the /prefix so tryExecuteExtensionCommand parses the name correctly.
326
+ pi.executeSlashCommand("/plan-execute-new-session");
327
+ }
242
328
  async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false) {
243
329
  const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
244
330
  if (reasoningModel) {
@@ -249,7 +335,13 @@ async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false)
249
335
  targetPermissionMode: permissionMode,
250
336
  };
251
337
  leavePlanMode(pi, "approved", permissionMode);
252
- await pi.sendUserMessage(buildExecutionKickoffMessage({ permissionMode, executeWithSubagent }), { deliverAs: "followUp" });
338
+ // Deliver the kickoff as a steering message so it is injected BEFORE the LLM
339
+ // produces its next assistant turn. Using "followUp" would defer delivery
340
+ // until the agent has no more tool calls, which lets the LLM call the
341
+ // subagent tool with the default session model BEFORE it ever sees the
342
+ // explicit model="<planModeCodingModel>" instruction. Steering ensures the
343
+ // configured plan-mode coding model reaches the subagent invocation.
344
+ await pi.sendUserMessage(buildExecutionKickoffMessage({ permissionMode, executeWithSubagent }), { deliverAs: "steer" });
253
345
  }
254
346
  async function cancelPlan(pi, ctx, clearTask = true) {
255
347
  const restoreMode = state.previousMode ?? "accept-on-edit";
@@ -282,33 +374,46 @@ function readPlanArtifact(planPath) {
282
374
  return undefined;
283
375
  }
284
376
  }
285
- function buildApprovalDialogInstructions() {
377
+ function buildNewSessionOptionLabel() {
378
+ const codingModel = readPlanModeCodingModel();
379
+ const codingSubagent = readPlanModeCodingSubagent() ?? DEFAULT_PLAN_CODING_AGENT;
380
+ const modelSuffix = codingModel ? codingModel.split("/")[1] ?? codingModel : null;
381
+ // e.g. "Approve plan — new session (worker · claude-sonnet-4-6)"
382
+ // "Approve plan — new session (worker)"
383
+ const suffix = modelSuffix ? `${codingSubagent} · ${modelSuffix}` : codingSubagent;
384
+ return `${APPROVE_LABEL} — ${APPROVE_NEW_SESSION_LABEL} (${suffix})`;
385
+ }
386
+ function buildApprovalActionInstructions() {
387
+ return [
388
+ "Ask for plan approval now using ask_user_questions.",
389
+ `One single-select question with id \"${PLAN_APPROVAL_ACTION_QUESTION_ID}\". Ask what to do next with the plan.`,
390
+ `Options: ${APPROVE_LABEL}, ${REVIEW_LABEL}, ${REVISE_LABEL}.`,
391
+ `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.`,
392
+ "Do not restate the plan. Just show the question.",
393
+ ].join(" ");
394
+ }
395
+ function buildApprovalModeInstructions() {
396
+ const autoSwitchEnabled = readAutoSwitchPlanModelSetting();
397
+ const showNewSessionOption = autoSwitchEnabled;
398
+ const newSessionLabel = buildNewSessionOptionLabel();
399
+ const options = showNewSessionOption
400
+ ? `${APPROVE_AUTO_LABEL}, ${APPROVE_BYPASS_LABEL}, ${APPROVE_AUTO_SUBAGENT_LABEL}, ${APPROVE_BYPASS_SUBAGENT_LABEL}, ${newSessionLabel}`
401
+ : `${APPROVE_AUTO_LABEL}, ${APPROVE_BYPASS_LABEL}, ${APPROVE_AUTO_SUBAGENT_LABEL}, ${APPROVE_BYPASS_SUBAGENT_LABEL}`;
286
402
  return [
287
- "Present approval options now using ask_user_questions with exactly two single-select questions.",
288
- `First question id: \"${PLAN_APPROVAL_ACTION_QUESTION_ID}\". Ask what to do next with the plan.`,
289
- "Use exactly these 3 options for the first question:",
290
- `1. ${APPROVE_LABEL} (Recommended)`,
291
- `2. ${REVIEW_LABEL}`,
292
- `3. ${REVISE_LABEL}`,
293
- `Second question id: \"${PLAN_APPROVAL_PERMISSION_QUESTION_ID}\". Ask which execution mode to use if the plan is approved.`,
294
- "Use exactly these 4 options for the second question:",
295
- `1. ${APPROVE_AUTO_LABEL} (Recommended)`,
296
- `2. ${APPROVE_BYPASS_LABEL}`,
297
- `3. ${APPROVE_AUTO_SUBAGENT_LABEL}`,
298
- `4. ${APPROVE_BYPASS_SUBAGENT_LABEL}`,
299
- `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.`,
300
- `If the user selects \"${REVIEW_LABEL}\" or \"${REVISE_LABEL}\", ignore the second answer for now.`,
301
- "If the dialog is dismissed or the user gives no answer, continue planning.",
403
+ "Plan approved. Now ask which execution mode to use via ask_user_questions.",
404
+ `One single-select question with id \"${PLAN_APPROVAL_PERMISSION_QUESTION_ID}\".`,
405
+ `Options: ${options}.`,
302
406
  ].join(" ");
303
407
  }
408
+ // Keep for external callers that reference the combined form (headless path)
409
+ function buildApprovalDialogInstructions() {
410
+ return buildApprovalActionInstructions();
411
+ }
304
412
  function buildApprovalSteeringMessage(planPath) {
305
- const details = [
413
+ return [
306
414
  `Plan artifact saved at ${planPath}.`,
307
- "Do not restate the plan in a normal assistant response.",
308
- "Ask for approval now via ask_user_questions.",
309
- buildApprovalDialogInstructions(),
310
- ];
311
- return details.join("\n\n");
415
+ buildApprovalActionInstructions(),
416
+ ].join("\n\n");
312
417
  }
313
418
  function buildPlanPreviewMessage(planPath, planMarkdown) {
314
419
  const details = [
@@ -342,7 +447,7 @@ function buildReviewSteeringMessage(planPath, planMarkdown) {
342
447
  else {
343
448
  details.push(`Have the subagent read ${planPath} before reviewing it.`);
344
449
  }
345
- details.push("After the subagent responds, summarize its feedback for the user, present the current plan again, and then ask for approval again.", buildApprovalDialogInstructions());
450
+ details.push("After the subagent responds, summarize its feedback for the user, present the current plan again, and then ask for approval again.", buildApprovalActionInstructions());
346
451
  return details.join("\n\n");
347
452
  }
348
453
  function approvalSelectionToExecutionMode(selected) {
@@ -397,6 +502,9 @@ export const __testing = {
397
502
  buildApprovalSteeringMessage,
398
503
  buildPlanPreviewMessage,
399
504
  buildReviewSteeringMessage,
505
+ buildAutoSuggestPlanModeSystemPrompt,
506
+ readAutoSuggestPlanModeSetting,
507
+ PLAN_SUGGEST_QUESTION_ID,
400
508
  };
401
509
  export default function planCommand(pi) {
402
510
  pi.registerFlag("plan", {
@@ -406,16 +514,27 @@ export default function planCommand(pi) {
406
514
  pi.on("session_start", async (_event, ctx) => {
407
515
  restoreStateFromSession(ctx);
408
516
  startedFromFlag = false;
517
+ reasoningModelSwitchDone = false;
409
518
  if (state.active) {
410
519
  setPermissionModeAndEnv("plan");
411
520
  }
412
521
  });
413
- pi.on("before_agent_start", async () => {
414
- if (!isPlanModeActive())
415
- return;
416
- return {
417
- systemPrompt: buildPlanModeSystemPrompt(),
418
- };
522
+ pi.on("before_agent_start", async (_event, ctx) => {
523
+ if (isPlanModeActive()) {
524
+ // Switch to reasoning model once per plan mode activation
525
+ if (!reasoningModelSwitchDone && readAutoSwitchPlanModelSetting()) {
526
+ const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
527
+ if (reasoningModel) {
528
+ reasoningModelSwitchDone = true;
529
+ await setModelIfNeeded(pi, ctx, reasoningModel);
530
+ }
531
+ }
532
+ return { systemPrompt: buildPlanModeSystemPrompt() };
533
+ }
534
+ if (readAutoSuggestPlanModeSetting()) {
535
+ return { systemPrompt: buildAutoSuggestPlanModeSystemPrompt() };
536
+ }
537
+ return;
419
538
  });
420
539
  pi.on("input", async (event, ctx) => {
421
540
  const planFlag = pi.getFlag("plan");
@@ -424,7 +543,7 @@ export default function planCommand(pi) {
424
543
  }
425
544
  startedFromFlag = true;
426
545
  ensurePlanDir();
427
- enablePlanMode(pi, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
546
+ await enablePlanModeWithModelSwitch(pi, ctx, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
428
547
  task: event.text.trim(),
429
548
  approvalStatus: "pending",
430
549
  latestPlanPath: undefined,
@@ -486,6 +605,28 @@ export default function planCommand(pi) {
486
605
  }
487
606
  return;
488
607
  }
608
+ if (event.toolName === "ask_user_questions" && !isPlanModeActive()) {
609
+ const details = event.details;
610
+ if (!details?.cancelled && details?.response?.answers) {
611
+ const suggestAnswer = details.response.answers[PLAN_SUGGEST_QUESTION_ID];
612
+ if (suggestAnswer) {
613
+ const selected = Array.isArray(suggestAnswer.selected) ? suggestAnswer.selected[0] : suggestAnswer.selected;
614
+ if (typeof selected === "string" && selected.toLowerCase().includes("yes")) {
615
+ ensurePlanDir();
616
+ await enablePlanModeWithModelSwitch(pi, ctx, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
617
+ task: state.task,
618
+ latestPlanPath: undefined,
619
+ approvalStatus: "pending",
620
+ targetPermissionMode: undefined,
621
+ });
622
+ ctx.ui?.notify?.("Plan mode enabled. Investigate and produce a plan before making changes.", "info");
623
+ pi.sendUserMessage("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.", { deliverAs: "steer" });
624
+ }
625
+ return;
626
+ }
627
+ }
628
+ return;
629
+ }
489
630
  if (!isPlanModeActive() || event.toolName !== "ask_user_questions")
490
631
  return;
491
632
  const details = event.details;
@@ -495,24 +636,21 @@ export default function planCommand(pi) {
495
636
  const permissionAnswer = details.response.answers[PLAN_APPROVAL_PERMISSION_QUESTION_ID];
496
637
  const actionValues = getAnswerValues(actionAnswer);
497
638
  const permissionValues = getAnswerValues(permissionAnswer);
498
- if (actionValues.length === 0 && permissionValues.length === 0)
499
- return;
500
- if (selectionRequestsCancel([...actionValues, ...permissionValues])) {
501
- await cancelPlan(pi, ctx, true);
502
- return;
503
- }
504
- const actionSelection = actionValues[0];
505
- if (!actionSelection)
506
- return;
507
- if (actionSelection.includes(APPROVE_LABEL)) {
639
+ // ── Second question answered (execution mode) ─────────────────────────
640
+ if (permissionValues.length > 0) {
641
+ if (selectionRequestsCancel(permissionValues)) {
642
+ await cancelPlan(pi, ctx, true);
643
+ return;
644
+ }
645
+ if (permissionValues[0]?.includes(APPROVE_NEW_SESSION_LABEL)) {
646
+ scheduleNewSession(pi, ctx);
647
+ return;
648
+ }
508
649
  const executionMode = approvalSelectionToExecutionMode(permissionValues[0]) ?? {
509
650
  permissionMode: DEFAULT_APPROVAL_PERMISSION_MODE,
510
651
  executeWithSubagent: false,
511
652
  };
512
- state = {
513
- ...state,
514
- targetPermissionMode: executionMode.permissionMode,
515
- };
653
+ state = { ...state, targetPermissionMode: executionMode.permissionMode };
516
654
  if (executionMode.executeWithSubagent) {
517
655
  const modeLabel = executionMode.permissionMode === "danger-full-access" ? "bypass" : "auto";
518
656
  ctx.ui?.notify?.(`Plan approved: subagent(${modeLabel})`, "info");
@@ -520,6 +658,21 @@ export default function planCommand(pi) {
520
658
  await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent);
521
659
  return;
522
660
  }
661
+ // ── First question answered (action) ──────────────────────────────────
662
+ if (actionValues.length === 0)
663
+ return;
664
+ if (selectionRequestsCancel(actionValues)) {
665
+ await cancelPlan(pi, ctx, true);
666
+ return;
667
+ }
668
+ const actionSelection = actionValues[0];
669
+ if (!actionSelection)
670
+ return;
671
+ if (actionSelection.includes(APPROVE_LABEL)) {
672
+ // Steer the second question — handle in the next tool_result cycle
673
+ pi.sendUserMessage(buildApprovalModeInstructions(), { deliverAs: "steer" });
674
+ return;
675
+ }
523
676
  if (actionSelection.includes(REVIEW_LABEL)) {
524
677
  setState(pi, {
525
678
  ...state,
@@ -557,15 +710,16 @@ export default function planCommand(pi) {
557
710
  }
558
711
  ensurePlanDir();
559
712
  const task = args.trim();
560
- enablePlanMode(pi, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
713
+ await enablePlanModeWithModelSwitch(pi, ctx, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
561
714
  task,
562
715
  latestPlanPath: undefined,
563
716
  approvalStatus: "pending",
564
717
  targetPermissionMode: undefined,
565
718
  });
719
+ const reasoningModel = readAutoSwitchPlanModelSetting() ? readPlanModeReasoningModel() : undefined;
566
720
  ctx.ui.notify(task
567
- ? `Plan mode enabled. Current task: ${task}`
568
- : "Plan mode enabled. Investigation is allowed; source changes stay blocked until you exit plan mode.", "info");
721
+ ? `Plan mode enabled${reasoningModel ? ` · ${reasoningModel.split("/")[1] ?? reasoningModel}` : ""}. Current task: ${task}`
722
+ : `Plan mode enabled${reasoningModel ? ` · ${reasoningModel.split("/")[1] ?? reasoningModel}` : ""}. Investigation is allowed; source changes stay blocked until you exit plan mode.`, "info");
569
723
  },
570
724
  });
571
725
  pi.registerCommand("execute", {
@@ -591,4 +745,37 @@ export default function planCommand(pi) {
591
745
  ctx.ui.notify("Plan mode cancelled.", "info");
592
746
  },
593
747
  });
748
+ // Internal command — called by scheduleNewSession() via pi.executeSlashCommand().
749
+ // Runs in ExtensionCommandContext so ctx.newSession() is available.
750
+ pi.registerCommand("plan-execute-new-session", {
751
+ description: "Internal: execute approved plan in a new session with the coding model",
752
+ async handler(_args, ctx) {
753
+ const payload = pendingNewSession;
754
+ pendingNewSession = null;
755
+ if (!payload)
756
+ return;
757
+ // Switch to coding model first
758
+ if (payload.codingModelRef) {
759
+ await setModelIfNeeded(pi, ctx, payload.codingModelRef);
760
+ }
761
+ const result = await ctx.newSession();
762
+ if (result.cancelled)
763
+ return;
764
+ // Inject plan into the new session as a steer message
765
+ const parts = [
766
+ `Plan approved. You are acting as the ${payload.codingSubagent} agent. Implement the following plan now without re-investigating or re-planning.`,
767
+ ];
768
+ if (payload.task)
769
+ parts.push(`Original task: ${payload.task}`);
770
+ if (payload.planPath)
771
+ parts.push(`Plan artifact: ${payload.planPath}`);
772
+ if (payload.planContent) {
773
+ parts.push(`Full plan:\n\`\`\`markdown\n${payload.planContent}\n\`\`\``);
774
+ }
775
+ else if (payload.planPath) {
776
+ parts.push(`Read the plan from ${payload.planPath} before starting.`);
777
+ }
778
+ pi.sendUserMessage(parts.join("\n\n"), { deliverAs: "steer" });
779
+ },
780
+ });
594
781
  }