lsd-pi 1.1.2 → 1.1.3

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 (183) hide show
  1. package/README.md +2 -1
  2. package/dist/bedrock-auth.d.ts +25 -0
  3. package/dist/bedrock-auth.js +59 -0
  4. package/dist/headless.js +8 -3
  5. package/dist/loader.js +1 -0
  6. package/dist/onboarding-llm.d.ts +37 -0
  7. package/dist/onboarding-llm.js +64 -0
  8. package/dist/onboarding.d.ts +2 -14
  9. package/dist/onboarding.js +146 -71
  10. package/dist/pi-migration.js +1 -0
  11. package/dist/resources/extensions/memory/auto-extract.js +21 -3
  12. package/dist/resources/extensions/memory/dream.js +703 -0
  13. package/dist/resources/extensions/memory/extension-manifest.json +2 -2
  14. package/dist/resources/extensions/memory/index.js +115 -8
  15. package/dist/resources/extensions/slash-commands/extension-manifest.json +10 -10
  16. package/dist/resources/extensions/slash-commands/index.js +0 -4
  17. package/dist/resources/extensions/slash-commands/plan.js +181 -45
  18. package/dist/resources/extensions/subagent/agents.js +14 -1
  19. package/dist/resources/extensions/subagent/configured-model.js +3 -2
  20. package/dist/resources/extensions/subagent/index.js +34 -28
  21. package/dist/resources/extensions/subagent/launch-helpers.js +24 -0
  22. package/dist/resources/extensions/subagent/model-resolution.js +41 -3
  23. package/dist/resources/extensions/usage/extension-manifest.json +11 -0
  24. package/dist/resources/extensions/usage/index.js +346 -0
  25. package/{src/resources/skills/create-gsd-extension → dist/resources/skills/create-lsd-extension}/SKILL.md +6 -6
  26. package/{src/resources/skills/create-gsd-extension → dist/resources/skills/create-lsd-extension}/references/custom-tools.md +1 -1
  27. package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/extension-lifecycle.md +2 -2
  28. package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/extensioncontext-reference.md +1 -1
  29. package/{src/resources/skills/create-gsd-extension → dist/resources/skills/create-lsd-extension}/references/key-rules-gotchas.md +4 -4
  30. package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/packaging-distribution.md +6 -6
  31. package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/workflows/create-extension.md +3 -3
  32. package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/workflows/debug-extension.md +5 -5
  33. package/dist/resources/skills/teams-debug/SKILL.md +5 -6
  34. package/dist/resources/skills/teams-document/SKILL.md +1 -2
  35. package/dist/resources/skills/teams-plan/SKILL.md +3 -4
  36. package/dist/resources/skills/teams-run/SKILL.md +3 -4
  37. package/dist/resources/skills/teams-verify/SKILL.md +4 -5
  38. package/dist/startup-model-validation.js +1 -0
  39. package/dist/welcome-screen.js +13 -11
  40. package/dist/wizard.js +12 -0
  41. package/package.json +1 -1
  42. package/packages/pi-ai/dist/models.generated.d.ts +688 -409
  43. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  44. package/packages/pi-ai/dist/models.generated.js +761 -488
  45. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  46. package/packages/pi-ai/scripts/generate-models.ts +40 -18
  47. package/packages/pi-ai/src/models.generated.ts +759 -486
  48. package/packages/pi-coding-agent/dist/cli/config-selector.js +1 -1
  49. package/packages/pi-coding-agent/dist/cli/config-selector.js.map +1 -1
  50. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +1 -2
  51. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/agent-session.js +6 -30
  53. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  55. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  56. package/packages/pi-coding-agent/dist/core/settings-manager.js +44 -1
  57. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  58. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +9 -5
  59. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  61. package/packages/pi-coding-agent/dist/core/skills.js +3 -2
  62. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  63. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  64. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  65. package/packages/pi-coding-agent/dist/index.js +1 -1
  66. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  67. package/packages/pi-coding-agent/dist/main.js +1 -1
  68. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +2 -2
  70. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  71. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  72. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +15 -12
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +6 -0
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +25 -1
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +1 -1
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +10 -0
  82. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +31 -22
  87. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts +18 -5
  89. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +139 -20
  91. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +4 -0
  94. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  95. package/packages/pi-coding-agent/package.json +1 -1
  96. package/packages/pi-coding-agent/src/cli/config-selector.ts +1 -1
  97. package/packages/pi-coding-agent/src/core/agent-session.ts +5 -28
  98. package/packages/pi-coding-agent/src/core/settings-manager.ts +52 -1
  99. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +18 -5
  100. package/packages/pi-coding-agent/src/core/skills.ts +3 -2
  101. package/packages/pi-coding-agent/src/index.ts +1 -1
  102. package/packages/pi-coding-agent/src/main.ts +1 -1
  103. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +2 -2
  104. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +12 -13
  105. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +39 -1
  106. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +1 -1
  107. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +10 -0
  108. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -1
  109. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +46 -20
  110. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +171 -20
  111. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +4 -0
  112. package/packages/pi-tui/dist/components/editor.d.ts +1 -0
  113. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  114. package/packages/pi-tui/dist/components/editor.js +23 -0
  115. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  116. package/packages/pi-tui/src/components/editor.ts +23 -0
  117. package/pkg/dist/modes/interactive/theme/theme.d.ts +18 -5
  118. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  119. package/pkg/dist/modes/interactive/theme/theme.js +139 -20
  120. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  121. package/pkg/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  122. package/pkg/dist/modes/interactive/theme/themes.js +4 -0
  123. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  124. package/pkg/package.json +1 -1
  125. package/src/resources/extensions/memory/auto-extract.ts +23 -3
  126. package/src/resources/extensions/memory/dream.ts +814 -0
  127. package/src/resources/extensions/memory/extension-manifest.json +2 -2
  128. package/src/resources/extensions/memory/index.ts +134 -13
  129. package/src/resources/extensions/memory/tests/auto-extract.test.ts +10 -2
  130. package/src/resources/extensions/memory/tests/dream.test.ts +142 -0
  131. package/src/resources/extensions/slash-commands/extension-manifest.json +10 -10
  132. package/src/resources/extensions/slash-commands/index.ts +3 -7
  133. package/src/resources/extensions/slash-commands/plan.ts +192 -46
  134. package/src/resources/extensions/subagent/agents.ts +11 -1
  135. package/src/resources/extensions/subagent/configured-model.ts +3 -2
  136. package/src/resources/extensions/subagent/index.ts +38 -30
  137. package/src/resources/extensions/subagent/launch-helpers.ts +30 -0
  138. package/src/resources/extensions/subagent/model-resolution.ts +40 -3
  139. package/src/resources/extensions/usage/extension-manifest.json +11 -0
  140. package/src/resources/extensions/usage/index.ts +441 -0
  141. package/{dist/resources/skills/create-gsd-extension → src/resources/skills/create-lsd-extension}/SKILL.md +6 -6
  142. package/{dist/resources/skills/create-gsd-extension → src/resources/skills/create-lsd-extension}/references/custom-tools.md +1 -1
  143. package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/extension-lifecycle.md +2 -2
  144. package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/extensioncontext-reference.md +1 -1
  145. package/{dist/resources/skills/create-gsd-extension → src/resources/skills/create-lsd-extension}/references/key-rules-gotchas.md +4 -4
  146. package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/packaging-distribution.md +6 -6
  147. package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/workflows/create-extension.md +3 -3
  148. package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/workflows/debug-extension.md +5 -5
  149. package/src/resources/skills/teams-debug/SKILL.md +5 -6
  150. package/src/resources/skills/teams-document/SKILL.md +1 -2
  151. package/src/resources/skills/teams-plan/SKILL.md +3 -4
  152. package/src/resources/skills/teams-run/SKILL.md +3 -4
  153. package/src/resources/skills/teams-verify/SKILL.md +4 -5
  154. package/dist/resources/extensions/slash-commands/create-extension.js +0 -264
  155. package/dist/resources/extensions/slash-commands/create-slash-command.js +0 -208
  156. package/src/resources/extensions/slash-commands/create-extension.ts +0 -297
  157. package/src/resources/extensions/slash-commands/create-slash-command.ts +0 -234
  158. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/compaction-session-control.md +0 -0
  159. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/custom-commands.md +0 -0
  160. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/custom-rendering.md +0 -0
  161. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/custom-ui.md +0 -0
  162. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/events-reference.md +0 -0
  163. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/extensionapi-reference.md +0 -0
  164. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/mode-behavior.md +0 -0
  165. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/model-provider-management.md +0 -0
  166. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/remote-execution-overrides.md +0 -0
  167. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/state-management.md +0 -0
  168. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/references/system-prompt-modification.md +0 -0
  169. /package/dist/resources/skills/{create-gsd-extension → create-lsd-extension}/workflows/add-capability.md +0 -0
  170. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/compaction-session-control.md +0 -0
  171. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/custom-commands.md +0 -0
  172. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/custom-rendering.md +0 -0
  173. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/custom-ui.md +0 -0
  174. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/events-reference.md +0 -0
  175. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/extensionapi-reference.md +0 -0
  176. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/mode-behavior.md +0 -0
  177. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/model-provider-management.md +0 -0
  178. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/remote-execution-overrides.md +0 -0
  179. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/state-management.md +0 -0
  180. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/references/system-prompt-modification.md +0 -0
  181. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/templates/extension-skeleton.ts +0 -0
  182. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/templates/stateful-tool-skeleton.ts +0 -0
  183. /package/src/resources/skills/{create-gsd-extension → create-lsd-extension}/workflows/add-capability.md +0 -0
@@ -1,7 +1,6 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { getAgentDir, getPermissionMode, isToolCallEventType, setPermissionMode, type ExtensionAPI, type ExtensionCommandContext, type PermissionMode } from "@gsd/pi-coding-agent";
2
3
  import { join } from "node:path";
3
- import type { ExtensionAPI, ExtensionCommandContext, PermissionMode } from "@gsd/pi-coding-agent";
4
- import { getPermissionMode, isToolCallEventType, setPermissionMode } from "@gsd/pi-coding-agent";
5
4
 
6
5
  const PLAN_ENTRY_TYPE = "plan-mode-state";
7
6
  const PLAN_APPROVAL_QUESTION_ID = "plan_mode_approval";
@@ -58,9 +57,15 @@ const BLOCKED_TOOLS = new Set([
58
57
  "write",
59
58
  "edit",
60
59
  ]);
60
+ const DEFAULT_APPROVAL_PERMISSION_MODE: RestorablePermissionMode = "auto";
61
+ const APPROVE_AUTO_LABEL = "Approve & switch to Auto mode";
62
+ const APPROVE_BYPASS_LABEL = "Approve & switch to Bypass mode";
63
+ const REVISE_LABEL = "Revise plan";
64
+ const CANCEL_LABEL = "Cancel";
61
65
 
62
66
  type PlanApprovalStatus = "pending" | "approved" | "revising" | "cancelled";
63
67
  type RestorablePermissionMode = Exclude<PermissionMode, "plan">;
68
+ type ModelRef = { provider: string; id: string };
64
69
 
65
70
  interface PlanModeState {
66
71
  active: boolean;
@@ -68,19 +73,65 @@ interface PlanModeState {
68
73
  latestPlanPath?: string;
69
74
  approvalStatus: PlanApprovalStatus;
70
75
  previousMode?: RestorablePermissionMode;
76
+ preplanModel?: ModelRef;
77
+ targetPermissionMode?: PermissionMode;
71
78
  }
72
79
 
73
- let state: PlanModeState = {
80
+ const INITIAL_STATE: PlanModeState = {
74
81
  active: false,
75
82
  task: "",
83
+ latestPlanPath: undefined,
76
84
  approvalStatus: "cancelled",
85
+ previousMode: undefined,
86
+ preplanModel: undefined,
87
+ targetPermissionMode: undefined,
77
88
  };
89
+
90
+ let state: PlanModeState = { ...INITIAL_STATE };
78
91
  let startedFromFlag = false;
79
92
 
80
93
  function isPlanModeActive(): boolean {
81
94
  return getPermissionMode() === "plan";
82
95
  }
83
96
 
97
+ function parseQualifiedModelRef(value: unknown): ModelRef | undefined {
98
+ if (typeof value !== "string") return undefined;
99
+ const trimmed = value.trim();
100
+ if (!trimmed) return undefined;
101
+ const parts = trimmed.split("/");
102
+ if (parts.length !== 2) return undefined;
103
+ const [provider, id] = parts.map((part) => part.trim());
104
+ if (!provider || !id) return undefined;
105
+ return { provider, id };
106
+ }
107
+
108
+ export function readPlanModeReasoningModel(): string | undefined {
109
+ try {
110
+ const settingsPath = join(getAgentDir(), "settings.json");
111
+ if (!existsSync(settingsPath)) return undefined;
112
+ const raw = readFileSync(settingsPath, "utf-8");
113
+ const parsed = JSON.parse(raw) as { planModeReasoningModel?: unknown };
114
+ const model = parseQualifiedModelRef(parsed.planModeReasoningModel);
115
+ return model ? `${model.provider}/${model.id}` : undefined;
116
+ } catch {
117
+ return undefined;
118
+ }
119
+ }
120
+
121
+ function sameModel(left: ModelRef | undefined, right: ModelRef | undefined): boolean {
122
+ return !!left && !!right && left.provider === right.provider && left.id === right.id;
123
+ }
124
+
125
+ function resolveModelFromContext(ctx: any, modelRef: ModelRef): any | undefined {
126
+ const allModels = typeof ctx?.modelRegistry?.getAll === "function" ? ctx.modelRegistry.getAll() : [];
127
+ return allModels.find((model: any) => model.provider === modelRef.provider && model.id === modelRef.id);
128
+ }
129
+
130
+ function setPermissionModeAndEnv(mode: PermissionMode): void {
131
+ setPermissionMode(mode);
132
+ process.env.LUCENT_CODE_PERMISSION_MODE = mode;
133
+ }
134
+
84
135
  function saveState(pi: ExtensionAPI): void {
85
136
  pi.appendEntry<PlanModeState>(PLAN_ENTRY_TYPE, { ...state });
86
137
  }
@@ -90,15 +141,11 @@ function setState(pi: ExtensionAPI, next: PlanModeState): void {
90
141
  saveState(pi);
91
142
  }
92
143
 
93
- function clearState(pi: ExtensionAPI): void {
94
- state = {
95
- active: false,
96
- task: "",
97
- latestPlanPath: undefined,
98
- approvalStatus: "cancelled",
99
- previousMode: undefined,
100
- };
101
- saveState(pi);
144
+ function resetState(pi: ExtensionAPI, overrides: Partial<PlanModeState> = {}): void {
145
+ setState(pi, {
146
+ ...INITIAL_STATE,
147
+ ...overrides,
148
+ });
102
149
  }
103
150
 
104
151
  function ensurePlanDir(): string {
@@ -124,35 +171,73 @@ function restoreStateFromSession(ctx: ExtensionCommandContext | any): void {
124
171
 
125
172
  function enablePlanMode(
126
173
  pi: ExtensionAPI,
127
- next: Partial<Pick<PlanModeState, "task" | "latestPlanPath" | "approvalStatus" | "previousMode">> = {},
174
+ currentModel: ModelRef | undefined,
175
+ next: Partial<Pick<PlanModeState, "task" | "latestPlanPath" | "approvalStatus" | "previousMode" | "preplanModel" | "targetPermissionMode">> = {},
128
176
  ): void {
129
177
  const currentMode = getPermissionMode();
130
- const previousMode: RestorablePermissionMode = currentMode === "plan"
131
- ? (state.previousMode ?? "accept-on-edit")
132
- : currentMode;
178
+ const enteringPlanMode = currentMode !== "plan";
179
+ const previousMode: RestorablePermissionMode = enteringPlanMode
180
+ ? currentMode
181
+ : (state.previousMode ?? "accept-on-edit");
133
182
 
134
- setPermissionMode("plan");
135
- process.env.LUCENT_CODE_PERMISSION_MODE = "plan";
183
+ setPermissionModeAndEnv("plan");
136
184
  setState(pi, {
137
185
  active: true,
138
186
  task: next.task ?? state.task,
139
187
  latestPlanPath: next.latestPlanPath ?? state.latestPlanPath,
140
188
  approvalStatus: next.approvalStatus ?? state.approvalStatus ?? "pending",
141
189
  previousMode: next.previousMode ?? previousMode,
190
+ preplanModel: next.preplanModel ?? (enteringPlanMode ? (currentModel ?? state.preplanModel) : state.preplanModel),
191
+ targetPermissionMode: next.targetPermissionMode ?? state.targetPermissionMode,
142
192
  });
143
193
  }
144
194
 
145
- function disablePlanMode(pi: ExtensionAPI, approvalStatus: PlanApprovalStatus, clearTask = false): RestorablePermissionMode {
146
- const restoreMode = state.previousMode ?? "accept-on-edit";
147
- setPermissionMode(restoreMode);
148
- process.env.LUCENT_CODE_PERMISSION_MODE = restoreMode;
195
+ function leavePlanMode(
196
+ pi: ExtensionAPI,
197
+ approvalStatus: PlanApprovalStatus,
198
+ nextPermissionMode: RestorablePermissionMode,
199
+ clearTask = false,
200
+ ): RestorablePermissionMode {
201
+ setPermissionModeAndEnv(nextPermissionMode);
149
202
  setState(pi, {
150
203
  active: false,
151
204
  task: clearTask ? "" : state.task,
152
205
  latestPlanPath: state.latestPlanPath,
153
206
  approvalStatus,
154
- previousMode: restoreMode,
207
+ previousMode: state.previousMode,
208
+ preplanModel: state.preplanModel,
209
+ targetPermissionMode: state.targetPermissionMode ?? nextPermissionMode,
155
210
  });
211
+ return nextPermissionMode;
212
+ }
213
+
214
+ async function setModelIfNeeded(pi: ExtensionAPI, ctx: any, modelRef: ModelRef | undefined): Promise<void> {
215
+ if (!modelRef) return;
216
+ const currentModel = parseQualifiedModelRef(ctx?.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
217
+ if (sameModel(currentModel, modelRef)) return;
218
+ const model = resolveModelFromContext(ctx, modelRef);
219
+ if (!model) return;
220
+ await pi.setModel(model, { persist: false });
221
+ }
222
+
223
+ async function approvePlan(pi: ExtensionAPI, ctx: any, permissionMode: RestorablePermissionMode): Promise<void> {
224
+ const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
225
+ if (reasoningModel) {
226
+ await setModelIfNeeded(pi, ctx, reasoningModel);
227
+ }
228
+
229
+ state = {
230
+ ...state,
231
+ targetPermissionMode: permissionMode,
232
+ };
233
+ leavePlanMode(pi, "approved", permissionMode);
234
+ }
235
+
236
+ async function cancelPlan(pi: ExtensionAPI, ctx: any, clearTask = true): Promise<RestorablePermissionMode> {
237
+ const restoreMode = state.previousMode ?? "accept-on-edit";
238
+ await setModelIfNeeded(pi, ctx, state.preplanModel);
239
+ leavePlanMode(pi, "cancelled", restoreMode, clearTask);
240
+ resetState(pi, { approvalStatus: "cancelled" });
156
241
  return restoreMode;
157
242
  }
158
243
 
@@ -168,6 +253,49 @@ function buildPlanModeSystemPrompt(): string {
168
253
  return details.join(" ");
169
254
  }
170
255
 
256
+ function buildApprovalSteeringMessage(planPath: string): string {
257
+ return [
258
+ `Plan artifact saved at ${planPath}.`,
259
+ "Present approval options now using ask_user_questions with exactly one single-select question.",
260
+ `Use question id \"${PLAN_APPROVAL_QUESTION_ID}\" and ask the user what to do next.`,
261
+ "Important: ask_user_questions single-select supports only 2-3 explicit options.",
262
+ "Use exactly these 3 options:",
263
+ `1. ${APPROVE_AUTO_LABEL} (Recommended)`,
264
+ `2. ${APPROVE_BYPASS_LABEL}`,
265
+ `3. ${REVISE_LABEL}`,
266
+ `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 free-text note.`,
267
+ "If the dialog is dismissed or the user gives no answer, continue planning.",
268
+ ].join(" ");
269
+ }
270
+
271
+ function approvalSelectionToPermissionMode(selected: string): RestorablePermissionMode | undefined {
272
+ if (selected.includes(APPROVE_AUTO_LABEL)) return "auto";
273
+ if (selected.includes(APPROVE_BYPASS_LABEL)) return "danger-full-access";
274
+ return undefined;
275
+ }
276
+
277
+ function selectionRequestsCancel(selected: string | string[]): boolean {
278
+ const values = Array.isArray(selected) ? selected : [selected];
279
+ return values.some((value) => {
280
+ if (typeof value !== "string") return false;
281
+ if (value.includes(CANCEL_LABEL)) return true;
282
+ const normalized = value.replace(/^user_note:\s*/i, "").trim().toLowerCase();
283
+ return normalized === "cancel" || normalized.includes("cancel plan");
284
+ });
285
+ }
286
+
287
+ export const __testing = {
288
+ getState(): PlanModeState {
289
+ return { ...state };
290
+ },
291
+ resetState(): void {
292
+ state = { ...INITIAL_STATE };
293
+ startedFromFlag = false;
294
+ },
295
+ parseQualifiedModelRef,
296
+ approvalSelectionToPermissionMode,
297
+ };
298
+
171
299
  export default function planCommand(pi: ExtensionAPI) {
172
300
  pi.registerFlag("plan", {
173
301
  description: "Start the session in plan mode and require a persisted .lsd/plan markdown plan before execution",
@@ -178,8 +306,7 @@ export default function planCommand(pi: ExtensionAPI) {
178
306
  restoreStateFromSession(ctx);
179
307
  startedFromFlag = false;
180
308
  if (state.active) {
181
- setPermissionMode("plan");
182
- process.env.LUCENT_CODE_PERMISSION_MODE = "plan";
309
+ setPermissionModeAndEnv("plan");
183
310
  }
184
311
  });
185
312
 
@@ -190,7 +317,7 @@ export default function planCommand(pi: ExtensionAPI) {
190
317
  };
191
318
  });
192
319
 
193
- pi.on("input", async (event) => {
320
+ pi.on("input", async (event, ctx) => {
194
321
  const planFlag = pi.getFlag("plan");
195
322
  if (startedFromFlag || planFlag !== true || event.source !== "interactive") {
196
323
  return { action: "continue" as const };
@@ -198,9 +325,11 @@ export default function planCommand(pi: ExtensionAPI) {
198
325
 
199
326
  startedFromFlag = true;
200
327
  ensurePlanDir();
201
- enablePlanMode(pi, {
328
+ enablePlanMode(pi, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
202
329
  task: event.text.trim(),
203
330
  approvalStatus: "pending",
331
+ latestPlanPath: undefined,
332
+ targetPermissionMode: undefined,
204
333
  });
205
334
 
206
335
  return { action: "continue" as const };
@@ -235,16 +364,24 @@ export default function planCommand(pi: ExtensionAPI) {
235
364
  }
236
365
  });
237
366
 
238
- pi.on("tool_result", async (event) => {
367
+ pi.on("tool_result", async (event, ctx) => {
239
368
  if (event.toolName === "write" || event.toolName === "edit") {
240
369
  const input = event.input as { path?: string } | undefined;
241
370
  const path = input?.path;
242
- if (path && PLAN_DIR_RE.test(path)) {
371
+ if (path && PLAN_DIR_RE.test(path) && isPlanModeActive()) {
243
372
  setState(pi, {
244
373
  ...state,
245
374
  latestPlanPath: path,
246
- approvalStatus: state.approvalStatus === "revising" ? "pending" : state.approvalStatus,
375
+ approvalStatus: "pending",
376
+ targetPermissionMode: undefined,
247
377
  });
378
+
379
+ if (!ctx.hasUI) {
380
+ await approvePlan(pi, ctx, DEFAULT_APPROVAL_PERMISSION_MODE);
381
+ return;
382
+ }
383
+
384
+ pi.sendUserMessage(buildApprovalSteeringMessage(path), { deliverAs: "steer" });
248
385
  }
249
386
  return;
250
387
  }
@@ -260,22 +397,28 @@ export default function planCommand(pi: ExtensionAPI) {
260
397
  const answer = details.response.answers[PLAN_APPROVAL_QUESTION_ID];
261
398
  if (!answer) return;
262
399
 
263
- const selected = Array.isArray(answer.selected) ? answer.selected[0] : answer.selected;
264
- if (typeof selected !== "string") return;
400
+ const selected = Array.isArray(answer.selected) ? answer.selected : [answer.selected];
401
+ if (!selected.every((value) => typeof value === "string")) return;
265
402
 
266
- if (selected.includes("Approve plan")) {
267
- disablePlanMode(pi, "approved");
403
+ const targetPermissionMode = approvalSelectionToPermissionMode(selected[0]);
404
+ if (targetPermissionMode) {
405
+ state = {
406
+ ...state,
407
+ targetPermissionMode,
408
+ };
409
+ await approvePlan(pi, ctx, targetPermissionMode);
268
410
  return;
269
411
  }
270
412
 
271
- if (selected.includes("Revise plan")) {
272
- enablePlanMode(pi, { approvalStatus: "revising" });
413
+ if (selected.some((value) => value.includes(REVISE_LABEL))) {
414
+ enablePlanMode(pi, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
415
+ approvalStatus: "revising",
416
+ });
273
417
  return;
274
418
  }
275
419
 
276
- if (selected.includes("Cancel")) {
277
- disablePlanMode(pi, "cancelled", true);
278
- clearState(pi);
420
+ if (selectionRequestsCancel(selected)) {
421
+ await cancelPlan(pi, ctx, true);
279
422
  }
280
423
  });
281
424
 
@@ -283,16 +426,18 @@ export default function planCommand(pi: ExtensionAPI) {
283
426
  description: "Toggle plan mode. While active, only investigative tools and writes under .lsd/plan/ are allowed",
284
427
  async handler(args: string, ctx: ExtensionCommandContext) {
285
428
  if (isPlanModeActive()) {
286
- disablePlanMode(pi, "cancelled");
429
+ await cancelPlan(pi, ctx, true);
287
430
  ctx.ui.notify("Plan mode disabled.", "info");
288
431
  return;
289
432
  }
290
433
 
291
434
  ensurePlanDir();
292
435
  const task = args.trim();
293
- enablePlanMode(pi, {
436
+ enablePlanMode(pi, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, {
294
437
  task,
438
+ latestPlanPath: undefined,
295
439
  approvalStatus: "pending",
440
+ targetPermissionMode: undefined,
296
441
  });
297
442
  ctx.ui.notify(
298
443
  task
@@ -310,8 +455,8 @@ export default function planCommand(pi: ExtensionAPI) {
310
455
  ctx.ui.notify("Plan mode is not active.", "info");
311
456
  return;
312
457
  }
313
- const restoreMode = disablePlanMode(pi, "approved");
314
- ctx.ui.notify(`Plan mode disabled. Permission mode restored to ${restoreMode}.`, "info");
458
+ await approvePlan(pi, ctx, DEFAULT_APPROVAL_PERMISSION_MODE);
459
+ ctx.ui.notify(`Plan approved. Permission mode switched to ${DEFAULT_APPROVAL_PERMISSION_MODE}.`, "info");
315
460
  },
316
461
  });
317
462
 
@@ -319,9 +464,10 @@ export default function planCommand(pi: ExtensionAPI) {
319
464
  description: "Cancel the current plan-mode session without executing",
320
465
  async handler(_args: string, ctx: ExtensionCommandContext) {
321
466
  if (isPlanModeActive()) {
322
- disablePlanMode(pi, "cancelled", true);
467
+ await cancelPlan(pi, ctx, true);
468
+ } else {
469
+ resetState(pi, { approvalStatus: "cancelled" });
323
470
  }
324
- clearState(pi);
325
471
  ctx.ui.notify("Plan mode cancelled.", "info");
326
472
  },
327
473
  });
@@ -25,6 +25,16 @@ export interface AgentDiscoveryResult {
25
25
  projectAgentsDir: string | null;
26
26
  }
27
27
 
28
+ function normalizeAgentModel(model: string | undefined): string | undefined {
29
+ const trimmed = model?.trim();
30
+ if (!trimmed) return undefined;
31
+ if (trimmed === "$budget_model") return trimmed;
32
+ if (trimmed.includes(" ")) return undefined;
33
+ if (!trimmed.includes("/")) return trimmed;
34
+ const parts = trimmed.split("/");
35
+ return parts.length === 2 && parts.every(Boolean) ? trimmed : undefined;
36
+ }
37
+
28
38
  function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
29
39
  const agents: AgentConfig[] = [];
30
40
 
@@ -66,7 +76,7 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
66
76
  name: frontmatter.name,
67
77
  description: frontmatter.description,
68
78
  tools: tools && tools.length > 0 ? tools : undefined,
69
- model: frontmatter.model,
79
+ model: normalizeAgentModel(frontmatter.model),
70
80
  systemPrompt: body,
71
81
  source,
72
82
  filePath,
@@ -1,5 +1,6 @@
1
1
  import type { AgentConfig } from "./agents.js";
2
2
  import type { SharedPreferences } from "../shared/preferences.js";
3
+ import { normalizeSubagentModel } from "./model-resolution.js";
3
4
 
4
5
  export function resolveConfiguredSubagentModel(
5
6
  agent: AgentConfig,
@@ -9,7 +10,7 @@ export function resolveConfiguredSubagentModel(
9
10
  const configuredModel = agent.model?.trim();
10
11
  if (!configuredModel) return undefined;
11
12
  if (configuredModel === "$budget_model") {
12
- return settingsBudgetModel?.trim() || preferences?.subagent?.budget_model?.trim() || undefined;
13
+ return normalizeSubagentModel(settingsBudgetModel) ?? normalizeSubagentModel(preferences?.subagent?.budget_model);
13
14
  }
14
- return configuredModel;
15
+ return normalizeSubagentModel(configuredModel);
15
16
  }
@@ -12,7 +12,7 @@
12
12
  * Uses JSON mode to capture structured output from subagents.
13
13
  */
14
14
 
15
- import { spawn, execSync, type ChildProcess } from "node:child_process";
15
+ import { spawn, execFileSync, type ChildProcess } from "node:child_process";
16
16
  import * as crypto from "node:crypto";
17
17
  import * as fs from "node:fs";
18
18
  import * as os from "node:os";
@@ -31,6 +31,7 @@ import { Container, Markdown, Spacer, Text } from "@gsd/pi-tui";
31
31
  import { Type } from "@sinclair/typebox";
32
32
  import { formatTokenCount } from "../shared/mod.js";
33
33
  import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
34
+ import { buildSubagentProcessArgs, getBundledExtensionPathsFromEnv } from "./launch-helpers.js";
34
35
  import {
35
36
  type IsolationEnvironment,
36
37
  type IsolationMode,
@@ -42,7 +43,10 @@ import {
42
43
  import { registerWorker, updateWorker } from "./worker-registry.js";
43
44
  import { handleSubagentPermissionRequest, isSubagentPermissionRequest } from "./approval-proxy.js";
44
45
  import { resolveConfiguredSubagentModel } from "./configured-model.js";
45
- import { resolveSubagentModel } from "./model-resolution.js";
46
+ import {
47
+ normalizeSubagentModel,
48
+ resolveSubagentModel,
49
+ } from "./model-resolution.js";
46
50
  import { loadEffectivePreferences } from "../shared/preferences.js";
47
51
  import { CmuxClient, shellEscape } from "../cmux/index.js";
48
52
 
@@ -51,7 +55,7 @@ const MAX_CONCURRENCY = 4;
51
55
  const COLLAPSED_ITEM_COUNT = 10;
52
56
  const liveSubagentProcesses = new Set<ChildProcess>();
53
57
 
54
- async function stopLiveSubagents(): Promise<void> {
58
+ export async function stopLiveSubagents(): Promise<void> {
55
59
  const active = Array.from(liveSubagentProcesses);
56
60
  if (active.length === 0) return;
57
61
 
@@ -274,26 +278,14 @@ function readBudgetSubagentModelFromSettings(): string | undefined {
274
278
  if (!fs.existsSync(settingsPath)) return undefined;
275
279
  const raw = fs.readFileSync(settingsPath, "utf-8");
276
280
  const parsed = JSON.parse(raw) as { budgetSubagentModel?: unknown };
277
- return typeof parsed.budgetSubagentModel === "string" ? parsed.budgetSubagentModel.trim() || undefined : undefined;
281
+ return typeof parsed.budgetSubagentModel === "string"
282
+ ? normalizeSubagentModel(parsed.budgetSubagentModel)
283
+ : undefined;
278
284
  } catch {
279
285
  return undefined;
280
286
  }
281
287
  }
282
288
 
283
- function buildSubagentProcessArgs(
284
- agent: AgentConfig,
285
- task: string,
286
- tmpPromptPath: string | null,
287
- model: string | undefined,
288
- ): string[] {
289
- const args: string[] = ["--mode", "json", "-p", "--no-session"];
290
- if (model) args.push("--model", model);
291
- if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
292
- if (tmpPromptPath) args.push("--append-system-prompt", tmpPromptPath);
293
- args.push(`Task: ${task}`);
294
- return args;
295
- }
296
-
297
289
  function resolveSubagentCliPath(defaultCwd: string): string | null {
298
290
  const candidates = [process.env.GSD_BIN_PATH, process.env.LSD_BIN_PATH, process.argv[1]]
299
291
  .map((value) => value?.trim())
@@ -310,7 +302,7 @@ function resolveSubagentCliPath(defaultCwd: string): string | null {
310
302
 
311
303
  for (const binName of ["lsd", "gsd"]) {
312
304
  try {
313
- const resolved = execSync(`which ${binName}`, { encoding: "utf-8" }).trim();
305
+ const resolved = execFileSync("which", [binName], { encoding: "utf-8" }).trim();
314
306
  if (resolved) return resolved;
315
307
  } catch {
316
308
  /* ignore */
@@ -325,13 +317,13 @@ function processSubagentEventLine(
325
317
  currentResult: SingleResult,
326
318
  emitUpdate: () => void,
327
319
  proc?: ChildProcess,
328
- ): void {
329
- if (!line.trim()) return;
320
+ ): boolean {
321
+ if (!line.trim()) return false;
330
322
  let event: any;
331
323
  try {
332
324
  event = JSON.parse(line);
333
325
  } catch {
334
- return;
326
+ return false;
335
327
  }
336
328
 
337
329
  if (proc && isSubagentPermissionRequest(event)) {
@@ -339,10 +331,10 @@ function processSubagentEventLine(
339
331
  requestFileChangeApproval,
340
332
  requestClassifierDecision,
341
333
  });
342
- return;
334
+ return false;
343
335
  }
344
336
 
345
- if (event.type === "message_end" && event.message) {
337
+ if ((event.type === "message_end" || event.type === "turn_end") && event.message) {
346
338
  const msg = event.message as Message;
347
339
  currentResult.messages.push(msg);
348
340
 
@@ -357,7 +349,7 @@ function processSubagentEventLine(
357
349
  currentResult.usage.cost += usage.cost?.total || 0;
358
350
  currentResult.usage.contextTokens = usage.totalTokens || 0;
359
351
  }
360
- if (msg.model) currentResult.model = msg.model;
352
+ if (msg.model && (!currentResult.model || msg.model.includes("/"))) currentResult.model = msg.model;
361
353
  if (msg.stopReason) currentResult.stopReason = msg.stopReason;
362
354
  if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
363
355
  }
@@ -368,6 +360,8 @@ function processSubagentEventLine(
368
360
  currentResult.messages.push(event.message as Message);
369
361
  emitUpdate();
370
362
  }
363
+
364
+ return event.type === "agent_end";
371
365
  }
372
366
 
373
367
  async function waitForFile(filePath: string, signal: AbortSignal | undefined, timeoutMs = 30 * 60 * 1000): Promise<boolean> {
@@ -453,8 +447,8 @@ async function runSingleAgent(
453
447
  let wasAborted = false;
454
448
 
455
449
  const exitCode = await new Promise<number>((resolve) => {
456
- const bundledPaths = (process.env.GSD_BUNDLED_EXTENSION_PATHS ?? "").split(path.delimiter).map(s => s.trim()).filter(Boolean);
457
- const extensionArgs = bundledPaths.flatMap(p => ["--extension", p]);
450
+ const bundledPaths = getBundledExtensionPathsFromEnv();
451
+ const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
458
452
  const cliPath = resolveSubagentCliPath(cwd ?? defaultCwd);
459
453
  if (!cliPath) {
460
454
  currentResult.stderr += "Unable to resolve LSD/GSD CLI path for subagent launch.";
@@ -466,14 +460,25 @@ async function runSingleAgent(
466
460
  [cliPath, ...extensionArgs, ...args],
467
461
  { cwd: cwd ?? defaultCwd, shell: false, stdio: ["pipe", "pipe", "pipe"] },
468
462
  );
463
+ proc.stdin.end();
469
464
  liveSubagentProcesses.add(proc);
470
465
  let buffer = "";
466
+ let completionSeen = false;
471
467
 
472
468
  proc.stdout.on("data", (data) => {
473
469
  buffer += data.toString();
474
470
  const lines = buffer.split("\n");
475
471
  buffer = lines.pop() || "";
476
- for (const line of lines) processSubagentEventLine(line, currentResult, emitUpdate, proc);
472
+ for (const line of lines) {
473
+ if (processSubagentEventLine(line, currentResult, emitUpdate, proc)) {
474
+ completionSeen = true;
475
+ try {
476
+ proc.kill("SIGTERM");
477
+ } catch {
478
+ /* ignore */
479
+ }
480
+ }
481
+ }
477
482
  });
478
483
 
479
484
  proc.stderr.on("data", (data) => {
@@ -482,8 +487,11 @@ async function runSingleAgent(
482
487
 
483
488
  proc.on("close", (code) => {
484
489
  liveSubagentProcesses.delete(proc);
485
- if (buffer.trim()) processSubagentEventLine(buffer, currentResult, emitUpdate, proc);
486
- resolve(code ?? 0);
490
+ if (buffer.trim()) {
491
+ const completedOnFlush = processSubagentEventLine(buffer, currentResult, emitUpdate, proc);
492
+ completionSeen = completionSeen || completedOnFlush;
493
+ }
494
+ resolve(completionSeen && (code === null || code === 143 || code === 15) ? 0 : (code ?? 0));
487
495
  });
488
496
 
489
497
  proc.on("error", () => {
@@ -0,0 +1,30 @@
1
+ import * as path from "node:path";
2
+
3
+ import type { AgentConfig } from "./agents.js";
4
+
5
+ export function getBundledExtensionPathsFromEnv(env: NodeJS.ProcessEnv = process.env): string[] {
6
+ const rawPaths = [env.GSD_BUNDLED_EXTENSION_PATHS, env.LSD_BUNDLED_EXTENSION_PATHS]
7
+ .map((value) => value?.trim())
8
+ .filter((value): value is string => Boolean(value));
9
+ const unique = new Set<string>();
10
+ for (const raw of rawPaths) {
11
+ for (const entry of raw.split(path.delimiter).map((value) => value.trim()).filter(Boolean)) {
12
+ unique.add(entry);
13
+ }
14
+ }
15
+ return Array.from(unique);
16
+ }
17
+
18
+ export function buildSubagentProcessArgs(
19
+ agent: AgentConfig,
20
+ task: string,
21
+ tmpPromptPath: string | null,
22
+ model: string | undefined,
23
+ ): string[] {
24
+ const args: string[] = ["--mode", "json", "-p", "--no-session"];
25
+ if (model) args.push("--model", model);
26
+ if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
27
+ if (tmpPromptPath) args.push("--append-system-prompt", tmpPromptPath);
28
+ args.push(`Task: ${task}`);
29
+ return args;
30
+ }