diff --git a/vscode/src/cc/profiles.test.ts b/vscode/src/cc/profiles.test.ts new file mode 100644 index 0000000..bcd3298 --- /dev/null +++ b/vscode/src/cc/profiles.test.ts @@ -0,0 +1,118 @@ +import { accessSync, constants, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import * as os from 'node:os' +import * as path from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + getProfilesDir, + resolveProfilePath, + setProfilesDirOverride, +} from './profiles' + +const originalEnv = process.env.SUPERPOWERS_PROFILES_DIR + +afterEach(() => { + setProfilesDirOverride(undefined) + if (originalEnv === undefined) + delete process.env.SUPERPOWERS_PROFILES_DIR + else + process.env.SUPERPOWERS_PROFILES_DIR = originalEnv +}) + +describe('getProfilesDir', () => { + it('env SUPERPOWERS_PROFILES_DIR 优先', () => { + const dir = mkdtempSync(path.join(os.tmpdir(), 'profiles-env-')) + try { + process.env.SUPERPOWERS_PROFILES_DIR = dir + setProfilesDirOverride('~/ignored') + expect(getProfilesDir()).toBe(path.resolve(dir)) + } + finally { + rmSync(dir, { recursive: true, force: true }) + } + }) + + it('settings override 支持 ~ 并优先于 auto', () => { + delete process.env.SUPERPOWERS_PROFILES_DIR + const dir = mkdtempSync(path.join(os.tmpdir(), 'profiles-override-')) + try { + const tildePath = dir.replace(os.homedir(), '~') + setProfilesDirOverride(tildePath) + expect(getProfilesDir()).toBe(path.resolve(dir)) + } + finally { + rmSync(dir, { recursive: true, force: true }) + } + }) + + it('空 override 时优先 cruldra-profile 真实源(若可读)', () => { + delete process.env.SUPERPOWERS_PROFILES_DIR + setProfilesDirOverride(undefined) + const preferred = path.join(os.homedir(), 'Sources', 'cruldra-profile', 'claude-config', 'profiles') + try { + accessSync(preferred, constants.R_OK) + } + catch { + // 本机无该目录则跳过(CI / 其它机器) + return + } + expect(getProfilesDir()).toBe(preferred) + }) +}) + +describe('resolveProfilePath', () => { + it('空值回退默认 profile 路径', () => { + delete process.env.SUPERPOWERS_PROFILES_DIR + const dir = mkdtempSync(path.join(os.tmpdir(), 'profiles-default-')) + try { + writeFileSync(path.join(dir, 'offical.json'), '{}') + setProfilesDirOverride(dir) + expect(resolveProfilePath(undefined)).toBe(path.join(dir, 'offical.json')) + expect(resolveProfilePath('')).toBe(path.join(dir, 'offical.json')) + } + finally { + rmSync(dir, { recursive: true, force: true }) + } + }) + + it('可读绝对路径原样返回', () => { + delete process.env.SUPERPOWERS_PROFILES_DIR + const dir = mkdtempSync(path.join(os.tmpdir(), 'profiles-readable-')) + try { + const file = path.join(dir, 'grok-4.5.json') + writeFileSync(file, '{}') + setProfilesDirOverride(dir) + expect(resolveProfilePath(file)).toBe(file) + } + finally { + rmSync(dir, { recursive: true, force: true }) + } + }) + + it('跨机绝对路径按 basename remap 到本机 profiles 目录', () => { + delete process.env.SUPERPOWERS_PROFILES_DIR + const dir = mkdtempSync(path.join(os.tmpdir(), 'profiles-remap-')) + try { + writeFileSync(path.join(dir, 'grok-4.5.json'), '{}') + setProfilesDirOverride(dir) + const foreign = '/Users/cruldra/.claude/profile-settings/grok-4.5.json' + expect(resolveProfilePath(foreign)).toBe(path.join(dir, 'grok-4.5.json')) + } + finally { + rmSync(dir, { recursive: true, force: true }) + } + }) + + it('无法 remap 时返回原路径', () => { + delete process.env.SUPERPOWERS_PROFILES_DIR + const dir = mkdtempSync(path.join(os.tmpdir(), 'profiles-miss-')) + try { + mkdirSync(dir, { recursive: true }) + setProfilesDirOverride(dir) + const foreign = '/Users/cruldra/.claude/profile-settings/missing-model.json' + expect(resolveProfilePath(foreign)).toBe(foreign) + } + finally { + rmSync(dir, { recursive: true, force: true }) + } + }) +}) diff --git a/vscode/src/cc/profiles.ts b/vscode/src/cc/profiles.ts index c37a44c..d127655 100644 --- a/vscode/src/cc/profiles.ts +++ b/vscode/src/cc/profiles.ts @@ -1,17 +1,25 @@ /** - * Lists Claude settings profiles from a hardcoded directory. Each `.json` - * file there is treated as a profile whose `name` is its basename without - * the extension and whose `path` is the absolute file path, suitable for - * passing to `claude --settings `. + * Lists Claude settings profiles from a resolved directory. + * Each `.json` file is a profile: `name` = basename without extension, + * `path` = absolute path for `claude --settings `. * - * Failures (directory missing, unreadable, etc.) are swallowed — the - * caller treats an empty list as "no profile selector". + * Resolution order for the profiles directory: + * 1. `$SUPERPOWERS_PROFILES_DIR` (env override) + * 2. settings `profilesDirectory` via setProfilesDirOverride (supports ~) + * 3. auto candidates (first readable): + * ~/Sources/cruldra-profile/claude-config/profiles + * ~/.claude/profile-settings + * ~/.claude/profiles + * 4. fallback to the cruldra-profile path for error messages + * + * Missing/unreadable dirs yield an empty list — never throws. */ +import { accessSync, constants } from 'node:fs' import { readdir } from 'node:fs/promises' +import { homedir } from 'node:os' import * as path from 'node:path' - -const PROFILES_DIR = '/home/cruldra/Sources/cruldra-profile/claude-config/profiles' +import { expandTilde } from '../git/worktree' export interface ClaudeProfile { /** basename without .json */ @@ -20,20 +28,100 @@ export interface ClaudeProfile { path: string } +/** Settings-panel override; empty/undefined means auto-detect. */ +let profilesDirOverride: string | undefined + /** - * List Claude settings profiles from the hardcoded directory. Returns an - * empty array if the directory is missing or unreadable; does not throw. - * Sorted alphabetically by name. + * Apply settings `profilesDirectory` (or clear with empty/undefined). + * Call on activate, settings open, and after settings save. + */ +export function setProfilesDirOverride(dir: string | undefined): void { + const t = dir?.trim() + profilesDirOverride = t ? t : undefined +} + +function isReadableDir(dir: string): boolean { + try { + accessSync(dir, constants.R_OK) + return true + } + catch { + return false + } +} + +function isReadableFile(file: string): boolean { + try { + accessSync(file, constants.R_OK) + return true + } + catch { + return false + } +} + +/** + * Resolve the Claude profiles directory without hardcoding a user home path. + * Prefers the first existing candidate; falls back to the conventional path + * under `$HOME` so error messages stay meaningful when nothing exists yet. + */ +export function getProfilesDir(): string { + // ① env 显式覆盖优先 + const envOverride = process.env.SUPERPOWERS_PROFILES_DIR?.trim() + if (envOverride) + return path.resolve(envOverride) + + // ② 设置面板 override(支持 ~) + if (profilesDirOverride) + return path.resolve(expandTilde(profilesDirOverride)) + + // ③ auto:真实 profile 源优先,再兼容常见链接/目录 + const home = homedir() + const candidates = [ + path.join(home, 'Sources', 'cruldra-profile', 'claude-config', 'profiles'), + path.join(home, '.claude', 'profile-settings'), + path.join(home, '.claude', 'profiles'), + ] + for (const dir of candidates) { + if (isReadableDir(dir)) + return dir + } + return candidates[0]! +} + +/** + * Remap a stored profile path (often an absolute path from another machine) + * onto the current profiles directory by basename when the original is missing. + */ +export function resolveProfilePath(stored: string | undefined): string { + const raw = stored?.trim() + if (!raw) + return getDefaultProfilePath() + + if (isReadableFile(raw)) + return raw + + const remapped = path.join(getProfilesDir(), path.basename(raw)) + if (isReadableFile(remapped)) + return remapped + + return raw +} + +/** + * List Claude settings profiles. Empty array if the directory is missing + * or unreadable. Sorted alphabetically by name. */ export async function listClaudeProfiles(): Promise { + const dir = getProfilesDir() try { - const entries = await readdir(PROFILES_DIR) + const entries = await readdir(dir) const profiles: ClaudeProfile[] = [] for (const entry of entries) { if (!entry.endsWith('.json')) continue const name = entry.slice(0, -'.json'.length) - profiles.push({ name, path: path.join(PROFILES_DIR, entry) }) + profiles.push({ name, path: path.join(dir, entry) }) } profiles.sort((a, b) => a.name.localeCompare(b.name)) return profiles @@ -42,3 +130,31 @@ export async function listClaudeProfiles(): Promise { return [] } } + +/** + * Default profile for new sessions: prefer `offical.json` (legacy spelling), + * then `official.json`. Path is always under getProfilesDir(). + */ +export function getDefaultProfilePath(): string { + const dir = getProfilesDir() + for (const name of ['offical.json', 'official.json']) { + const p = path.join(dir, name) + if (isReadableFile(p)) + return p + } + return path.join(dir, 'offical.json') +} + +/** + * Profile for PR diff summaries / background content generation. + * Prefer `deepseek-v4-pro`, then `deepseek.json`. + */ +export function getPrDiffSummaryProfilePath(): string { + const dir = getProfilesDir() + for (const name of ['deepseek-v4-pro.json', 'deepseek.json']) { + const p = path.join(dir, name) + if (isReadableFile(p)) + return p + } + return path.join(dir, 'deepseek-v4-pro.json') +} diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index f9f6291..b2e7f8e 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -5,7 +5,9 @@ import type { TreeItem, } from 'vscode' import { EventEmitter, commands, window } from 'vscode' +import { setProfilesDirOverride } from './cc/profiles' import { KanbanWebviewPanel } from './panel/KanbanPanel' +import { getSettings } from './settings/store' import { webhookCoordinator } from './webhook/coordinator' /** @@ -26,6 +28,8 @@ class EmptyTreeProvider implements TreeDataProvider { } export function activate(context: ExtensionContext): void { + // Profiles 目录 override 在 panel 未打开时也要生效(list / launch 共用)。 + setProfilesDirOverride(getSettings(context).profilesDirectory || undefined) // Start the webhook server immediately so PR callbacks are received even // when the Kanban panel is closed. Disposed automatically via subscriptions. webhookCoordinator.init(context) diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index c2eb4c8..25dbf85 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -36,11 +36,8 @@ import * as worktree from './handlers/worktree' import * as youtrackIssues from './handlers/youtrackIssues' import { PALETTE, resolveIssueColor, themeColorIdToIconUri } from './issueColor' -export const DEFAULT_PROFILE_PATH = '/home/cruldra/Sources/cruldra-profile/claude-config/profiles/offical.json' - -/** PR 变更摘要等后台内容生成任务用的 profile:DeepSeek(自带 token + base_url, - * headless 稳定,不依赖订阅 OAuth,避免 403 Request not allowed)。 */ -export const PR_DIFF_SUMMARY_PROFILE_PATH = '/home/cruldra/Sources/cruldra-profile/claude-config/profiles/deepseek.json' +/** Resolved at runtime via getProfilesDir() — do not hardcode user paths. */ +export { getDefaultProfilePath, getPrDiffSummaryProfilePath } from '../cc/profiles' export class KanbanWebviewPanel { static readonly viewType = 'superpowers.kanbanPanel' @@ -861,6 +858,7 @@ export class KanbanWebviewPanel { conflictResolutionProfilePath: s.conflictResolutionProfilePath, codexModel: s.codexModel, codexReasoningEffort: s.codexReasoningEffort, + profilesDirectory: s.profilesDirectory, }) return } @@ -926,6 +924,7 @@ export class KanbanWebviewPanel { conflictResolutionProfilePath: s.conflictResolutionProfilePath, codexModel: s.codexModel, codexReasoningEffort: s.codexReasoningEffort, + profilesDirectory: s.profilesDirectory, }) return } diff --git a/vscode/src/panel/handlers/managedSessions.ts b/vscode/src/panel/handlers/managedSessions.ts index ec82740..f4afff0 100644 --- a/vscode/src/panel/handlers/managedSessions.ts +++ b/vscode/src/panel/handlers/managedSessions.ts @@ -5,7 +5,7 @@ import { buildCcCommand } from '../../cc/ccCommand' import { pollForNewSession, projectsDirFor } from '../../cc/sessionWatcher' import { logger } from '../../logging/logger' import { readManagedSessions, writeManagedSessions } from '../../sessions/managedStore' -import { DEFAULT_PROFILE_PATH } from '../KanbanPanel' +import { resolveProfilePath } from '../../cc/profiles' /** 默认会话名:有 prompt 取前 20 字符,否则用短 id(前 8 位)。 */ function defaultSessionName(sessionId: string, prompt?: string): string { @@ -66,8 +66,9 @@ export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, pro return } - const effectiveProfilePath - = profilePath && profilePath.trim() !== '' ? profilePath : DEFAULT_PROFILE_PATH + const effectiveProfilePath = resolveProfilePath( + profilePath && profilePath.trim() !== '' ? profilePath : undefined, + ) // 单引号会破坏下面的 shell 单引号包裹,防御性拒绝(与现有 handler 一致)。 if (effectiveProfilePath.includes('\'')) { void window.showErrorMessage( @@ -219,8 +220,9 @@ export async function handleManagedSessionsResume(panel: KanbanWebviewPanel, ses const data = await readManagedSessions(workspaceRoot) const target = data.sessions.find(s => s.id === sessionId) - const effectiveProfilePath - = target?.profilePath && target.profilePath.trim() !== '' ? target.profilePath : DEFAULT_PROFILE_PATH + const effectiveProfilePath = resolveProfilePath( + target?.profilePath && target.profilePath.trim() !== '' ? target.profilePath : undefined, + ) if (effectiveProfilePath.includes('\'')) { void window.showErrorMessage( `resume 失败:profilePath 含单引号,拒绝执行 (${effectiveProfilePath})`, diff --git a/vscode/src/panel/handlers/sessions.ts b/vscode/src/panel/handlers/sessions.ts index 559840d..12953ee 100644 --- a/vscode/src/panel/handlers/sessions.ts +++ b/vscode/src/panel/handlers/sessions.ts @@ -18,7 +18,8 @@ import { createWorktree, resolveWorktreePath } from '../../git/worktree' import { logger } from '../../logging/logger' import { getSettings } from '../../settings/store' import { webhookCoordinator } from '../../webhook/coordinator' -import { DEFAULT_PROFILE_PATH, makeNonce } from '../KanbanPanel' +import { resolveProfilePath } from '../../cc/profiles' +import { makeNonce } from '../KanbanPanel' export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId: string, profilePath?: string, relCwd?: string, issueNumber?: number): Promise { // kind 跟着 sessionRole 提前判定(原来散落在方法中部,提到入口是为了构造 @@ -1562,9 +1563,11 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: { return } terminal.show(false) - // 冲突解决只用全局设置 conflictResolutionProfilePath;空串 = DEFAULT_PROFILE_PATH。 - const effectiveProfilePath - = settings.conflictResolutionProfilePath.trim() || DEFAULT_PROFILE_PATH + // 冲突解决只用全局设置 conflictResolutionProfilePath;空串 = 默认。 + // 工单/设置里可能持久化了跨机绝对路径,统一走 resolveProfilePath remapping。 + const effectiveProfilePath = resolveProfilePath( + settings.conflictResolutionProfilePath.trim() || undefined, + ) if (effectiveProfilePath.includes('\'')) { logger.add({ level: 'error', @@ -1603,17 +1606,14 @@ export async function startConflictResolution(panel: KanbanWebviewPanel, opts: { * (handleImplement / handleResumeSession when sessionKind === 'implement') * should launch with. Priority: * 1. 工单级 `profilePath`(详情面板「实施配置文件」) - * 2. DEFAULT_PROFILE_PATH + * 2. getDefaultProfilePath() * * 冲突解决用全局 `settings.conflictResolutionProfilePath`。 * 头脑风暴用 resolveBrainstormProfilePath;测试用 resolveTestProfilePath。 * 三者互不回退。 */ export function resolveImplementProfilePath(_panel: KanbanWebviewPanel, issueLevelProfilePath: string | undefined): string { - const issueLevel = issueLevelProfilePath?.trim() - if (issueLevel) - return issueLevel - return DEFAULT_PROFILE_PATH + return resolveProfilePath(issueLevelProfilePath?.trim() || undefined) } /** @@ -1621,7 +1621,7 @@ export function resolveImplementProfilePath(_panel: KanbanWebviewPanel, issueLev * 不回退实施 profile。 */ export function resolveBrainstormProfilePath(_panel: KanbanWebviewPanel, brainstormProfilePath: string | undefined): string { - return brainstormProfilePath?.trim() || DEFAULT_PROFILE_PATH + return resolveProfilePath(brainstormProfilePath?.trim() || undefined) } /** @@ -1629,7 +1629,7 @@ export function resolveBrainstormProfilePath(_panel: KanbanWebviewPanel, brainst * 不回退实施 profile。 */ export function resolveTestProfilePath(_panel: KanbanWebviewPanel, testProfilePath: string | undefined): string { - return testProfilePath?.trim() || DEFAULT_PROFILE_PATH + return resolveProfilePath(testProfilePath?.trim() || undefined) } /** diff --git a/vscode/src/panel/handlers/settings.ts b/vscode/src/panel/handlers/settings.ts index 1c7fe63..248d623 100644 --- a/vscode/src/panel/handlers/settings.ts +++ b/vscode/src/panel/handlers/settings.ts @@ -6,7 +6,7 @@ import * as os from 'node:os' import * as path from 'node:path' import { commands, env, Uri, workspace } from 'vscode' import { getToken, getYouTrackToken, setToken, setYouTrackToken } from '../../auth/secrets' -import { listClaudeProfiles } from '../../cc/profiles' +import { listClaudeProfiles, setProfilesDirOverride } from '../../cc/profiles' import { detectRepo } from '../../git/remote' import { logger } from '../../logging/logger' import { readProfiles, writeProfiles } from '../../profiles/store' @@ -35,6 +35,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { conflictResolutionProfilePath: string codexModel: string codexReasoningEffort: string + profilesDirectory: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string @@ -66,6 +67,8 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { // codex 模型 / 思考级别:空串有意义(= 用 codex 默认),只 trim。 const trimmedCodexModel = payload.codexModel.trim() const trimmedCodexReasoningEffort = payload.codexReasoningEffort.trim() + // profiles 目录:空串有意义(= 自动探测),只 trim。 + const trimmedProfilesDirectory = payload.profilesDirectory.trim() const prev = getSettings(panel.context) // Capture the previous token *for this host* before overwriting it, so // we can decide below whether the kanban needs a re-fetch. (Only host @@ -98,6 +101,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { conflictResolutionProfilePath: trimmedConflictProfile, codexModel: trimmedCodexModel, codexReasoningEffort: trimmedCodexReasoningEffort, + profilesDirectory: trimmedProfilesDirectory, }) return } @@ -122,10 +126,14 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { conflictResolutionProfilePath: trimmedConflictProfile, codexModel: trimmedCodexModel, codexReasoningEffort: trimmedCodexReasoningEffort, + profilesDirectory: trimmedProfilesDirectory, youtrackBaseUrl: trimmedYtBase, youtrackProjectShortName: trimmedYtProject, youtrackCloseCommand: trimmedYtClose, }) + // 保存后立刻让 listClaudeProfiles / getDefaultProfilePath 吃到新目录。 + setProfilesDirOverride(trimmedProfilesDirectory || undefined) + void handleProfilesList(panel) if (!keepExisting) await setToken(panel.context, trimmedHost, trimmedToken) // YouTrack token: stored under the base URL's host. Empty input keeps the @@ -179,6 +187,8 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom host = remote.host } const s = getSettings(panel.context) + // 打开设置时同步 override,避免未 activate 路径或旧值漂移。 + setProfilesDirOverride(s.profilesDirectory || undefined) const tok = host ? await getToken(panel.context, host) : undefined const tokenSaved = !!tok && tok.length > 0 const ytTok = s.youtrackBaseUrl.trim() @@ -207,6 +217,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom conflictResolutionProfilePath: s.conflictResolutionProfilePath, codexModel: s.codexModel, codexReasoningEffort: s.codexReasoningEffort, + profilesDirectory: s.profilesDirectory, youtrackBaseUrl: s.youtrackBaseUrl, youtrackProjectShortName: s.youtrackProjectShortName, youtrackCloseCommand: s.youtrackCloseCommand, diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index a76ef34..6e11b11 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -69,6 +69,7 @@ export type ExtensionToWebview conflictResolutionProfilePath: string codexModel: string codexReasoningEffort: string + profilesDirectory: string youtrackBaseUrl?: string youtrackProjectShortName?: string youtrackCloseCommand?: string @@ -133,6 +134,7 @@ export type WebviewToExtension conflictResolutionProfilePath: string codexModel: string codexReasoningEffort: string + profilesDirectory: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string diff --git a/vscode/src/settings/store.ts b/vscode/src/settings/store.ts index 26346b2..28fb15e 100644 --- a/vscode/src/settings/store.ts +++ b/vscode/src/settings/store.ts @@ -179,6 +179,11 @@ export interface Settings { * 用 codex 默认。合法值 minimal/low/medium/high/xhigh。 */ codexReasoningEffort: string + /** + * Claude `--settings` profile 所在目录。空串 = 自动探测。 + * 支持前导 `~`。保存后 listClaudeProfiles / getDefaultProfilePath 都从这里读。 + */ + profilesDirectory: string } export const SETTINGS_KEY = 'superpowers.settings' @@ -206,6 +211,7 @@ function defaults(ctx: ExtensionContext): Settings { conflictResolutionProfilePath: '', codexModel: '', codexReasoningEffort: '', + profilesDirectory: '', } } @@ -297,6 +303,10 @@ export function getSettings(ctx: ExtensionContext): Settings { const codexReasoningEffort = typeof stored.codexReasoningEffort === 'string' ? stored.codexReasoningEffort : base.codexReasoningEffort + // profiles 目录:'' 有意义(= 自动探测),不强制回退。 + const profilesDirectory = typeof stored.profilesDirectory === 'string' + ? stored.profilesDirectory + : base.profilesDirectory return { webhookPort, @@ -320,6 +330,7 @@ export function getSettings(ctx: ExtensionContext): Settings { conflictResolutionProfilePath, codexModel, codexReasoningEffort, + profilesDirectory, } } diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index 0410a5f..f5f405e 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -460,6 +460,7 @@ export function App() { initialConflictResolutionProfilePath={settings?.conflictResolutionProfilePath ?? ''} initialCodexModel={settings?.codexModel ?? ''} initialCodexReasoningEffort={settings?.codexReasoningEffort ?? ''} + initialProfilesDirectory={settings?.profilesDirectory ?? ''} initialYoutrackBaseUrl={settings?.youtrackBaseUrl ?? ''} initialYoutrackProjectShortName={settings?.youtrackProjectShortName ?? ''} initialYoutrackCloseCommand={settings?.youtrackCloseCommand ?? ''} diff --git a/vscode/webview-ui/src/components/SettingsModal.tsx b/vscode/webview-ui/src/components/SettingsModal.tsx index 1baa829..8eab8d7 100644 --- a/vscode/webview-ui/src/components/SettingsModal.tsx +++ b/vscode/webview-ui/src/components/SettingsModal.tsx @@ -34,6 +34,7 @@ interface SubmitValues { conflictResolutionProfilePath: string codexModel: string codexReasoningEffort: string + profilesDirectory: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string @@ -76,6 +77,7 @@ export interface SettingsModalProps { initialConflictResolutionProfilePath: string initialCodexModel: string initialCodexReasoningEffort: string + initialProfilesDirectory: string initialYoutrackBaseUrl: string initialYoutrackProjectShortName: string initialYoutrackCloseCommand: string @@ -152,6 +154,7 @@ export function SettingsModal({ initialConflictResolutionProfilePath, initialCodexModel, initialCodexReasoningEffort, + initialProfilesDirectory, initialYoutrackBaseUrl, initialYoutrackProjectShortName, initialYoutrackCloseCommand, @@ -182,6 +185,7 @@ export function SettingsModal({ const [conflictResolutionProfilePath, setConflictResolutionProfilePath] = useState(initialConflictResolutionProfilePath) const [codexModel, setCodexModel] = useState(initialCodexModel) const [codexReasoningEffort, setCodexReasoningEffort] = useState(initialCodexReasoningEffort) + const [profilesDirectory, setProfilesDirectory] = useState(initialProfilesDirectory) const [youtrackBaseUrl, setYoutrackBaseUrl] = useState(initialYoutrackBaseUrl) const [youtrackProjectShortName, setYoutrackProjectShortName] = useState(initialYoutrackProjectShortName) const [youtrackCloseCommand, setYoutrackCloseCommand] = useState(initialYoutrackCloseCommand) @@ -214,6 +218,7 @@ export function SettingsModal({ setConflictResolutionProfilePath(initialConflictResolutionProfilePath) setCodexModel(initialCodexModel) setCodexReasoningEffort(initialCodexReasoningEffort) + setProfilesDirectory(initialProfilesDirectory) setYoutrackBaseUrl(initialYoutrackBaseUrl) setYoutrackProjectShortName(initialYoutrackProjectShortName) setYoutrackCloseCommand(initialYoutrackCloseCommand) @@ -300,6 +305,8 @@ export function SettingsModal({ // codex 模型 / 思考级别:trim 后留空 = 用 codex config.toml 默认 codexModel: codexModel.trim(), codexReasoningEffort: codexReasoningEffort.trim(), + // profiles 目录:trim 后留空 = 自动探测 + profilesDirectory: profilesDirectory.trim(), youtrackBaseUrl: youtrackBaseUrl.trim(), youtrackProjectShortName: youtrackProjectShortName.trim(), youtrackCloseCommand: youtrackCloseCommand.trim(), @@ -559,6 +566,29 @@ export function SettingsModal({ 启用 + + + claude --settings 用的 .json 所在目录。留空自动探测(优先 + {' '} + ~/Sources/cruldra-profile/claude-config/profiles + )。支持 + {' '} + ~ + 。 + + )} + > + setProfilesDirectory(e.target.value)} + placeholder="~/Sources/cruldra-profile/claude-config/profiles" + className={inputClass} + /> + )} diff --git a/vscode/webview-ui/src/hooks/useIssues.ts b/vscode/webview-ui/src/hooks/useIssues.ts index 5c2ef92..729a1a3 100644 --- a/vscode/webview-ui/src/hooks/useIssues.ts +++ b/vscode/webview-ui/src/hooks/useIssues.ts @@ -43,6 +43,7 @@ export interface SettingsValues { conflictResolutionProfilePath: string codexModel: string codexReasoningEffort: string + profilesDirectory: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string @@ -71,6 +72,7 @@ export interface SettingsOverlayState { conflictResolutionProfilePath: string codexModel: string codexReasoningEffort: string + profilesDirectory: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string @@ -290,6 +292,7 @@ export function useIssues(): UseIssuesResult { conflictResolutionProfilePath: values.conflictResolutionProfilePath, codexModel: values.codexModel, codexReasoningEffort: values.codexReasoningEffort, + profilesDirectory: values.profilesDirectory, youtrackBaseUrl: values.youtrackBaseUrl, youtrackProjectShortName: values.youtrackProjectShortName, youtrackCloseCommand: values.youtrackCloseCommand, @@ -635,6 +638,7 @@ export function useIssues(): UseIssuesResult { conflictResolutionProfilePath: msg.conflictResolutionProfilePath, codexModel: msg.codexModel, codexReasoningEffort: msg.codexReasoningEffort, + profilesDirectory: msg.profilesDirectory, youtrackBaseUrl: msg.youtrackBaseUrl ?? '', youtrackProjectShortName: msg.youtrackProjectShortName ?? '', youtrackCloseCommand: msg.youtrackCloseCommand ?? '', diff --git a/vscode/webview-ui/src/lib/messages.ts b/vscode/webview-ui/src/lib/messages.ts index 2ed00d2..ec511e2 100644 --- a/vscode/webview-ui/src/lib/messages.ts +++ b/vscode/webview-ui/src/lib/messages.ts @@ -84,6 +84,7 @@ export type ExtensionToWebview conflictResolutionProfilePath: string codexModel: string codexReasoningEffort: string + profilesDirectory: string youtrackBaseUrl?: string youtrackProjectShortName?: string youtrackCloseCommand?: string @@ -148,6 +149,7 @@ export type WebviewToExtension conflictResolutionProfilePath: string codexModel: string codexReasoningEffort: string + profilesDirectory: string youtrackBaseUrl: string youtrackProjectShortName: string youtrackCloseCommand: string