✨ feat(vscode): 「改动」面板加审查勾 + deepseek 增量生成文件改动说明

右栏 diff 头部加一组按钮:
- 审查对勾:与左树同一状态源,勾上则文件路径标题置灰
- ✨ 一键调 deepseek 生成该文件「一句话改动目的」

增量复用会话:首个文件开新 cc 会话并把 sessionId 记到
.spx/pr-file-summaries.json,后续文件 --resume 接上去,省去每次重新
理解 PR 的开销;resume 失败则丢弃旧 id 重开一次。说明也落盘(跨 push
保留),重载后仍显示。

- spawnClaude 增 resumeSessionId(--resume)
- 新增 prFileSummaryStore;handler handleGeneratePrFileSummary
  用 jsdiff 拼 unified diff 喂模型
- 依赖 diff(jsdiff)
This commit is contained in:
cruldra committed 2026-06-29 15:24:43 +08:00
1 parent fb505d59c5
commit 8c5cc90eda
11 files changed
+387 -37

No files matched your search

@@ -36,8 +36,11 @@ export function PrFilesPanel({ issue }: PrFilesPanelProps) {
loading,
confirmed,
diffByPath,
summariesByPath,
generatingPath,
getFileDiff,
setConfirmed,
generateSummary,
} = usePrFiles(issueNumber)
const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined)
@@ -150,6 +153,11 @@ export function PrFilesPanel({ issue }: PrFilesPanelProps) {
path={selectedPath}
status={selectedFile?.status}
diff={selectedPath ? diffByPath[selectedPath] : undefined}
confirmed={!!selectedPath && confirmedSet.has(selectedPath)}
onToggleConfirm={next => selectedPath && onToggleFileConfirm(selectedPath, next)}
summary={selectedPath ? summariesByPath[selectedPath] : undefined}
generating={!!selectedPath && generatingPath === selectedPath}
onGenerate={() => selectedPath && generateSummary(selectedPath, selectedFile?.previousFilename)}
/>
</div>
</div>
@@ -11,6 +11,7 @@
import type { FileDiff } from '../../hooks/usePrFiles'
import { generateDiffFile } from '@git-diff-view/file'
import { DiffModeEnum, DiffView } from '@git-diff-view/react'
import { Check, Loader2, Sparkles } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { statusBadge, statusColorVar } from './fileTree'
import '@git-diff-view/react/styles/diff-view.css'
@@ -22,6 +23,16 @@ interface FileDiffPaneProps {
status: string | undefined
/** 该文件的 diff 内容;undefined 表示请求中(加载态)。 */
diff: FileDiff | undefined
/** 当前文件是否已审查(已查看);与左树同一状态源。 */
confirmed: boolean
/** 切换当前文件审查态。 */
onToggleConfirm: (next: boolean) => void
/** 当前文件已生成的一句话改动说明;undefined 表示未生成。 */
summary: string | undefined
/** 是否正在为当前文件生成说明。 */
generating: boolean
/** 触发 deepseek 生成当前文件说明。 */
onGenerate: () => void
}
/** 读当前 VS Code 主题明暗:body 带 vscode-light / vscode-high-contrast-light 即浅色,否则深色。 */
@@ -42,7 +53,7 @@ function Centered({ children }: { children: React.ReactNode }) {
)
}
export function FileDiffPane({ path, status, diff }: FileDiffPaneProps) {
export function FileDiffPane({ path, status, diff, confirmed, onToggleConfirm, summary, generating, onGenerate }: FileDiffPaneProps) {
const [mode, setMode] = useState<DiffModeEnum>(DiffModeEnum.Split)
const [theme, setTheme] = useState<'light' | 'dark'>(() => readThemeKind())
@@ -82,35 +93,71 @@ export function FileDiffPane({ path, status, diff }: FileDiffPaneProps) {
return (
<div className="flex h-full w-full flex-col overflow-hidden">
{/* 右栏标题条:文件路径 + 状态徽标 + Split/Unified 切换 */}
<div className="flex shrink-0 items-center gap-2 border-b border-[var(--vscode-panel-border)] px-2 py-1">
{status && (
<span
className="w-4 shrink-0 text-center font-mono text-xs font-semibold"
style={{ color: statusColorVar(status) }}
>
{statusBadge(status)}
</span>
)}
<span className="min-w-0 flex-1 truncate font-mono text-xs text-[var(--vscode-foreground)]" title={path}>
{path}
</span>
<div className="flex shrink-0 overflow-hidden rounded border border-[var(--vscode-panel-border)] text-[10px]">
{([['分栏', DiffModeEnum.Split], ['统一', DiffModeEnum.Unified]] as const).map(([label, m]) => (
<button
key={label}
type="button"
onClick={() => setMode(m)}
className={`px-1.5 py-0.5 transition-colors ${
mode === m
? 'bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)]'
: 'text-[var(--vscode-descriptionForeground)] hover:text-[var(--vscode-foreground)]'
}`}
{/* 右栏标题条:文件路径 + 审查勾 + 一键生成说明 + Split/Unified 切换;下方可选一行说明 */}
<div className="flex shrink-0 flex-col gap-1 border-b border-[var(--vscode-panel-border)] px-2 py-1">
<div className="flex items-center gap-2">
{status && (
<span
className="w-4 shrink-0 text-center font-mono text-xs font-semibold"
style={{ color: statusColorVar(status) }}
>
{label}
</button>
))}
{statusBadge(status)}
</span>
)}
{/* 已审查则标题置灰 */}
<span
className="min-w-0 flex-1 truncate font-mono text-xs"
style={{ color: confirmed ? 'var(--vscode-descriptionForeground)' : 'var(--vscode-foreground)' }}
title={path}
>
{path}
</span>
{/* 审查勾:与左树同一状态源,点击联动 */}
<button
type="button"
onClick={() => onToggleConfirm(!confirmed)}
title={confirmed ? '已审查,点击取消' : '标记为已审查'}
className="shrink-0"
style={{
color: confirmed
? 'var(--vscode-gitDecoration-addedResourceForeground)'
: 'var(--vscode-descriptionForeground)',
}}
>
<Check className="size-3.5" />
</button>
{/* 一键调 deepseek 生成本文件改动说明 */}
<button
type="button"
onClick={onGenerate}
disabled={generating}
title={generating ? '生成中…' : '用 deepseek 生成本文件改动说明'}
className="shrink-0 text-[var(--vscode-descriptionForeground)] hover:text-[var(--vscode-foreground)] disabled:opacity-60"
>
{generating ? <Loader2 className="size-3.5 animate-spin" /> : <Sparkles className="size-3.5" />}
</button>
<div className="flex shrink-0 overflow-hidden rounded border border-[var(--vscode-panel-border)] text-[10px]">
{([['分栏', DiffModeEnum.Split], ['统一', DiffModeEnum.Unified]] as const).map(([label, m]) => (
<button
key={label}
type="button"
onClick={() => setMode(m)}
className={`px-1.5 py-0.5 transition-colors ${
mode === m
? 'bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)]'
: 'text-[var(--vscode-descriptionForeground)] hover:text-[var(--vscode-foreground)]'
}`}
>
{label}
</button>
))}
</div>
</div>
{(generating || summary) && (
<div className="whitespace-pre-wrap break-words text-[11px] leading-snug text-[var(--vscode-descriptionForeground)]">
{generating ? '正在生成说明…' : summary}
</div>
)}
</div>
{/* 内容区 */}