From 9a69aeed48a5370050f7d6ba48027054c03eda99 Mon Sep 17 00:00:00 2001 From: cruldra Date: Wed, 24 Jun 2026 02:56:33 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20fix(vscode):=20worktree=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E9=85=8D=E7=BD=AE=E6=94=B9=E7=94=A8=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E8=87=AA=E5=B8=A6=E8=AE=BE=E7=BD=AE=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=EF=BC=8C=E6=92=A4=E9=94=80=20VS=20Code=20=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版误把 worktree 目录模板注册成 VS Code 原生设置(contributes.configuration + workspace.getConfiguration),但本插件有自己的设置面板(SettingsModal,存 globalState)。 改为: - Settings 新增 worktreeDirectory 字段(默认 ~/Sources/worktree/$project_name/$feature_name, 留空回退默认),贯穿 store/handler/两端 messages/useIssues/App/SettingsModal - SettingsModal「钩子」分组新增「工作树目录」输入项,走既有 save/load 流程 - handleImplement 改从 getSettings(panel.context).worktreeDirectory 读模板 - 删除 package.json 的 contributes.configuration(meta.ts 为 gitignore 生成物,已本地还原) deriveSlug / resolveWorktreeDir / ~ 展开 / 绝对路径 / feature/ 逻辑不变。 Co-Authored-By: Claude Opus 4.7 (1M context) --- vscode/package.json | 12 +----- vscode/src/panel/KanbanPanel.ts | 2 + vscode/src/panel/handlers/sessions.ts | 5 ++- vscode/src/panel/handlers/settings.ts | 7 ++++ vscode/src/panel/messages.ts | 2 + vscode/src/settings/store.ts | 15 ++++++++ vscode/webview-ui/src/App.tsx | 1 + .../src/components/SettingsModal.tsx | 37 +++++++++++++++++++ vscode/webview-ui/src/hooks/useIssues.ts | 4 ++ vscode/webview-ui/src/lib/messages.ts | 2 + 10 files changed, 74 insertions(+), 13 deletions(-) diff --git a/vscode/package.json b/vscode/package.json index 869e930..e140080 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -2,7 +2,7 @@ "publisher": "clurdra", "name": "superpowers-vscode-clurdra", "displayName": "Superpowers-clurdra", - "version": "0.2.43", + "version": "0.2.44", "packageManager": "pnpm@10.27.0", "description": "Superpowers specs and plans Kanban explorer", "author": "clurdra", @@ -93,16 +93,6 @@ "group": "navigation" } ] - }, - "configuration": { - "title": "Superpowers", - "properties": { - "superpowers.worktreeDirectory": { - "type": "string", - "default": "~/Sources/worktree/$project_name/$feature_name", - "markdownDescription": "运行 Plan 时创建 worktree 的目录模板。支持占位符 `$project_root`(工作区根目录的绝对路径)、`$project_name`(工作区目录名)、`$feature_name`(spec/plan 的 slug)。开头的 `~` 会展开为当前用户主目录。默认会把 worktree 建到 `~/Sources/worktree/<项目名>/`。" - } - } } }, "scripts": { diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index ff7e24d..4e6cd7d 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -840,6 +840,7 @@ export class KanbanWebviewPanel { reviewPrompt: s.reviewPrompt, devBranch: s.devBranch, autoBuildBranch: s.autoBuildBranch, + worktreeDirectory: s.worktreeDirectory, worktreePostCreateScript: s.worktreePostCreateScript, worktreePreRemoveScript: s.worktreePreRemoveScript, implTabPreCreateScript: s.implTabPreCreateScript, @@ -899,6 +900,7 @@ export class KanbanWebviewPanel { reviewPrompt: s.reviewPrompt, devBranch: s.devBranch, autoBuildBranch: s.autoBuildBranch, + worktreeDirectory: s.worktreeDirectory, worktreePostCreateScript: s.worktreePostCreateScript, worktreePreRemoveScript: s.worktreePreRemoveScript, implTabPreCreateScript: s.implTabPreCreateScript, diff --git a/vscode/src/panel/handlers/sessions.ts b/vscode/src/panel/handlers/sessions.ts index 2caca12..aeb70ed 100644 --- a/vscode/src/panel/handlers/sessions.ts +++ b/vscode/src/panel/handlers/sessions.ts @@ -684,8 +684,9 @@ export async function handleImplement( // worktree 目录名用 spec/plan 的 slug(可读、与 spec 目录同名),不再用 planFile 的 hash。 const slug = deriveSlug(planFile) const branch = `feature/${slug}` - // 目录走 superpowers.worktreeDirectory 模板(设置里可改),默认 ~/Sources/worktree/<项目>/。 - const worktreeTemplate = workspace.getConfiguration('superpowers').get('worktreeDirectory') + // 目录模板取自插件自己的设置面板(globalState),不是 VS Code 原生配置; + // 留空时回退到默认 ~/Sources/worktree/<项目>/。 + const worktreeTemplate = getSettings(panel.context).worktreeDirectory || '~/Sources/worktree/$project_name/$feature_name' const worktreePath = resolveWorktreeDir(worktreeTemplate, workspaceRoot, slug) diff --git a/vscode/src/panel/handlers/settings.ts b/vscode/src/panel/handlers/settings.ts index 99f439e..608db26 100644 --- a/vscode/src/panel/handlers/settings.ts +++ b/vscode/src/panel/handlers/settings.ts @@ -25,6 +25,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { reviewPrompt: string devBranch: string autoBuildBranch: string + worktreeDirectory: string worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string @@ -43,6 +44,9 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { const trimmedYtToken = payload.youtrackToken.trim() const trimmedDevBranch = payload.devBranch.trim() const trimmedAutoBuildBranch = payload.autoBuildBranch.trim() + // worktree dir template: '' = "use default", so don't preserve blanks + // specially — getSettings falls back to the default at read time. + const trimmedWorktreeDir = payload.worktreeDirectory.trim() // Hook script paths: keep '' meaningful (= "use default // .spx/*.sh"). Just strip whitespace. const trimmedPostCreate = payload.worktreePostCreateScript.trim() @@ -71,6 +75,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { reviewPrompt: payload.reviewPrompt || prev.reviewPrompt, devBranch: trimmedDevBranch || prev.devBranch, autoBuildBranch: trimmedAutoBuildBranch, + worktreeDirectory: trimmedWorktreeDir || prev.worktreeDirectory, worktreePostCreateScript: trimmedPostCreate, worktreePreRemoveScript: trimmedPreRemove, implTabPreCreateScript: trimmedImplPre, @@ -89,6 +94,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { // fallback at read time. devBranch: trimmedDevBranch, autoBuildBranch: trimmedAutoBuildBranch, + worktreeDirectory: trimmedWorktreeDir, worktreePostCreateScript: trimmedPostCreate, worktreePreRemoveScript: trimmedPreRemove, implTabPreCreateScript: trimmedImplPre, @@ -168,6 +174,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom reviewPrompt: s.reviewPrompt, devBranch: s.devBranch, autoBuildBranch: s.autoBuildBranch, + worktreeDirectory: s.worktreeDirectory, worktreePostCreateScript: s.worktreePostCreateScript, worktreePreRemoveScript: s.worktreePreRemoveScript, implTabPreCreateScript: s.implTabPreCreateScript, diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index 1d22328..da451a2 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -61,6 +61,7 @@ export type ExtensionToWebview reviewPrompt: string devBranch: string autoBuildBranch: string + worktreeDirectory: string worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string @@ -115,6 +116,7 @@ export type WebviewToExtension reviewPrompt: string devBranch: string autoBuildBranch: string + worktreeDirectory: string worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string diff --git a/vscode/src/settings/store.ts b/vscode/src/settings/store.ts index d4ad2b9..585b4ec 100644 --- a/vscode/src/settings/store.ts +++ b/vscode/src/settings/store.ts @@ -86,6 +86,14 @@ export interface Settings { * disabled because there's nothing to fast-forward. */ autoBuildBranch: string + /** + * Directory template for the worktree created when implementing a plan. + * Placeholders: `$project_root` (workspace abs path), `$project_name` (its + * basename), `$feature_name` (the spec/plan slug). A leading `~` expands to + * the user's home dir. Empty string means "use the default + * `~/Sources/worktree/$project_name/$feature_name`". + */ + worktreeDirectory: string /** * Path to a user-provided shell script that runs *after* the extension * creates a worktree via `git worktree add`. Empty string means use the @@ -157,6 +165,7 @@ function defaults(ctx: ExtensionContext): Settings { reviewPrompt: readDefaultPrompt(ctx.extensionPath, 'review'), devBranch: 'main', autoBuildBranch: '', + worktreeDirectory: '~/Sources/worktree/$project_name/$feature_name', worktreePostCreateScript: '', worktreePreRemoveScript: '', implTabPreCreateScript: '', @@ -200,6 +209,11 @@ export function getSettings(ctx: ExtensionContext): Settings { const autoBuildBranch = typeof stored.autoBuildBranch === 'string' ? stored.autoBuildBranch : base.autoBuildBranch + // worktree dir template: empty/blank means "use the default template", so + // fall back to base (unlike the hook paths below, where '' is meaningful). + const worktreeDirectory = typeof stored.worktreeDirectory === 'string' && stored.worktreeDirectory.length > 0 + ? stored.worktreeDirectory + : base.worktreeDirectory // worktree hook script paths: '' is meaningful (= "use default // .spx/worktree-*.sh"), so don't coerce. Non-string legacy values fall // back to ''. The hook runner resolves '' to the default at execution @@ -238,6 +252,7 @@ export function getSettings(ctx: ExtensionContext): Settings { reviewPrompt, devBranch, autoBuildBranch, + worktreeDirectory, worktreePostCreateScript, worktreePreRemoveScript, implTabPreCreateScript, diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index 432790d..cece1fb 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -445,6 +445,7 @@ export function App() { initialReviewPrompt={settings?.reviewPrompt ?? ''} initialDevBranch={settings?.devBranch ?? 'main'} initialAutoBuildBranch={settings?.autoBuildBranch ?? ''} + initialWorktreeDirectory={settings?.worktreeDirectory ?? ''} initialWorktreePostCreateScript={settings?.worktreePostCreateScript ?? ''} initialWorktreePreRemoveScript={settings?.worktreePreRemoveScript ?? ''} initialImplTabPreCreateScript={settings?.implTabPreCreateScript ?? ''} diff --git a/vscode/webview-ui/src/components/SettingsModal.tsx b/vscode/webview-ui/src/components/SettingsModal.tsx index 95c88b3..1fd8458 100644 --- a/vscode/webview-ui/src/components/SettingsModal.tsx +++ b/vscode/webview-ui/src/components/SettingsModal.tsx @@ -23,6 +23,7 @@ interface SubmitValues { reviewPrompt: string devBranch: string autoBuildBranch: string + worktreeDirectory: string worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string @@ -59,6 +60,7 @@ export interface SettingsModalProps { initialReviewPrompt: string initialDevBranch: string initialAutoBuildBranch: string + initialWorktreeDirectory: string initialWorktreePostCreateScript: string initialWorktreePreRemoveScript: string initialImplTabPreCreateScript: string @@ -128,6 +130,7 @@ export function SettingsModal({ initialReviewPrompt, initialDevBranch, initialAutoBuildBranch, + initialWorktreeDirectory, initialWorktreePostCreateScript, initialWorktreePreRemoveScript, initialImplTabPreCreateScript, @@ -151,6 +154,7 @@ export function SettingsModal({ const [reviewPrompt, setReviewPrompt] = useState(initialReviewPrompt) const [devBranch, setDevBranch] = useState(initialDevBranch) const [autoBuildBranch, setAutoBuildBranch] = useState(initialAutoBuildBranch) + const [worktreeDirectory, setWorktreeDirectory] = useState(initialWorktreeDirectory) const [worktreePostCreateScript, setWorktreePostCreateScript] = useState(initialWorktreePostCreateScript) const [worktreePreRemoveScript, setWorktreePreRemoveScript] = useState(initialWorktreePreRemoveScript) const [implTabPreCreateScript, setImplTabPreCreateScript] = useState(initialImplTabPreCreateScript) @@ -252,6 +256,8 @@ export function SettingsModal({ reviewPrompt, devBranch: devBranch.trim(), autoBuildBranch: autoBuildBranch.trim(), + // worktree 目录模板:trim 后留空 = 用默认模板(getSettings 读时回退) + worktreeDirectory: worktreeDirectory.trim(), // 钩子路径保持原样 trim(前后空格无意义;空串 = 用默认 .spx/*.sh) worktreePostCreateScript: worktreePostCreateScript.trim(), worktreePreRemoveScript: worktreePreRemoveScript.trim(), @@ -605,6 +611,37 @@ export function SettingsModal({ {activeGroup === 'hooks' && ( <> + + 实施 Plan 时创建 worktree 的目录模板。占位符 + {' '} + $project_root + (工作区绝对路径)、 + $project_name + (工作区目录名)、 + $feature_name + (spec/plan 的 slug);开头 + {' '} + ~ + {' '} + 展开为主目录。留空 = 默认 + {' '} + ~/Sources/worktree/$project_name/$feature_name + 。 + + )} + > + setWorktreeDirectory(e.target.value)} + placeholder="~/Sources/worktree/$project_name/$feature_name" + className={inputClass} + /> + +