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