diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index 4e6cd7d..8e7de09 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -841,6 +841,7 @@ export class KanbanWebviewPanel { devBranch: s.devBranch, autoBuildBranch: s.autoBuildBranch, worktreeDirectory: s.worktreeDirectory, + worktreeOpenWith: s.worktreeOpenWith, worktreePostCreateScript: s.worktreePostCreateScript, worktreePreRemoveScript: s.worktreePreRemoveScript, implTabPreCreateScript: s.implTabPreCreateScript, @@ -901,6 +902,7 @@ export class KanbanWebviewPanel { devBranch: s.devBranch, autoBuildBranch: s.autoBuildBranch, worktreeDirectory: s.worktreeDirectory, + worktreeOpenWith: s.worktreeOpenWith, worktreePostCreateScript: s.worktreePostCreateScript, worktreePreRemoveScript: s.worktreePreRemoveScript, implTabPreCreateScript: s.implTabPreCreateScript, diff --git a/vscode/src/panel/handlers/settings.ts b/vscode/src/panel/handlers/settings.ts index 608db26..a6ff20e 100644 --- a/vscode/src/panel/handlers/settings.ts +++ b/vscode/src/panel/handlers/settings.ts @@ -26,6 +26,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { devBranch: string autoBuildBranch: string worktreeDirectory: string + worktreeOpenWith: 'vscode' | 'pycharm' worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string @@ -76,6 +77,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { devBranch: trimmedDevBranch || prev.devBranch, autoBuildBranch: trimmedAutoBuildBranch, worktreeDirectory: trimmedWorktreeDir || prev.worktreeDirectory, + worktreeOpenWith: payload.worktreeOpenWith || prev.worktreeOpenWith, worktreePostCreateScript: trimmedPostCreate, worktreePreRemoveScript: trimmedPreRemove, implTabPreCreateScript: trimmedImplPre, @@ -95,6 +97,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { devBranch: trimmedDevBranch, autoBuildBranch: trimmedAutoBuildBranch, worktreeDirectory: trimmedWorktreeDir, + worktreeOpenWith: payload.worktreeOpenWith, worktreePostCreateScript: trimmedPostCreate, worktreePreRemoveScript: trimmedPreRemove, implTabPreCreateScript: trimmedImplPre, @@ -175,6 +178,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom devBranch: s.devBranch, autoBuildBranch: s.autoBuildBranch, worktreeDirectory: s.worktreeDirectory, + worktreeOpenWith: s.worktreeOpenWith, worktreePostCreateScript: s.worktreePostCreateScript, worktreePreRemoveScript: s.worktreePreRemoveScript, implTabPreCreateScript: s.implTabPreCreateScript, diff --git a/vscode/src/panel/handlers/worktree.ts b/vscode/src/panel/handlers/worktree.ts index 254c91f..a559009 100644 --- a/vscode/src/panel/handlers/worktree.ts +++ b/vscode/src/panel/handlers/worktree.ts @@ -1,10 +1,10 @@ import type { HookContext } from '../../git/worktreeHooks' import type { KanbanWebviewPanel } from '../KanbanPanel' import { execFile } from 'node:child_process' -import * as path from 'node:path' import { commands, Uri, window, workspace } from 'vscode' import { getToken } from '../../auth/secrets' import { detectRepo } from '../../git/remote' +import { resolveWorktreePath } from '../../git/worktree' import { runImplTabPostCloseHook, runImplTabPreCreateHook, @@ -26,7 +26,26 @@ export async function handleOpenWorktree(panel: KanbanWebviewPanel, relPath: str void window.showErrorMessage('请先打开一个工作区文件夹') return } - const abs = path.isAbsolute(relPath) ? relPath : path.join(workspaceRoot, relPath) + // 存量工单可能存了带 `~` 的 worktreePath,必须经 resolveWorktreePath 展开, + // 否则 path.join(workspaceRoot, '~/...') 会拼出不存在的路径。 + const abs = resolveWorktreePath(relPath, workspaceRoot) + + // PyCharm 分支用 JetBrains 启动器打开目录。错误在回调里自行处理,不要落进 + // 下面 openFolder 的 catch。 + const { worktreeOpenWith } = getSettings(panel.context) + if (worktreeOpenWith === 'pycharm') { + execFile('pycharm', [abs], (err) => { + if (err) { + const code = (err as NodeJS.ErrnoException).code ?? 'unknown' + const message = code === 'ENOENT' + ? '找不到 pycharm 命令,请在 JetBrains Toolbox 里启用命令行启动器或把 pycharm 加入 PATH' + : err.message + void window.showErrorMessage(`用 PyCharm 打开失败: ${message}`) + } + }) + return + } + try { await commands.executeCommand('vscode.openFolder', Uri.file(abs), true) } @@ -56,7 +75,7 @@ export async function handleDeleteWorktree(panel: KanbanWebviewPanel, issueNumbe if (choice !== '删除') return - const abs = path.isAbsolute(relPath) ? relPath : path.join(workspaceRoot, relPath) + const abs = resolveWorktreePath(relPath, workspaceRoot) // Run the pre-remove lifecycle hook so the user can tear down resources // (close IDE windows, etc.) before the worktree dir vanishes. Best-effort @@ -265,7 +284,7 @@ export async function dispatchImplTabPostCloseAsync(panel: KanbanWebviewPanel, i const wt = typeof stateObj.worktreePath === 'string' ? stateObj.worktreePath : '' if (!wt) return - const absWorktree = path.isAbsolute(wt) ? wt : path.join(workspaceRoot, wt) + const absWorktree = resolveWorktreePath(wt, workspaceRoot) const settings = getSettings(panel.context) await dispatchWorktreeHook(panel, 'impl-tab-post-close', { workspaceRoot, diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index da451a2..46bbb81 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -62,6 +62,7 @@ export type ExtensionToWebview devBranch: string autoBuildBranch: string worktreeDirectory: string + worktreeOpenWith: 'vscode' | 'pycharm' worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string @@ -117,6 +118,7 @@ export type WebviewToExtension devBranch: string autoBuildBranch: string worktreeDirectory: string + worktreeOpenWith: 'vscode' | 'pycharm' worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string diff --git a/vscode/src/settings/store.ts b/vscode/src/settings/store.ts index 585b4ec..acff90b 100644 --- a/vscode/src/settings/store.ts +++ b/vscode/src/settings/store.ts @@ -94,6 +94,12 @@ export interface Settings { * `~/Sources/worktree/$project_name/$feature_name`". */ worktreeDirectory: string + /** + * Which editor opens a worktree when the user clicks it in the detail + * panel. `'pycharm'` shells out to the JetBrains `pycharm` launcher; + * `'vscode'` (default) opens a new VS Code window. + */ + worktreeOpenWith: 'vscode' | 'pycharm' /** * Path to a user-provided shell script that runs *after* the extension * creates a worktree via `git worktree add`. Empty string means use the @@ -166,6 +172,7 @@ function defaults(ctx: ExtensionContext): Settings { devBranch: 'main', autoBuildBranch: '', worktreeDirectory: '~/Sources/worktree/$project_name/$feature_name', + worktreeOpenWith: 'vscode', worktreePostCreateScript: '', worktreePreRemoveScript: '', implTabPreCreateScript: '', @@ -214,6 +221,11 @@ export function getSettings(ctx: ExtensionContext): Settings { const worktreeDirectory = typeof stored.worktreeDirectory === 'string' && stored.worktreeDirectory.length > 0 ? stored.worktreeDirectory : base.worktreeDirectory + // worktreeOpenWith is an enum — only the two known values are valid; anything + // else (legacy installs, garbage) collapses to the default. + const worktreeOpenWith = stored.worktreeOpenWith === 'pycharm' || stored.worktreeOpenWith === 'vscode' + ? stored.worktreeOpenWith + : base.worktreeOpenWith // 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 @@ -253,6 +265,7 @@ export function getSettings(ctx: ExtensionContext): Settings { devBranch, autoBuildBranch, worktreeDirectory, + worktreeOpenWith, worktreePostCreateScript, worktreePreRemoveScript, implTabPreCreateScript, diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index cece1fb..8c5ac3a 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -446,6 +446,7 @@ export function App() { initialDevBranch={settings?.devBranch ?? 'main'} initialAutoBuildBranch={settings?.autoBuildBranch ?? ''} initialWorktreeDirectory={settings?.worktreeDirectory ?? ''} + initialWorktreeOpenWith={settings?.worktreeOpenWith ?? 'vscode'} 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 1fd8458..4455575 100644 --- a/vscode/webview-ui/src/components/SettingsModal.tsx +++ b/vscode/webview-ui/src/components/SettingsModal.tsx @@ -24,6 +24,7 @@ interface SubmitValues { devBranch: string autoBuildBranch: string worktreeDirectory: string + worktreeOpenWith: 'vscode' | 'pycharm' worktreePostCreateScript: string worktreePreRemoveScript: string implTabPreCreateScript: string @@ -61,6 +62,7 @@ export interface SettingsModalProps { initialDevBranch: string initialAutoBuildBranch: string initialWorktreeDirectory: string + initialWorktreeOpenWith: 'vscode' | 'pycharm' initialWorktreePostCreateScript: string initialWorktreePreRemoveScript: string initialImplTabPreCreateScript: string @@ -131,6 +133,7 @@ export function SettingsModal({ initialDevBranch, initialAutoBuildBranch, initialWorktreeDirectory, + initialWorktreeOpenWith, initialWorktreePostCreateScript, initialWorktreePreRemoveScript, initialImplTabPreCreateScript, @@ -155,6 +158,7 @@ export function SettingsModal({ const [devBranch, setDevBranch] = useState(initialDevBranch) const [autoBuildBranch, setAutoBuildBranch] = useState(initialAutoBuildBranch) const [worktreeDirectory, setWorktreeDirectory] = useState(initialWorktreeDirectory) + const [worktreeOpenWith, setWorktreeOpenWith] = useState(initialWorktreeOpenWith) const [worktreePostCreateScript, setWorktreePostCreateScript] = useState(initialWorktreePostCreateScript) const [worktreePreRemoveScript, setWorktreePreRemoveScript] = useState(initialWorktreePreRemoveScript) const [implTabPreCreateScript, setImplTabPreCreateScript] = useState(initialImplTabPreCreateScript) @@ -258,6 +262,7 @@ export function SettingsModal({ autoBuildBranch: autoBuildBranch.trim(), // worktree 目录模板:trim 后留空 = 用默认模板(getSettings 读时回退) worktreeDirectory: worktreeDirectory.trim(), + worktreeOpenWith, // 钩子路径保持原样 trim(前后空格无意义;空串 = 用默认 .spx/*.sh) worktreePostCreateScript: worktreePostCreateScript.trim(), worktreePreRemoveScript: worktreePreRemoveScript.trim(), @@ -642,6 +647,28 @@ export function SettingsModal({ /> + + 点击工单详情里的工作树时用哪个编辑器打开。PyCharm 需 + {' '} + pycharm + {' '} + 命令在 PATH 中(JetBrains Toolbox 里启用命令行启动器)。 + + )} + > + + +