diff --git a/vscode/README.md b/vscode/README.md index c9f6e20..a3b3351 100644 --- a/vscode/README.md +++ b/vscode/README.md @@ -129,6 +129,7 @@ prompts 在 `prompts/*.md`,随 VSIX 一起安装,改完即生效,也可在 | | `autoBuildBranch` | 空 | Jenkins 等监听的分支;空 = 与 `devBranch` 相同,分支同步按钮禁用 | | | `conflictResolutionProfilePath` | 空 | 冲突解决会话 profile | | | `commitProfilePath` | `deepseek-v4-pro` | 「提交代码」用的 profile;未配置时提交直接失败 | +| | `prDiffSummaryProfilePath` | `deepseek-v4-pro` | 「改动」说明 / PR 变更摘要用的 profile;未配置时生成失败 | | | `codexModel` / `codexReasoningEffort` | 空 | 审查会话的 codex `-c` 覆盖;空 = 用 codex 自己的 config | | 网络 | `webhookPort` | `17421` | 本机 webhook 端口 | | | `webhookSecret` | 空 | Gitea webhook secret,空 = 不校验签名 | diff --git a/vscode/package.json b/vscode/package.json index 6ae1a33..a5b1f15 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.100", + "version": "0.2.101", "packageManager": "pnpm@10.27.0", "description": "Superpowers specs and plans Kanban explorer", "author": "clurdra", diff --git a/vscode/src/cc/profiles.ts b/vscode/src/cc/profiles.ts index d127655..8ea2c47 100644 --- a/vscode/src/cc/profiles.ts +++ b/vscode/src/cc/profiles.ts @@ -37,7 +37,7 @@ let profilesDirOverride: string | undefined */ export function setProfilesDirOverride(dir: string | undefined): void { const t = dir?.trim() - profilesDirOverride = t ? t : undefined + profilesDirOverride = t || undefined } function isReadableDir(dir: string): boolean { @@ -144,17 +144,3 @@ export function getDefaultProfilePath(): string { } 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/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index 8d1d124..c60afff 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -24,7 +24,7 @@ import { overlayLocalIssueState } from '../issues/localState' import { closeIssueByRef, mergeIssueState, readIssueState } from '../issues/stateRouter' import { logger } from '../logging/logger' import { annotateReviewSessionFileExists } from '../sessions/codexSessions' -import { getEffectiveCommitProfilePath, getSettings } from '../settings/store' +import { getEffectiveCommitProfilePath, getEffectivePrDiffSummaryProfilePath, getSettings } from '../settings/store' import { webhookCoordinator } from '../webhook/coordinator' import { loadYouTrackIssues } from '../youtrack/issueLoader' import * as handoffFlow from './handlers/handoffFlow' @@ -44,7 +44,7 @@ import { PALETTE, resolveIssueColor, themeColorIdToIconUri } from './issueColor' import { KANBAN_SCOPE_KEY } from './kanbanScope' /** Resolved at runtime via getProfilesDir() — do not hardcode user paths. */ -export { getDefaultProfilePath, getPrDiffSummaryProfilePath } from '../cc/profiles' +export { getDefaultProfilePath } from '../cc/profiles' export class KanbanWebviewPanel { static readonly viewType = 'superpowers.kanbanPanel' @@ -875,6 +875,7 @@ export class KanbanWebviewPanel { implTabPostCloseScript: s.implTabPostCloseScript, conflictResolutionProfilePath: s.conflictResolutionProfilePath, commitProfilePath: await getEffectiveCommitProfilePath(this.context), + prDiffSummaryProfilePath: await getEffectivePrDiffSummaryProfilePath(this.context), codexModel: s.codexModel, codexReasoningEffort: s.codexReasoningEffort, profilesDirectory: s.profilesDirectory, @@ -954,6 +955,7 @@ export class KanbanWebviewPanel { implTabPostCloseScript: s.implTabPostCloseScript, conflictResolutionProfilePath: s.conflictResolutionProfilePath, commitProfilePath: await getEffectiveCommitProfilePath(this.context), + prDiffSummaryProfilePath: await getEffectivePrDiffSummaryProfilePath(this.context), codexModel: s.codexModel, codexReasoningEffort: s.codexReasoningEffort, profilesDirectory: s.profilesDirectory, diff --git a/vscode/src/panel/handlers/issues.ts b/vscode/src/panel/handlers/issues.ts index 891bdfa..e3484a4 100644 --- a/vscode/src/panel/handlers/issues.ts +++ b/vscode/src/panel/handlers/issues.ts @@ -9,7 +9,8 @@ import * as path from 'node:path' import { commands, env, ThemeColor, Uri, window, workspace } from 'vscode' import { getToken } from '../../auth/secrets' import { buildCcCommand } from '../../cc/ccCommand' -import { getDefaultProfilePath, getPrDiffSummaryProfilePath } from '../../cc/profiles' +import { resolveCommitProfileForRun } from '../../cc/commitProfile' +import { getDefaultProfilePath, resolveProfilePath } from '../../cc/profiles' import { getBrainstormPrompt } from '../../cc/prompts' import { claudeProjectsRoot, installClaudeSession } from '../../cc/sessionBundle' import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher' @@ -31,7 +32,7 @@ import { import { isValidSpxFilePath, loadIssues } from '../../gitea/issueLoader' import { mergeStateJsonComment } from '../../gitea/stateJson' import { logger } from '../../logging/logger' -import { getSettings } from '../../settings/store' +import { getEffectivePrDiffSummaryProfilePath, getSettings } from '../../settings/store' import { webhookCoordinator } from '../../webhook/coordinator' import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor' import { makeNonce } from '../KanbanPanel' @@ -2275,7 +2276,34 @@ export async function handleGeneratePrDiffSummary(panel: KanbanWebviewPanel, iss dismissOnTimer: 5000, }) - const profilePath = getPrDiffSummaryProfilePath() + const rawProfile = await getEffectivePrDiffSummaryProfilePath(panel.context) + const resolution = resolveCommitProfileForRun( + rawProfile, + resolveProfilePath, + (p) => { + try { + fs.accessSync(p, fs.constants.R_OK) + return true + } + catch { + return false + } + }, + ) + if (!resolution.ok) { + const message = resolution.reason === 'empty' + ? '未配置改动说明 profile,请在设置中选择' + : `改动说明 profile 不可用:${resolution.profilePath}` + panel.postMessage({ + type: 'toast/show', + id: randomUUID(), + level: 'error', + message, + dismissOnTimer: 8000, + }) + return + } + const profilePath = resolution.profilePath const prompt = `你在一个 VS Code 扩展启动的后台 Claude 会话中工作。\n\n硬性限制:\n- 不要修改代码。\n- 不要创建或切换分支。\n- 不要提交。\n- 不要 push。\n- 不要写任何文件。\n- 只分析当前 PR 的代码变动,并把 Markdown 摘要正文直接输出到 stdout。\n\n任务:\n- 当前工作目录是工单 #\${issueNumber} 的 worktree。\n- 分析 PR #\${pr} 与主分支的代码差异。\n- 用 git 拿差异:先 git fetch origin main,再 git diff origin/main...HEAD(概览可加 --stat)。注意 tea 没有 diff 子命令,不要尝试 tea pulls diff。\n\n输出必须是合法 Markdown,预览器要能渲染出标题层级与代码块高亮,结构如下:\n1. 第一行是一级标题:# PR #\${pr} 代码变更摘要\n2. 紧接着用 1~2 句话概述本次变更:做了什么、新增了哪些包/模块、关键依赖方向(例如「stockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入」)。\n3. 然后写一行图例:图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n4. 然后是一棵统一的目录树,放进单个围栏代码块(语言标注用 text):树前一行写三个反引号紧跟 text,树后一行写三个反引号。\n\n目录树规则:\n- 用 box-drawing 连线字符画树:├──、└──、│,缩进对齐,像 tree 命令的输出,不要只用纯空格缩进糊一棵没有连线的树。\n- 从仓库根开始保留真实目录层级。\n- 每个文件行以标记开头:新建文件用「+ 」、修改文件用「✎ 」,后接文件名 + 若干空格对齐 + 一句中文目的描述(简短,点出关键符号/端点/职责)。\n- 目录节点行尾可加可选的阶段/分组标签(如 [Phase N]),仅当本次变更本身有清晰阶段时才加;没有就不加,不要硬编。\n- 关键:整棵树一定要写在围栏代码块之内,否则 Markdown 预览器会折叠空白、丢掉层级与连线。\n\n排序与重点:\n- 按架构重要性排列:核心业务/领域代码(实现逻辑、入口、API routes、数据模型、service/repository)靠前。\n- alembic 数据库迁移文件、测试文件不算重点:放在各自模块或整棵树的靠后位置,照常用 +/✎ 标记列出,但不要放在最前、不要强调。\n- 测试文件判定:文件名形如 test_* / *_test.* / *.test.* / *.spec.*,或路径包含 tests/、__tests__/ 目录,或 conftest.py、fixture、mock 文件。\n- 迁移文件判定:路径在 alembic/、migrations/、versions/ 之下的文件。\n\n格式示例(请照此结构输出,不要照抄内容):\n\n# PR #123 代码变更摘要\n\nstockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入。\n\n图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n\n\\\`\\\`\\\`text\nbackend/\n└── src/luffy_agent/commerce/\n ├── models.py ✎ +StockPool;ProductCatalog 加 pool_id\n ├── stockpool/\n │ ├── service.py + reserve_pool_or_raise + compute_pool_views\n │ └── routes.py + /admin/commerce/stock-pools\n ├── orders/service.py ✎ create_order 绑定池分支\n └── alembic/versions/ + stock_pool 建表 + pool_id 加列\n\\\`\\\`\\\`\n\n要求:\n- 每个新增/修改文件一行,一句话说明新增/修改了什么及其目的。\n- 整棵树只用一个围栏代码块,用 box-drawing 连线保留真实目录层级。\n- 只输出上述 Markdown 正文本身,不要写文件,不要任何前导说明/寒暄/结尾总结,第一行就是 # PR #\${pr} 代码变更摘要 标题。` const result = await spawnClaude({ diff --git a/vscode/src/panel/handlers/prFiles.ts b/vscode/src/panel/handlers/prFiles.ts index 2085646..d7f4d1c 100644 --- a/vscode/src/panel/handlers/prFiles.ts +++ b/vscode/src/panel/handlers/prFiles.ts @@ -8,18 +8,22 @@ * 404 兜底空串,故新增/删除/改名天然成立。 */ +import type { PrDiffMode } from '../../cc/prDiffMode' import type { KanbanWebviewPanel } from '../KanbanPanel' +import { accessSync, constants } from 'node:fs' import { createPatch } from 'diff' import { workspace } from 'vscode' import { getToken } from '../../auth/secrets' -import { listClaudeProfiles } from '../../cc/profiles' +import { resolveCommitProfileForRun } from '../../cc/commitProfile' +import { parsePrDiffMode } from '../../cc/prDiffMode' +import { resolveProfilePath } from '../../cc/profiles' import { spawnClaude } from '../../cc/spawnClaude' import { detectRepo } from '../../git/remote' import { getPullRequest, getRawFile, listPullRequestFiles } from '../../gitea/api' import { readStateJsonComment } from '../../gitea/stateJson' import { readFileSummaries, readSummarySession, writeFileSummary, writeSummarySession } from '../../sessions/prFileSummaryStore' import { readConfirmedSigs, writeConfirmedSigs } from '../../sessions/prReviewStore' -import { parsePrDiffMode, type PrDiffMode } from '../../cc/prDiffMode' +import { getEffectivePrDiffSummaryProfilePath } from '../../settings/store' import { makeNonce } from '../KanbanPanel' /** @@ -259,7 +263,7 @@ function buildSummaryPrompt(prNumber: number, prBody: string, filePath: string, } /** - * 用 deepseek 给单个文件生成「一句话说明改了什么」的改动说明。增量复用同一会话:首个文件开新会话并把 + * 给单个文件生成「一句话说明改了什么」的改动说明。增量复用同一会话:首个文件开新会话并把 * sessionId 记到 `.spx/pr-file-summaries.json`,后续文件 `--resume` 接上去,省去每次重新理解 * PR 的开销。说明也落盘,重载后仍显示。 * @@ -279,18 +283,32 @@ export async function handleGeneratePrFileSummary( return } - // deepseek profile 与「一键提交」同一来源;缺失则提示用户创建并清掉前端 loading。 - const profiles = await listClaudeProfiles() - const deepseek = profiles.find(p => p.name === 'deepseek-v4-pro') - if (!deepseek) { + const raw = await getEffectivePrDiffSummaryProfilePath(panel.context) + const resolution = resolveCommitProfileForRun( + raw, + resolveProfilePath, + (p) => { + try { + accessSync(p, constants.R_OK) + return true + } + catch { + return false + } + }, + ) + if (!resolution.ok) { + const message = resolution.reason === 'empty' + ? '未配置改动说明 profile,请在设置中选择' + : `改动说明 profile 不可用:${resolution.profilePath}` panel.postMessage({ type: 'toast/show', id: makeNonce(), level: 'error', - message: '未找到 deepseek profile(deepseek-v4-pro / deepseek),请在 Claude profiles 目录下创建对应 .json', + message, dismissOnTimer: 8000, }) - panel.postMessage({ type: 'pr-file-summary/show', issueNumber, path: filePath, error: '未找到 deepseek profile' }) + panel.postMessage({ type: 'pr-file-summary/show', issueNumber, path: filePath, error: message }) return } @@ -315,7 +333,7 @@ export async function handleGeneratePrFileSummary( result = await spawnClaude({ prompt: buildSummaryPrompt(index, pr.body, filePath, patch, !prevSession), cwd: workspaceRoot, - profilePath: deepseek.path, + profilePath: resolution.profilePath, resumeSessionId: prevSession, timeoutMs: 120_000, }) @@ -328,7 +346,7 @@ export async function handleGeneratePrFileSummary( result = await spawnClaude({ prompt: buildSummaryPrompt(index, pr.body, filePath, patch, true), cwd: workspaceRoot, - profilePath: deepseek.path, + profilePath: resolution.profilePath, timeoutMs: 120_000, }) } @@ -370,4 +388,3 @@ export async function handleSetPrDiffMode( const next: PrDiffMode = parsePrDiffMode(mode) await panel.context.workspaceState.update(PR_DIFF_MODE_KEY, next) } - diff --git a/vscode/src/panel/handlers/settings.ts b/vscode/src/panel/handlers/settings.ts index be01ac2..8499db1 100644 --- a/vscode/src/panel/handlers/settings.ts +++ b/vscode/src/panel/handlers/settings.ts @@ -11,7 +11,7 @@ import { listClaudeProfiles, setProfilesDirOverride } from '../../cc/profiles' import { detectRepo } from '../../git/remote' import { logger } from '../../logging/logger' import { readProfiles, writeProfiles } from '../../profiles/store' -import { getEffectiveCommitProfilePath, getSettings, saveSettings } from '../../settings/store' +import { getEffectiveCommitProfilePath, getEffectivePrDiffSummaryProfilePath, getSettings, saveSettings } from '../../settings/store' import { webhookCoordinator } from '../../webhook/coordinator' import { youtrackHost } from '../../youtrack/issueLoader' import { makeNonce } from '../KanbanPanel' @@ -37,6 +37,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { implTabPostCloseScript: string conflictResolutionProfilePath: string commitProfilePath: string + prDiffSummaryProfilePath: string codexModel: string codexReasoningEffort: string profilesDirectory: string @@ -70,6 +71,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { const trimmedConflictProfile = payload.conflictResolutionProfilePath.trim() // 提交代码 profile:'' 有意义(= 未配置 → 提交严格失败),只 trim。 const trimmedCommitProfile = payload.commitProfilePath.trim() + const trimmedPrDiffSummaryProfile = payload.prDiffSummaryProfilePath.trim() // codex 模型 / 思考级别:空串有意义(= 用 codex 默认),只 trim。 const trimmedCodexModel = payload.codexModel.trim() const trimmedCodexReasoningEffort = payload.codexReasoningEffort.trim() @@ -108,6 +110,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { implTabPostCloseScript: trimmedImplPost, conflictResolutionProfilePath: trimmedConflictProfile, commitProfilePath: trimmedCommitProfile, + prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile, codexModel: trimmedCodexModel, codexReasoningEffort: trimmedCodexReasoningEffort, profilesDirectory: trimmedProfilesDirectory, @@ -136,6 +139,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { implTabPostCloseScript: trimmedImplPost, conflictResolutionProfilePath: trimmedConflictProfile, commitProfilePath: trimmedCommitProfile, + prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile, codexModel: trimmedCodexModel, codexReasoningEffort: trimmedCodexReasoningEffort, profilesDirectory: trimmedProfilesDirectory, @@ -213,6 +217,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom : undefined // 升级 seed:key 缺失时预填 deepseek path(若有),仅展示不写回。 const commitProfilePath = await getEffectiveCommitProfilePath(panel.context) + const prDiffSummaryProfilePath = await getEffectivePrDiffSummaryProfilePath(panel.context) // User clicked the gear themselves — let them back out without saving. panel.postMessage({ type: 'settings/show', @@ -237,6 +242,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom implTabPostCloseScript: s.implTabPostCloseScript, conflictResolutionProfilePath: s.conflictResolutionProfilePath, commitProfilePath, + prDiffSummaryProfilePath, codexModel: s.codexModel, codexReasoningEffort: s.codexReasoningEffort, profilesDirectory: s.profilesDirectory, diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index a765971..028f70c 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -83,6 +83,7 @@ export type ExtensionToWebview implTabPostCloseScript: string conflictResolutionProfilePath: string commitProfilePath: string + prDiffSummaryProfilePath: string codexModel: string codexReasoningEffort: string profilesDirectory: string @@ -154,6 +155,7 @@ export type WebviewToExtension implTabPostCloseScript: string conflictResolutionProfilePath: string commitProfilePath: string + prDiffSummaryProfilePath: string codexModel: string codexReasoningEffort: string profilesDirectory: string diff --git a/vscode/src/settings/store.ts b/vscode/src/settings/store.ts index 63e0058..626e119 100644 --- a/vscode/src/settings/store.ts +++ b/vscode/src/settings/store.ts @@ -181,6 +181,11 @@ export interface Settings { * 空串 = 未配置 → 提交严格失败。升级时 key 缺失会 seed deepseek-v4-pro(若存在)。 */ commitProfilePath: string + /** + * 「改动」tab 单文件说明 / 工单 PR 变更摘要使用的 Claude profile 绝对路径。 + * 空串 = 未配置 → 生成失败。升级时 key 缺失会 seed deepseek-v4-pro(若存在)。 + */ + prDiffSummaryProfilePath: string /** * codex 审查会话使用的模型(`-c model=`)。空串 = 不传,用 codex * `config.toml` 默认。 @@ -223,6 +228,7 @@ function defaults(ctx: ExtensionContext): Settings { youtrackCloseCommand: '', conflictResolutionProfilePath: '', commitProfilePath: '', + prDiffSummaryProfilePath: '', codexModel: '', codexReasoningEffort: '', profilesDirectory: '', @@ -319,6 +325,9 @@ export function getSettings(ctx: ExtensionContext): Settings { const commitProfilePath = typeof stored.commitProfilePath === 'string' ? stored.commitProfilePath : base.commitProfilePath + const prDiffSummaryProfilePath = typeof stored.prDiffSummaryProfilePath === 'string' + ? stored.prDiffSummaryProfilePath + : base.prDiffSummaryProfilePath // codex 模型 / 思考级别:'' 有意义(= 用 codex 默认),不强制回退。 const codexModel = typeof stored.codexModel === 'string' ? stored.codexModel @@ -353,6 +362,7 @@ export function getSettings(ctx: ExtensionContext): Settings { youtrackCloseCommand, conflictResolutionProfilePath, commitProfilePath, + prDiffSummaryProfilePath, codexModel, codexReasoningEffort, profilesDirectory, @@ -372,6 +382,17 @@ export async function getEffectiveCommitProfilePath(ctx: ExtensionContext): Prom return seedCommitProfilePath(undefined, profiles) } +/** + * 读取改动说明有效的 profile path。规则与提交代码相同:已有 key 原样返回,缺 key 才 seed。 + */ +export async function getEffectivePrDiffSummaryProfilePath(ctx: ExtensionContext): Promise { + const stored = ctx.globalState.get>(SETTINGS_KEY) ?? {} + if (typeof stored.prDiffSummaryProfilePath === 'string') + return stored.prDiffSummaryProfilePath + const profiles = await listClaudeProfiles() + return seedCommitProfilePath(undefined, profiles) +} + export async function saveSettings(ctx: ExtensionContext, next: Partial): Promise { const current = ctx.globalState.get>(SETTINGS_KEY) ?? {} const merged: Partial = { ...current, ...next } diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index fe3126c..8f358a8 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -533,6 +533,7 @@ export function App() { initialImplTabPostCloseScript={settings?.implTabPostCloseScript ?? ''} initialConflictResolutionProfilePath={settings?.conflictResolutionProfilePath ?? ''} initialCommitProfilePath={settings?.commitProfilePath ?? ''} + initialPrDiffSummaryProfilePath={settings?.prDiffSummaryProfilePath ?? ''} initialCodexModel={settings?.codexModel ?? ''} initialCodexReasoningEffort={settings?.codexReasoningEffort ?? ''} initialProfilesDirectory={settings?.profilesDirectory ?? ''} diff --git a/vscode/webview-ui/src/components/SettingsModal.tsx b/vscode/webview-ui/src/components/SettingsModal.tsx index fe22601..bbd7a23 100644 --- a/vscode/webview-ui/src/components/SettingsModal.tsx +++ b/vscode/webview-ui/src/components/SettingsModal.tsx @@ -36,6 +36,7 @@ interface SubmitValues { implTabPostCloseScript: string conflictResolutionProfilePath: string commitProfilePath: string + prDiffSummaryProfilePath: string codexModel: string codexReasoningEffort: string profilesDirectory: string @@ -82,6 +83,7 @@ export interface SettingsModalProps { initialImplTabPostCloseScript: string initialConflictResolutionProfilePath: string initialCommitProfilePath: string + initialPrDiffSummaryProfilePath: string initialCodexModel: string initialCodexReasoningEffort: string initialProfilesDirectory: string @@ -164,6 +166,7 @@ export function SettingsModal({ initialImplTabPostCloseScript, initialConflictResolutionProfilePath, initialCommitProfilePath, + initialPrDiffSummaryProfilePath, initialCodexModel, initialCodexReasoningEffort, initialProfilesDirectory, @@ -198,6 +201,7 @@ export function SettingsModal({ const [implTabPostCloseScript, setImplTabPostCloseScript] = useState(initialImplTabPostCloseScript) const [conflictResolutionProfilePath, setConflictResolutionProfilePath] = useState(initialConflictResolutionProfilePath) const [commitProfilePath, setCommitProfilePath] = useState(initialCommitProfilePath) + const [prDiffSummaryProfilePath, setPrDiffSummaryProfilePath] = useState(initialPrDiffSummaryProfilePath) const [codexModel, setCodexModel] = useState(initialCodexModel) const [codexReasoningEffort, setCodexReasoningEffort] = useState(initialCodexReasoningEffort) const [profilesDirectory, setProfilesDirectory] = useState(initialProfilesDirectory) @@ -234,6 +238,7 @@ export function SettingsModal({ setImplTabPostCloseScript(initialImplTabPostCloseScript) setConflictResolutionProfilePath(initialConflictResolutionProfilePath) setCommitProfilePath(initialCommitProfilePath) + setPrDiffSummaryProfilePath(initialPrDiffSummaryProfilePath) setCodexModel(initialCodexModel) setCodexReasoningEffort(initialCodexReasoningEffort) setProfilesDirectory(initialProfilesDirectory) @@ -324,6 +329,7 @@ export function SettingsModal({ conflictResolutionProfilePath: conflictResolutionProfilePath.trim(), // 提交代码 profile:空串 = 未配置 → 提交严格失败 commitProfilePath: commitProfilePath.trim(), + prDiffSummaryProfilePath: prDiffSummaryProfilePath.trim(), // codex 模型 / 思考级别:trim 后留空 = 用 codex config.toml 默认 codexModel: codexModel.trim(), codexReasoningEffort: codexReasoningEffort.trim(), @@ -528,6 +534,30 @@ export function SettingsModal({ /> + + 「改动」tab 单文件说明和工单 PR 变更摘要使用此 profile。留空则生成失败,不会回退默认。 + + )} + > + ({ value: p.path, label: p.name })), + ...(prDiffSummaryProfilePath + && !profiles.some(p => p.path === prDiffSummaryProfilePath) + ? [{ value: prDiffSummaryProfilePath, label: `自定义:${prDiffSummaryProfilePath}` }] + : []), + ]} + /> + + void /** 统一 / 分栏;由父级从 workspaceState 拉并持久化。 */ mode: DiffModeEnum @@ -158,12 +158,12 @@ export function FileDiffPane({ > - {/* 一键调 deepseek 生成本文件改动说明 */} + {/* 一键生成本文件改动说明 */}