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.
- package/dist/resources/extensions/slash-commands/index.js +2 -0
- package/dist/resources/extensions/slash-commands/init.js +47 -0
- package/dist/resources/extensions/slash-commands/plan.js +231 -50
- package/dist/resources/extensions/slash-commands/tools.js +14 -27
- package/dist/resources/extensions/subagent/index.js +5 -10
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +11 -5
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader-lsd-md.test.js +59 -7
- package/packages/pi-coding-agent/dist/core/resource-loader-lsd-md.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +4 -4
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +18 -7
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.test.js +80 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +12 -5
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +23 -9
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +8 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +32 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +8 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +34 -25
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +13 -5
- package/packages/pi-coding-agent/src/core/resource-loader-lsd-md.test.ts +67 -7
- package/packages/pi-coding-agent/src/core/resource-loader.ts +4 -4
- package/packages/pi-coding-agent/src/core/sdk.test.ts +100 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +23 -8
- package/packages/pi-coding-agent/src/core/settings-manager.ts +36 -15
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +41 -10
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +11 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +43 -27
- package/pkg/package.json +1 -1
- package/src/resources/extensions/slash-commands/index.ts +2 -0
- package/src/resources/extensions/slash-commands/init.ts +55 -0
- package/src/resources/extensions/slash-commands/plan.ts +268 -52
- package/src/resources/extensions/slash-commands/tools.ts +15 -29
- package/src/resources/extensions/subagent/index.ts +5 -10
|
@@ -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
|
+
}
|
|
@@ -4,6 +4,7 @@ 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
|
}
|
|
@@ -175,6 +224,16 @@ function restoreStateFromSession(ctx) {
|
|
|
175
224
|
// Best-effort restore only.
|
|
176
225
|
}
|
|
177
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
|
+
}
|
|
178
237
|
function enablePlanMode(pi, currentModel, next = {}) {
|
|
179
238
|
const currentMode = getPermissionMode();
|
|
180
239
|
const enteringPlanMode = currentMode !== "plan";
|
|
@@ -193,6 +252,7 @@ function enablePlanMode(pi, currentModel, next = {}) {
|
|
|
193
252
|
});
|
|
194
253
|
}
|
|
195
254
|
function leavePlanMode(pi, approvalStatus, nextPermissionMode, clearTask = false) {
|
|
255
|
+
reasoningModelSwitchDone = false;
|
|
196
256
|
setPermissionModeAndEnv(nextPermissionMode);
|
|
197
257
|
setState(pi, {
|
|
198
258
|
active: false,
|
|
@@ -230,9 +290,10 @@ function buildExecutionKickoffMessage(options) {
|
|
|
230
290
|
return details.join(" ");
|
|
231
291
|
}
|
|
232
292
|
const codingModel = readPlanModeCodingModel();
|
|
293
|
+
const codingSubagent = readPlanModeCodingSubagent() ?? DEFAULT_PLAN_CODING_AGENT;
|
|
233
294
|
const agentInvocationInstruction = codingModel
|
|
234
|
-
? `Invoke the subagent tool with agent "
|
|
235
|
-
: `Invoke the subagent tool with agent "
|
|
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.`;
|
|
236
297
|
const details = [
|
|
237
298
|
"Plan approved. Exit plan mode and execute the approved plan with a subagent now.",
|
|
238
299
|
agentInvocationInstruction,
|
|
@@ -245,6 +306,25 @@ function buildExecutionKickoffMessage(options) {
|
|
|
245
306
|
details.push("After subagent completion, summarize the result and any remaining follow-ups.");
|
|
246
307
|
return details.join(" ");
|
|
247
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
|
+
}
|
|
248
328
|
async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false) {
|
|
249
329
|
const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
|
|
250
330
|
if (reasoningModel) {
|
|
@@ -255,7 +335,13 @@ async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false)
|
|
|
255
335
|
targetPermissionMode: permissionMode,
|
|
256
336
|
};
|
|
257
337
|
leavePlanMode(pi, "approved", permissionMode);
|
|
258
|
-
|
|
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" });
|
|
259
345
|
}
|
|
260
346
|
async function cancelPlan(pi, ctx, clearTask = true) {
|
|
261
347
|
const restoreMode = state.previousMode ?? "accept-on-edit";
|
|
@@ -288,33 +374,46 @@ function readPlanArtifact(planPath) {
|
|
|
288
374
|
return undefined;
|
|
289
375
|
}
|
|
290
376
|
}
|
|
291
|
-
function
|
|
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() {
|
|
292
387
|
return [
|
|
293
|
-
"
|
|
294
|
-
`
|
|
295
|
-
|
|
296
|
-
`
|
|
297
|
-
|
|
298
|
-
`3. ${REVISE_LABEL}`,
|
|
299
|
-
`Second question id: \"${PLAN_APPROVAL_PERMISSION_QUESTION_ID}\". Ask which execution mode to use if the plan is approved.`,
|
|
300
|
-
"Use exactly these 4 options for the second question:",
|
|
301
|
-
`1. ${APPROVE_AUTO_LABEL} (Recommended)`,
|
|
302
|
-
`2. ${APPROVE_BYPASS_LABEL}`,
|
|
303
|
-
`3. ${APPROVE_AUTO_SUBAGENT_LABEL}`,
|
|
304
|
-
`4. ${APPROVE_BYPASS_SUBAGENT_LABEL}`,
|
|
305
|
-
`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.`,
|
|
306
|
-
`If the user selects \"${REVIEW_LABEL}\" or \"${REVISE_LABEL}\", ignore the second answer for now.`,
|
|
307
|
-
"If the dialog is dismissed or the user gives no answer, continue planning.",
|
|
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.",
|
|
308
393
|
].join(" ");
|
|
309
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}`;
|
|
402
|
+
return [
|
|
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}.`,
|
|
406
|
+
].join(" ");
|
|
407
|
+
}
|
|
408
|
+
// Keep for external callers that reference the combined form (headless path)
|
|
409
|
+
function buildApprovalDialogInstructions() {
|
|
410
|
+
return buildApprovalActionInstructions();
|
|
411
|
+
}
|
|
310
412
|
function buildApprovalSteeringMessage(planPath) {
|
|
311
|
-
|
|
413
|
+
return [
|
|
312
414
|
`Plan artifact saved at ${planPath}.`,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
buildApprovalDialogInstructions(),
|
|
316
|
-
];
|
|
317
|
-
return details.join("\n\n");
|
|
415
|
+
buildApprovalActionInstructions(),
|
|
416
|
+
].join("\n\n");
|
|
318
417
|
}
|
|
319
418
|
function buildPlanPreviewMessage(planPath, planMarkdown) {
|
|
320
419
|
const details = [
|
|
@@ -348,7 +447,7 @@ function buildReviewSteeringMessage(planPath, planMarkdown) {
|
|
|
348
447
|
else {
|
|
349
448
|
details.push(`Have the subagent read ${planPath} before reviewing it.`);
|
|
350
449
|
}
|
|
351
|
-
details.push("After the subagent responds, summarize its feedback for the user, present the current plan again, and then ask for approval again.",
|
|
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());
|
|
352
451
|
return details.join("\n\n");
|
|
353
452
|
}
|
|
354
453
|
function approvalSelectionToExecutionMode(selected) {
|
|
@@ -403,6 +502,9 @@ export const __testing = {
|
|
|
403
502
|
buildApprovalSteeringMessage,
|
|
404
503
|
buildPlanPreviewMessage,
|
|
405
504
|
buildReviewSteeringMessage,
|
|
505
|
+
buildAutoSuggestPlanModeSystemPrompt,
|
|
506
|
+
readAutoSuggestPlanModeSetting,
|
|
507
|
+
PLAN_SUGGEST_QUESTION_ID,
|
|
406
508
|
};
|
|
407
509
|
export default function planCommand(pi) {
|
|
408
510
|
pi.registerFlag("plan", {
|
|
@@ -412,16 +514,27 @@ export default function planCommand(pi) {
|
|
|
412
514
|
pi.on("session_start", async (_event, ctx) => {
|
|
413
515
|
restoreStateFromSession(ctx);
|
|
414
516
|
startedFromFlag = false;
|
|
517
|
+
reasoningModelSwitchDone = false;
|
|
415
518
|
if (state.active) {
|
|
416
519
|
setPermissionModeAndEnv("plan");
|
|
417
520
|
}
|
|
418
521
|
});
|
|
419
|
-
pi.on("before_agent_start", async () => {
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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;
|
|
425
538
|
});
|
|
426
539
|
pi.on("input", async (event, ctx) => {
|
|
427
540
|
const planFlag = pi.getFlag("plan");
|
|
@@ -430,7 +543,7 @@ export default function planCommand(pi) {
|
|
|
430
543
|
}
|
|
431
544
|
startedFromFlag = true;
|
|
432
545
|
ensurePlanDir();
|
|
433
|
-
|
|
546
|
+
await enablePlanModeWithModelSwitch(pi, ctx, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
|
|
434
547
|
task: event.text.trim(),
|
|
435
548
|
approvalStatus: "pending",
|
|
436
549
|
latestPlanPath: undefined,
|
|
@@ -492,6 +605,28 @@ export default function planCommand(pi) {
|
|
|
492
605
|
}
|
|
493
606
|
return;
|
|
494
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
|
+
}
|
|
495
630
|
if (!isPlanModeActive() || event.toolName !== "ask_user_questions")
|
|
496
631
|
return;
|
|
497
632
|
const details = event.details;
|
|
@@ -501,24 +636,21 @@ export default function planCommand(pi) {
|
|
|
501
636
|
const permissionAnswer = details.response.answers[PLAN_APPROVAL_PERMISSION_QUESTION_ID];
|
|
502
637
|
const actionValues = getAnswerValues(actionAnswer);
|
|
503
638
|
const permissionValues = getAnswerValues(permissionAnswer);
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
+
}
|
|
514
649
|
const executionMode = approvalSelectionToExecutionMode(permissionValues[0]) ?? {
|
|
515
650
|
permissionMode: DEFAULT_APPROVAL_PERMISSION_MODE,
|
|
516
651
|
executeWithSubagent: false,
|
|
517
652
|
};
|
|
518
|
-
state = {
|
|
519
|
-
...state,
|
|
520
|
-
targetPermissionMode: executionMode.permissionMode,
|
|
521
|
-
};
|
|
653
|
+
state = { ...state, targetPermissionMode: executionMode.permissionMode };
|
|
522
654
|
if (executionMode.executeWithSubagent) {
|
|
523
655
|
const modeLabel = executionMode.permissionMode === "danger-full-access" ? "bypass" : "auto";
|
|
524
656
|
ctx.ui?.notify?.(`Plan approved: subagent(${modeLabel})`, "info");
|
|
@@ -526,6 +658,21 @@ export default function planCommand(pi) {
|
|
|
526
658
|
await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent);
|
|
527
659
|
return;
|
|
528
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
|
+
}
|
|
529
676
|
if (actionSelection.includes(REVIEW_LABEL)) {
|
|
530
677
|
setState(pi, {
|
|
531
678
|
...state,
|
|
@@ -563,15 +710,16 @@ export default function planCommand(pi) {
|
|
|
563
710
|
}
|
|
564
711
|
ensurePlanDir();
|
|
565
712
|
const task = args.trim();
|
|
566
|
-
|
|
713
|
+
await enablePlanModeWithModelSwitch(pi, ctx, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
|
|
567
714
|
task,
|
|
568
715
|
latestPlanPath: undefined,
|
|
569
716
|
approvalStatus: "pending",
|
|
570
717
|
targetPermissionMode: undefined,
|
|
571
718
|
});
|
|
719
|
+
const reasoningModel = readAutoSwitchPlanModelSetting() ? readPlanModeReasoningModel() : undefined;
|
|
572
720
|
ctx.ui.notify(task
|
|
573
|
-
? `Plan mode enabled. Current task: ${task}`
|
|
574
|
-
:
|
|
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");
|
|
575
723
|
},
|
|
576
724
|
});
|
|
577
725
|
pi.registerCommand("execute", {
|
|
@@ -597,4 +745,37 @@ export default function planCommand(pi) {
|
|
|
597
745
|
ctx.ui.notify("Plan mode cancelled.", "info");
|
|
598
746
|
},
|
|
599
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
|
+
});
|
|
600
781
|
}
|
|
@@ -6,11 +6,6 @@ function getSettingsManager() {
|
|
|
6
6
|
function isHashlineMode(activeToolNames) {
|
|
7
7
|
return activeToolNames.includes("hashline_read") || activeToolNames.includes("hashline_edit");
|
|
8
8
|
}
|
|
9
|
-
function getCoreToolNames(activeToolNames) {
|
|
10
|
-
return isHashlineMode(activeToolNames)
|
|
11
|
-
? ["hashline_read", "bash", "lsp", "tool_search", "tool_enable"]
|
|
12
|
-
: ["read", "bash", "lsp", "tool_search", "tool_enable"];
|
|
13
|
-
}
|
|
14
9
|
function getBalancedToolNames(activeToolNames) {
|
|
15
10
|
return isHashlineMode(activeToolNames)
|
|
16
11
|
? [
|
|
@@ -25,6 +20,7 @@ function getBalancedToolNames(activeToolNames) {
|
|
|
25
20
|
"Skill",
|
|
26
21
|
"subagent",
|
|
27
22
|
"await_subagent",
|
|
23
|
+
"ask_user_questions",
|
|
28
24
|
]
|
|
29
25
|
: [
|
|
30
26
|
"read",
|
|
@@ -38,8 +34,12 @@ function getBalancedToolNames(activeToolNames) {
|
|
|
38
34
|
"Skill",
|
|
39
35
|
"subagent",
|
|
40
36
|
"await_subagent",
|
|
37
|
+
"ask_user_questions",
|
|
41
38
|
];
|
|
42
39
|
}
|
|
40
|
+
function getFullToolNames(pi) {
|
|
41
|
+
return pi.getAllTools().map((tool) => tool.name).filter((name) => Boolean(name));
|
|
42
|
+
}
|
|
43
43
|
function scoreTool(query, tool) {
|
|
44
44
|
const name = (tool.name ?? "").toLowerCase();
|
|
45
45
|
const description = (tool.description ?? "").toLowerCase();
|
|
@@ -153,43 +153,30 @@ export default function toolSearchExtension(pi) {
|
|
|
153
153
|
},
|
|
154
154
|
});
|
|
155
155
|
pi.registerCommand("tools", {
|
|
156
|
-
description: "
|
|
156
|
+
description: "Manage default tool profiles",
|
|
157
157
|
handler: async (args, _ctx) => {
|
|
158
158
|
const input = args.trim();
|
|
159
159
|
const settings = getSettingsManager();
|
|
160
160
|
const currentActive = pi.getActiveTools();
|
|
161
161
|
const toolProfile = settings.getToolProfile();
|
|
162
|
-
const toolSearchEnabled = toolProfile === "minimal";
|
|
163
162
|
if (!input) {
|
|
164
163
|
pi.sendMessage({
|
|
165
164
|
customType: "tools:status",
|
|
166
165
|
content: [
|
|
167
166
|
`Tool profile: ${toolProfile}`,
|
|
168
|
-
`Tool search mode: ${toolSearchEnabled ? "on" : "off"}`,
|
|
169
167
|
`Active tools: ${currentActive.length}`,
|
|
170
168
|
currentActive.length > 0 ? currentActive.join(", ") : "(none)",
|
|
171
169
|
"",
|
|
172
170
|
"Usage:",
|
|
173
|
-
" /tools on Enable lazy tool-search mode and switch to a small core tool set",
|
|
174
|
-
" /tools off Disable lazy tool-search mode and restore the balanced tool profile",
|
|
175
171
|
" /tools balanced Switch to the balanced tool profile",
|
|
176
|
-
" /tools
|
|
172
|
+
" /tools full Switch to the full tool profile (all available tools)",
|
|
173
|
+
" /tools on Alias for /tools full",
|
|
174
|
+
" /tools off Alias for /tools balanced",
|
|
177
175
|
].join("\n"),
|
|
178
176
|
display: true,
|
|
179
177
|
});
|
|
180
178
|
return;
|
|
181
179
|
}
|
|
182
|
-
if (["on", "enable", "mode on"].includes(input)) {
|
|
183
|
-
settings.setToolProfile("minimal");
|
|
184
|
-
const nextActive = getCoreToolNames(currentActive);
|
|
185
|
-
pi.setActiveTools(nextActive);
|
|
186
|
-
pi.sendMessage({
|
|
187
|
-
customType: "tools:mode",
|
|
188
|
-
content: `Tool search mode enabled. Active tools reduced to: ${pi.getActiveTools().join(", ")}`,
|
|
189
|
-
display: true,
|
|
190
|
-
});
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
180
|
if (["off", "disable", "mode off", "balanced", "default"].includes(input)) {
|
|
194
181
|
settings.setToolProfile("balanced");
|
|
195
182
|
const nextActive = getBalancedToolNames(currentActive);
|
|
@@ -201,20 +188,20 @@ export default function toolSearchExtension(pi) {
|
|
|
201
188
|
});
|
|
202
189
|
return;
|
|
203
190
|
}
|
|
204
|
-
if (["
|
|
205
|
-
settings.setToolProfile("
|
|
206
|
-
const nextActive =
|
|
191
|
+
if (["on", "enable", "mode on", "full", "all"].includes(input)) {
|
|
192
|
+
settings.setToolProfile("full");
|
|
193
|
+
const nextActive = getFullToolNames(pi);
|
|
207
194
|
pi.setActiveTools(nextActive);
|
|
208
195
|
pi.sendMessage({
|
|
209
196
|
customType: "tools:mode",
|
|
210
|
-
content: `
|
|
197
|
+
content: `Full tool profile active: ${pi.getActiveTools().join(", ")}`,
|
|
211
198
|
display: true,
|
|
212
199
|
});
|
|
213
200
|
return;
|
|
214
201
|
}
|
|
215
202
|
pi.sendMessage({
|
|
216
203
|
customType: "tools:help",
|
|
217
|
-
content: `Unknown /tools subcommand: ${input}\n\nTry /tools, /tools
|
|
204
|
+
content: `Unknown /tools subcommand: ${input}\n\nTry /tools, /tools balanced, /tools full, /tools on, or /tools off.`,
|
|
218
205
|
display: true,
|
|
219
206
|
});
|
|
220
207
|
},
|