✨ 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:
@@ -108,6 +108,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^6.7.3",
|
"@antfu/eslint-config": "^6.7.3",
|
||||||
|
"@types/diff": "^8.0.0",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"@types/vscode": "^1.97.0",
|
"@types/vscode": "^1.97.0",
|
||||||
"@vscode/vsce": "^3.9.1",
|
"@vscode/vsce": "^3.9.1",
|
||||||
@@ -118,5 +119,8 @@
|
|||||||
"ovsx": "^0.10.7",
|
"ovsx": "^0.10.7",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^4.0.16"
|
"vitest": "^4.0.16"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"diff": "^9.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+21
@@ -12,10 +12,17 @@ patchedDependencies:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
diff:
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@antfu/eslint-config':
|
'@antfu/eslint-config':
|
||||||
specifier: ^6.7.3
|
specifier: ^6.7.3
|
||||||
version: 6.7.3(@vue/compiler-sfc@3.4.27)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.16(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.19.2)(yaml@2.8.2))
|
version: 6.7.3(@vue/compiler-sfc@3.4.27)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.16(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.19.2)(yaml@2.8.2))
|
||||||
|
'@types/diff':
|
||||||
|
specifier: ^8.0.0
|
||||||
|
version: 8.0.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.0.3
|
specifier: ^25.0.3
|
||||||
version: 25.0.3
|
version: 25.0.3
|
||||||
@@ -1201,6 +1208,10 @@ packages:
|
|||||||
'@types/deep-eql@4.0.2':
|
'@types/deep-eql@4.0.2':
|
||||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
|
||||||
|
'@types/diff@8.0.0':
|
||||||
|
resolution: {integrity: sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==}
|
||||||
|
deprecated: This is a stub types definition. diff provides its own type definitions, so you do not need this installed.
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
@@ -1760,6 +1771,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
|
resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
|
|
||||||
|
diff@9.0.0:
|
||||||
|
resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==}
|
||||||
|
engines: {node: '>=0.3.1'}
|
||||||
|
|
||||||
dom-serializer@2.0.0:
|
dom-serializer@2.0.0:
|
||||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||||
|
|
||||||
@@ -4579,6 +4594,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/deep-eql@4.0.2': {}
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
|
'@types/diff@8.0.0':
|
||||||
|
dependencies:
|
||||||
|
diff: 9.0.0
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
@@ -5212,6 +5231,8 @@ snapshots:
|
|||||||
|
|
||||||
diff@8.0.4: {}
|
diff@8.0.4: {}
|
||||||
|
|
||||||
|
diff@9.0.0: {}
|
||||||
|
|
||||||
dom-serializer@2.0.0:
|
dom-serializer@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
domelementtype: 2.3.0
|
domelementtype: 2.3.0
|
||||||
|
|||||||
@@ -234,14 +234,16 @@ export async function spawnClaude(opts: {
|
|||||||
images?: ClaudeImage[]
|
images?: ClaudeImage[]
|
||||||
profilePath?: string
|
profilePath?: string
|
||||||
bare?: boolean
|
bare?: boolean
|
||||||
|
/** 续会话:传入则 `--resume <id>`,让本次 `-p` 接在该会话之后(复用上下文,省 token)。 */
|
||||||
|
resumeSessionId?: string
|
||||||
}): Promise<ClaudeResult> {
|
}): Promise<ClaudeResult> {
|
||||||
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
||||||
const hasImages = !!opts.images && opts.images.length > 0
|
const hasImages = !!opts.images && opts.images.length > 0
|
||||||
|
|
||||||
if (!hasImages) {
|
if (!hasImages) {
|
||||||
return spawnClaudeText(opts.prompt, opts.cwd, timeoutMs, opts.profilePath, opts.bare)
|
return spawnClaudeText(opts.prompt, opts.cwd, timeoutMs, opts.profilePath, opts.bare, opts.resumeSessionId)
|
||||||
}
|
}
|
||||||
return spawnClaudeStreamed(opts.prompt, opts.cwd, timeoutMs, opts.images!, opts.profilePath, opts.bare)
|
return spawnClaudeStreamed(opts.prompt, opts.cwd, timeoutMs, opts.images!, opts.profilePath, opts.bare, opts.resumeSessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnClaudeText(
|
function spawnClaudeText(
|
||||||
@@ -250,11 +252,13 @@ function spawnClaudeText(
|
|||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
profilePath?: string,
|
profilePath?: string,
|
||||||
bare?: boolean,
|
bare?: boolean,
|
||||||
|
resumeSessionId?: string,
|
||||||
): Promise<ClaudeResult> {
|
): Promise<ClaudeResult> {
|
||||||
const args = [
|
const args = [
|
||||||
...(bare ? ['--bare'] : []),
|
...(bare ? ['--bare'] : []),
|
||||||
'--dangerously-skip-permissions',
|
'--dangerously-skip-permissions',
|
||||||
...(profilePath ? ['--settings', profilePath] : []),
|
...(profilePath ? ['--settings', profilePath] : []),
|
||||||
|
...(resumeSessionId ? ['--resume', resumeSessionId] : []),
|
||||||
'-p',
|
'-p',
|
||||||
prompt,
|
prompt,
|
||||||
'--output-format',
|
'--output-format',
|
||||||
@@ -361,6 +365,7 @@ function spawnClaudeStreamed(
|
|||||||
images: ClaudeImage[],
|
images: ClaudeImage[],
|
||||||
profilePath?: string,
|
profilePath?: string,
|
||||||
bare?: boolean,
|
bare?: boolean,
|
||||||
|
resumeSessionId?: string,
|
||||||
): Promise<ClaudeResult> {
|
): Promise<ClaudeResult> {
|
||||||
// Local Claude Code v2.1.143 enforces that --input-format=stream-json must
|
// Local Claude Code v2.1.143 enforces that --input-format=stream-json must
|
||||||
// be paired with --output-format=stream-json (the friendlier `--output-format
|
// be paired with --output-format=stream-json (the friendlier `--output-format
|
||||||
@@ -370,6 +375,7 @@ function spawnClaudeStreamed(
|
|||||||
...(bare ? ['--bare'] : []),
|
...(bare ? ['--bare'] : []),
|
||||||
'--dangerously-skip-permissions',
|
'--dangerously-skip-permissions',
|
||||||
...(profilePath ? ['--settings', profilePath] : []),
|
...(profilePath ? ['--settings', profilePath] : []),
|
||||||
|
...(resumeSessionId ? ['--resume', resumeSessionId] : []),
|
||||||
'-p',
|
'-p',
|
||||||
'--input-format',
|
'--input-format',
|
||||||
'stream-json',
|
'stream-json',
|
||||||
|
|||||||
@@ -558,6 +558,14 @@ export class KanbanWebviewPanel {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (msg.type === 'pr-file-summary/generate') {
|
||||||
|
void prFiles.handleGeneratePrFileSummary(this, {
|
||||||
|
issueNumber: msg.issueNumber,
|
||||||
|
path: msg.path,
|
||||||
|
previousPath: msg.previousPath,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
if (msg.type === 'pr-review/set') {
|
if (msg.type === 'pr-review/set') {
|
||||||
void prFiles.handleSetPrReviewConfirmed(this, msg)
|
void prFiles.handleSetPrReviewConfirmed(this, msg)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,12 +8,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||||
|
import { createPatch } from 'diff'
|
||||||
import { workspace } from 'vscode'
|
import { workspace } from 'vscode'
|
||||||
import { getToken } from '../../auth/secrets'
|
import { getToken } from '../../auth/secrets'
|
||||||
|
import { listClaudeProfiles } from '../../cc/profiles'
|
||||||
|
import { spawnClaude } from '../../cc/spawnClaude'
|
||||||
import { detectRepo } from '../../git/remote'
|
import { detectRepo } from '../../git/remote'
|
||||||
import { getPullRequest, getRawFile, listPullRequestFiles } from '../../gitea/api'
|
import { getPullRequest, getRawFile, listPullRequestFiles } from '../../gitea/api'
|
||||||
import { readStateJsonComment } from '../../gitea/stateJson'
|
import { readStateJsonComment } from '../../gitea/stateJson'
|
||||||
|
import { readFileSummaries, readSummarySession, writeFileSummary, writeSummarySession } from '../../sessions/prFileSummaryStore'
|
||||||
import { readConfirmed, writeConfirmed } from '../../sessions/prReviewStore'
|
import { readConfirmed, writeConfirmed } from '../../sessions/prReviewStore'
|
||||||
|
import { makeNonce } from '../KanbanPanel'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析当前工作区的 Gitea 仓库与 token。三者任一缺失返回 undefined,
|
* 解析当前工作区的 Gitea 仓库与 token。三者任一缺失返回 undefined,
|
||||||
@@ -106,13 +111,13 @@ export async function handleGetPrFiles(panel: KanbanWebviewPanel, issueNumber: n
|
|||||||
try {
|
try {
|
||||||
const ctx = await resolveRepoContext(panel)
|
const ctx = await resolveRepoContext(panel)
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
panel.postMessage({ type: 'pr-files/show', issueNumber, headSha: '', mergeBase: '', files: [], confirmed: [] })
|
panel.postMessage({ type: 'pr-files/show', issueNumber, headSha: '', mergeBase: '', files: [], confirmed: [], summaries: {} })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = await resolvePrNumber(ctx, issueNumber)
|
const index = await resolvePrNumber(ctx, issueNumber)
|
||||||
if (index === undefined) {
|
if (index === undefined) {
|
||||||
panel.postMessage({ type: 'pr-files/show', issueNumber, headSha: '', mergeBase: '', files: [], confirmed: [] })
|
panel.postMessage({ type: 'pr-files/show', issueNumber, headSha: '', mergeBase: '', files: [], confirmed: [], summaries: {} })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +132,12 @@ export async function handleGetPrFiles(panel: KanbanWebviewPanel, issueNumber: n
|
|||||||
index,
|
index,
|
||||||
})
|
})
|
||||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||||
const confirmed = workspaceRoot
|
const [confirmed, summaries] = workspaceRoot
|
||||||
? await readConfirmed(workspaceRoot, issueNumber, pr.headSha)
|
? await Promise.all([
|
||||||
: []
|
readConfirmed(workspaceRoot, issueNumber, pr.headSha),
|
||||||
|
readFileSummaries(workspaceRoot, issueNumber),
|
||||||
|
])
|
||||||
|
: [[], {}]
|
||||||
panel.postMessage({
|
panel.postMessage({
|
||||||
type: 'pr-files/show',
|
type: 'pr-files/show',
|
||||||
issueNumber,
|
issueNumber,
|
||||||
@@ -137,6 +145,7 @@ export async function handleGetPrFiles(panel: KanbanWebviewPanel, issueNumber: n
|
|||||||
mergeBase,
|
mergeBase,
|
||||||
files,
|
files,
|
||||||
confirmed,
|
confirmed,
|
||||||
|
summaries,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@@ -147,6 +156,7 @@ export async function handleGetPrFiles(panel: KanbanWebviewPanel, issueNumber: n
|
|||||||
mergeBase: '',
|
mergeBase: '',
|
||||||
files: [],
|
files: [],
|
||||||
confirmed: [],
|
confirmed: [],
|
||||||
|
summaries: {},
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -220,3 +230,101 @@ export async function handleSetPrReviewConfirmed(
|
|||||||
console.warn('handleSetPrReviewConfirmed 写盘失败', err)
|
console.warn('handleSetPrReviewConfirmed 写盘失败', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 生成说明的 prompt:首次带审查上下文设定,续会话只发新文件,让 deepseek 复用已有 PR 理解、省 token。 */
|
||||||
|
function buildSummaryPrompt(prNumber: number, filePath: string, patch: string, isFirst: boolean): string {
|
||||||
|
if (isFirst) {
|
||||||
|
return `我在逐个审查 PR #${prNumber} 的改动。请用一句简短中文说明下面这个文件本次改动的目的,只输出这一句,不要解释、不要代码块。\n\n文件: ${filePath}\n\n\`\`\`diff\n${patch}\n\`\`\``
|
||||||
|
}
|
||||||
|
return `下一个文件,同样用一句简短中文说明其改动目的,只输出这一句:\n文件: ${filePath}\n\`\`\`diff\n${patch}\n\`\`\``
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用 deepseek 给单个文件生成「一句话改动目的」。增量复用同一会话:首个文件开新会话并把
|
||||||
|
* sessionId 记到 `.spx/pr-file-summaries.json`,后续文件 `--resume` 接上去,省去每次重新理解
|
||||||
|
* PR 的开销。说明也落盘,重载后仍显示。
|
||||||
|
*
|
||||||
|
* diff 取 merge_base → head 两版原文用 jsdiff 拼 unified diff 喂给模型(比塞两份全文省 token)。
|
||||||
|
* spawnClaude 走 spawn 数组参数、不经 shell,故 patch 里的引号/换行无需转义。
|
||||||
|
*/
|
||||||
|
export async function handleGeneratePrFileSummary(
|
||||||
|
panel: KanbanWebviewPanel,
|
||||||
|
args: { issueNumber: number, path: string, previousPath?: string },
|
||||||
|
): Promise<void> {
|
||||||
|
const { issueNumber, path: filePath } = args
|
||||||
|
try {
|
||||||
|
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||||
|
const ctx = await resolveRepoContext(panel)
|
||||||
|
if (!ctx || !workspaceRoot) {
|
||||||
|
panel.postMessage({ type: 'pr-file-summary/show', issueNumber, path: filePath, error: '当前工作区没有 Gitea 仓库或未配置 token' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// deepseek profile 与「一键提交」同一来源;缺失则提示用户创建并清掉前端 loading。
|
||||||
|
const profiles = await listClaudeProfiles()
|
||||||
|
const deepseek = profiles.find(p => p.name === 'deepseek-v4-pro')
|
||||||
|
if (!deepseek) {
|
||||||
|
panel.postMessage({
|
||||||
|
type: 'toast/show',
|
||||||
|
id: makeNonce(),
|
||||||
|
level: 'error',
|
||||||
|
message: '未找到 deepseek profile,请在 /home/cruldra/Sources/cruldra-profile/claude-config/profiles/ 下创建 deepseek.json',
|
||||||
|
dismissOnTimer: 8000,
|
||||||
|
})
|
||||||
|
panel.postMessage({ type: 'pr-file-summary/show', issueNumber, path: filePath, error: '未找到 deepseek profile' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = await resolvePrNumber(ctx, issueNumber)
|
||||||
|
if (index === undefined) {
|
||||||
|
panel.postMessage({ type: 'pr-file-summary/show', issueNumber, path: filePath, error: '该工单还没有 PR' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pr = await getPullRequest({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, index })
|
||||||
|
const mergeBase = pr.mergeBase || pr.baseSha
|
||||||
|
const oldPath = args.previousPath ?? filePath
|
||||||
|
const [oldContent, newContent] = await Promise.all([
|
||||||
|
getRawFile({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, filepath: oldPath, ref: mergeBase }),
|
||||||
|
getRawFile({ host: ctx.host, owner: ctx.owner, repo: ctx.repo, token: ctx.token, filepath: filePath, ref: pr.headSha }),
|
||||||
|
])
|
||||||
|
const patch = createPatch(filePath, oldContent, newContent, '', '')
|
||||||
|
|
||||||
|
const prevSession = await readSummarySession(workspaceRoot, issueNumber)
|
||||||
|
let result
|
||||||
|
try {
|
||||||
|
result = await spawnClaude({
|
||||||
|
prompt: buildSummaryPrompt(index, filePath, patch, !prevSession),
|
||||||
|
cwd: workspaceRoot,
|
||||||
|
profilePath: deepseek.path,
|
||||||
|
resumeSessionId: prevSession,
|
||||||
|
timeoutMs: 120_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
// resume 失败(会话过期/被清理)→ 丢弃旧 id,按首次重开一个会话再试一次。
|
||||||
|
if (!prevSession)
|
||||||
|
throw err
|
||||||
|
await writeSummarySession(workspaceRoot, issueNumber, undefined)
|
||||||
|
result = await spawnClaude({
|
||||||
|
prompt: buildSummaryPrompt(index, filePath, patch, true),
|
||||||
|
cwd: workspaceRoot,
|
||||||
|
profilePath: deepseek.path,
|
||||||
|
timeoutMs: 120_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = result.resultText.trim().replace(/\s+/g, ' ')
|
||||||
|
await writeFileSummary(workspaceRoot, issueNumber, filePath, summary)
|
||||||
|
await writeSummarySession(workspaceRoot, issueNumber, result.sessionId)
|
||||||
|
panel.postMessage({ type: 'pr-file-summary/show', issueNumber, path: filePath, summary })
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
panel.postMessage({
|
||||||
|
type: 'pr-file-summary/show',
|
||||||
|
issueNumber,
|
||||||
|
path: filePath,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,8 +100,9 @@ export type ExtensionToWebview
|
|||||||
| { type: 'issue/remove', issueNumber: number }
|
| { type: 'issue/remove', issueNumber: number }
|
||||||
| { type: 'profiles/show', data: ProfilesData }
|
| { type: 'profiles/show', data: ProfilesData }
|
||||||
| { type: 'managed-sessions/show', data: ManagedSessionsShowData }
|
| { type: 'managed-sessions/show', data: ManagedSessionsShowData }
|
||||||
| { type: 'pr-files/show', issueNumber: number, headSha: string, mergeBase: string, files: PrFile[], confirmed: string[], error?: string }
|
| { type: 'pr-files/show', issueNumber: number, headSha: string, mergeBase: string, files: PrFile[], confirmed: string[], summaries: Record<string, string>, error?: string }
|
||||||
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
||||||
|
| { type: 'pr-file-summary/show', issueNumber: number, path: string, summary?: string, error?: string }
|
||||||
|
|
||||||
export type WebviewToExtension
|
export type WebviewToExtension
|
||||||
= | { type: 'issues/refresh' }
|
= | { type: 'issues/refresh' }
|
||||||
@@ -174,4 +175,5 @@ export type WebviewToExtension
|
|||||||
| { type: 'managed-sessions/close-tab', sessionId: string }
|
| { type: 'managed-sessions/close-tab', sessionId: string }
|
||||||
| { type: 'pr-files/get', issueNumber: number }
|
| { type: 'pr-files/get', issueNumber: number }
|
||||||
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
||||||
|
| { type: 'pr-file-summary/generate', issueNumber: number, path: string, previousPath?: string }
|
||||||
| { type: 'pr-review/set', issueNumber: number, headSha: string, confirmed: string[] }
|
| { type: 'pr-review/set', issueNumber: number, headSha: string, confirmed: string[] }
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Workspace 级「PR 文件改动说明 + 续会话 id」持久化。
|
||||||
|
*
|
||||||
|
* 落盘到 `<workspace>/.spx/pr-file-summaries.json`,形状 `Record<string, string>`。
|
||||||
|
* 用前缀给两类值分命名空间,避免「文件路径」与「会话 key」相撞:
|
||||||
|
* - `sum:${issueNumber}:${path}` → 该文件一句话改动说明(跨 push 保留,不随 head sha 失效)。
|
||||||
|
* - `ses:${issueNumber}` → 该工单最近一次 deepseek 生成会话的 sessionId,供下次 `--resume`。
|
||||||
|
*
|
||||||
|
* 文件不存在或解析失败时按空处理。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { promises as fsp } from 'node:fs'
|
||||||
|
import * as path from 'node:path'
|
||||||
|
|
||||||
|
type SummaryMap = Record<string, string>
|
||||||
|
|
||||||
|
function summaryFile(workspaceRoot: string): string {
|
||||||
|
return path.join(workspaceRoot, '.spx', 'pr-file-summaries.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
function summaryKey(issueNumber: number, filePath: string): string {
|
||||||
|
return `sum:${issueNumber}:${filePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionKey(issueNumber: number): string {
|
||||||
|
return `ses:${issueNumber}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读出整张映射;文件不存在或解析失败返回空对象。 */
|
||||||
|
async function readMap(workspaceRoot: string): Promise<SummaryMap> {
|
||||||
|
let raw: string
|
||||||
|
try {
|
||||||
|
raw = await fsp.readFile(summaryFile(workspaceRoot), 'utf8')
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as unknown
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
||||||
|
return {}
|
||||||
|
const out: SummaryMap = {}
|
||||||
|
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
||||||
|
if (typeof v === 'string')
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeMap(workspaceRoot: string, map: SummaryMap): Promise<void> {
|
||||||
|
const dir = path.join(workspaceRoot, '.spx')
|
||||||
|
await fsp.mkdir(dir, { recursive: true })
|
||||||
|
await fsp.writeFile(summaryFile(workspaceRoot), `${JSON.stringify(map, null, 2)}\n`, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读某工单全部已生成的文件说明:`{ [path]: summary }`。无则空对象。 */
|
||||||
|
export async function readFileSummaries(
|
||||||
|
workspaceRoot: string,
|
||||||
|
issueNumber: number,
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
const map = await readMap(workspaceRoot)
|
||||||
|
const prefix = `sum:${issueNumber}:`
|
||||||
|
const out: Record<string, string> = {}
|
||||||
|
for (const [k, v] of Object.entries(map)) {
|
||||||
|
if (k.startsWith(prefix))
|
||||||
|
out[k.slice(prefix.length)] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 写某文件的说明。空串则删除该 key。 */
|
||||||
|
export async function writeFileSummary(
|
||||||
|
workspaceRoot: string,
|
||||||
|
issueNumber: number,
|
||||||
|
filePath: string,
|
||||||
|
summary: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const map = await readMap(workspaceRoot)
|
||||||
|
const key = summaryKey(issueNumber, filePath)
|
||||||
|
if (summary.length > 0)
|
||||||
|
map[key] = summary
|
||||||
|
else
|
||||||
|
delete map[key]
|
||||||
|
await writeMap(workspaceRoot, map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读某工单最近一次生成会话的 sessionId;无则 undefined。 */
|
||||||
|
export async function readSummarySession(
|
||||||
|
workspaceRoot: string,
|
||||||
|
issueNumber: number,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const map = await readMap(workspaceRoot)
|
||||||
|
return map[sessionKey(issueNumber)] || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 写某工单续会话 sessionId。传 undefined / 空串则删除(下次从头开会话)。 */
|
||||||
|
export async function writeSummarySession(
|
||||||
|
workspaceRoot: string,
|
||||||
|
issueNumber: number,
|
||||||
|
sessionId: string | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
const map = await readMap(workspaceRoot)
|
||||||
|
const key = sessionKey(issueNumber)
|
||||||
|
if (sessionId && sessionId.length > 0)
|
||||||
|
map[key] = sessionId
|
||||||
|
else
|
||||||
|
delete map[key]
|
||||||
|
await writeMap(workspaceRoot, map)
|
||||||
|
}
|
||||||
@@ -36,8 +36,11 @@ export function PrFilesPanel({ issue }: PrFilesPanelProps) {
|
|||||||
loading,
|
loading,
|
||||||
confirmed,
|
confirmed,
|
||||||
diffByPath,
|
diffByPath,
|
||||||
|
summariesByPath,
|
||||||
|
generatingPath,
|
||||||
getFileDiff,
|
getFileDiff,
|
||||||
setConfirmed,
|
setConfirmed,
|
||||||
|
generateSummary,
|
||||||
} = usePrFiles(issueNumber)
|
} = usePrFiles(issueNumber)
|
||||||
|
|
||||||
const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined)
|
const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined)
|
||||||
@@ -150,6 +153,11 @@ export function PrFilesPanel({ issue }: PrFilesPanelProps) {
|
|||||||
path={selectedPath}
|
path={selectedPath}
|
||||||
status={selectedFile?.status}
|
status={selectedFile?.status}
|
||||||
diff={selectedPath ? diffByPath[selectedPath] : undefined}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import type { FileDiff } from '../../hooks/usePrFiles'
|
import type { FileDiff } from '../../hooks/usePrFiles'
|
||||||
import { generateDiffFile } from '@git-diff-view/file'
|
import { generateDiffFile } from '@git-diff-view/file'
|
||||||
import { DiffModeEnum, DiffView } from '@git-diff-view/react'
|
import { DiffModeEnum, DiffView } from '@git-diff-view/react'
|
||||||
|
import { Check, Loader2, Sparkles } from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { statusBadge, statusColorVar } from './fileTree'
|
import { statusBadge, statusColorVar } from './fileTree'
|
||||||
import '@git-diff-view/react/styles/diff-view.css'
|
import '@git-diff-view/react/styles/diff-view.css'
|
||||||
@@ -22,6 +23,16 @@ interface FileDiffPaneProps {
|
|||||||
status: string | undefined
|
status: string | undefined
|
||||||
/** 该文件的 diff 内容;undefined 表示请求中(加载态)。 */
|
/** 该文件的 diff 内容;undefined 表示请求中(加载态)。 */
|
||||||
diff: FileDiff | 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 即浅色,否则深色。 */
|
/** 读当前 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 [mode, setMode] = useState<DiffModeEnum>(DiffModeEnum.Split)
|
||||||
const [theme, setTheme] = useState<'light' | 'dark'>(() => readThemeKind())
|
const [theme, setTheme] = useState<'light' | 'dark'>(() => readThemeKind())
|
||||||
|
|
||||||
@@ -82,8 +93,9 @@ export function FileDiffPane({ path, status, diff }: FileDiffPaneProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
{/* 右栏标题条:文件路径 + 状态徽标 + Split/Unified 切换 */}
|
{/* 右栏标题条:文件路径 + 审查勾 + 一键生成说明 + Split/Unified 切换;下方可选一行说明 */}
|
||||||
<div className="flex shrink-0 items-center gap-2 border-b border-[var(--vscode-panel-border)] px-2 py-1">
|
<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 && (
|
{status && (
|
||||||
<span
|
<span
|
||||||
className="w-4 shrink-0 text-center font-mono text-xs font-semibold"
|
className="w-4 shrink-0 text-center font-mono text-xs font-semibold"
|
||||||
@@ -92,9 +104,38 @@ export function FileDiffPane({ path, status, diff }: FileDiffPaneProps) {
|
|||||||
{statusBadge(status)}
|
{statusBadge(status)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="min-w-0 flex-1 truncate font-mono text-xs text-[var(--vscode-foreground)]" title={path}>
|
{/* 已审查则标题置灰 */}
|
||||||
|
<span
|
||||||
|
className="min-w-0 flex-1 truncate font-mono text-xs"
|
||||||
|
style={{ color: confirmed ? 'var(--vscode-descriptionForeground)' : 'var(--vscode-foreground)' }}
|
||||||
|
title={path}
|
||||||
|
>
|
||||||
{path}
|
{path}
|
||||||
</span>
|
</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]">
|
<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]) => (
|
{([['分栏', DiffModeEnum.Split], ['统一', DiffModeEnum.Unified]] as const).map(([label, m]) => (
|
||||||
<button
|
<button
|
||||||
@@ -112,6 +153,12 @@ export function FileDiffPane({ path, status, diff }: FileDiffPaneProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{(generating || summary) && (
|
||||||
|
<div className="whitespace-pre-wrap break-words text-[11px] leading-snug text-[var(--vscode-descriptionForeground)]">
|
||||||
|
{generating ? '正在生成说明…' : summary}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 内容区 */}
|
{/* 内容区 */}
|
||||||
<div className="min-h-0 flex-1 overflow-auto bg-[var(--vscode-editor-background)] text-xs">
|
<div className="min-h-0 flex-1 overflow-auto bg-[var(--vscode-editor-background)] text-xs">
|
||||||
|
|||||||
@@ -31,8 +31,13 @@ export interface UsePrFilesResult {
|
|||||||
confirmed: string[]
|
confirmed: string[]
|
||||||
/** key ∈ diffByPath 即「已请求/已到」;值为内容。仅 key 在但值 undefined 不会出现。 */
|
/** key ∈ diffByPath 即「已请求/已到」;值为内容。仅 key 在但值 undefined 不会出现。 */
|
||||||
diffByPath: Record<string, FileDiff>
|
diffByPath: Record<string, FileDiff>
|
||||||
|
/** 路径 → 已生成的一句话改动说明(deepseek 增量生成、跨 push 保留)。 */
|
||||||
|
summariesByPath: Record<string, string>
|
||||||
|
/** 正在生成说明的文件路径;null 表示无生成中。 */
|
||||||
|
generatingPath: string | null
|
||||||
getFileDiff: (path: string, previousPath?: string) => void
|
getFileDiff: (path: string, previousPath?: string) => void
|
||||||
setConfirmed: (paths: string[]) => void
|
setConfirmed: (paths: string[]) => void
|
||||||
|
generateSummary: (path: string, previousPath?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePrFiles(issueNumber: number | undefined): UsePrFilesResult {
|
export function usePrFiles(issueNumber: number | undefined): UsePrFilesResult {
|
||||||
@@ -42,6 +47,8 @@ export function usePrFiles(issueNumber: number | undefined): UsePrFilesResult {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [confirmed, setConfirmedState] = useState<string[]>([])
|
const [confirmed, setConfirmedState] = useState<string[]>([])
|
||||||
const [diffByPath, setDiffByPath] = useState<Record<string, FileDiff>>({})
|
const [diffByPath, setDiffByPath] = useState<Record<string, FileDiff>>({})
|
||||||
|
const [summariesByPath, setSummariesByPath] = useState<Record<string, string>>({})
|
||||||
|
const [generatingPath, setGeneratingPath] = useState<string | null>(null)
|
||||||
|
|
||||||
// issueNumber / headSha 装进 ref,让订阅闭包与回调始终读到当前值。
|
// issueNumber / headSha 装进 ref,让订阅闭包与回调始终读到当前值。
|
||||||
const issueRef = useRef<number | undefined>(issueNumber)
|
const issueRef = useRef<number | undefined>(issueNumber)
|
||||||
@@ -60,10 +67,20 @@ export function usePrFiles(issueNumber: number | undefined): UsePrFilesResult {
|
|||||||
setFiles(msg.files)
|
setFiles(msg.files)
|
||||||
setHeadSha(msg.headSha)
|
setHeadSha(msg.headSha)
|
||||||
setConfirmedState(msg.confirmed ?? [])
|
setConfirmedState(msg.confirmed ?? [])
|
||||||
|
setSummariesByPath(msg.summaries ?? {})
|
||||||
setFilesError(msg.error)
|
setFilesError(msg.error)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (msg.type === 'pr-file-summary/show') {
|
||||||
|
if (msg.issueNumber !== issueRef.current)
|
||||||
|
return
|
||||||
|
// 生成结束(成功带 summary / 失败带 error)都解除该文件的 loading;失败的 error 已由扩展侧 toast。
|
||||||
|
setGeneratingPath(p => (p === msg.path ? null : p))
|
||||||
|
if (msg.summary)
|
||||||
|
setSummariesByPath(prev => ({ ...prev, [msg.path]: msg.summary! }))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (msg.type === 'pr-file-diff/show') {
|
if (msg.type === 'pr-file-diff/show') {
|
||||||
if (msg.issueNumber !== issueRef.current)
|
if (msg.issueNumber !== issueRef.current)
|
||||||
return
|
return
|
||||||
@@ -89,6 +106,8 @@ export function usePrFiles(issueNumber: number | undefined): UsePrFilesResult {
|
|||||||
setFilesError(undefined)
|
setFilesError(undefined)
|
||||||
setConfirmedState([])
|
setConfirmedState([])
|
||||||
setDiffByPath({})
|
setDiffByPath({})
|
||||||
|
setSummariesByPath({})
|
||||||
|
setGeneratingPath(null)
|
||||||
requestedRef.current = new Set()
|
requestedRef.current = new Set()
|
||||||
if (issueNumber === undefined) {
|
if (issueNumber === undefined) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -117,6 +136,15 @@ export function usePrFiles(issueNumber: number | undefined): UsePrFilesResult {
|
|||||||
postMessage({ type: 'pr-review/set', issueNumber: num, headSha: headShaRef.current, confirmed: paths })
|
postMessage({ type: 'pr-review/set', issueNumber: num, headSha: headShaRef.current, confirmed: paths })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 触发 deepseek 增量生成该文件的改动说明;置 loading,结果经 pr-file-summary/show 回来。
|
||||||
|
const generateSummary = useCallback((path: string, previousPath?: string): void => {
|
||||||
|
const num = issueRef.current
|
||||||
|
if (num === undefined)
|
||||||
|
return
|
||||||
|
setGeneratingPath(path)
|
||||||
|
postMessage({ type: 'pr-file-summary/generate', issueNumber: num, path, previousPath })
|
||||||
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
files,
|
files,
|
||||||
headSha,
|
headSha,
|
||||||
@@ -124,7 +152,10 @@ export function usePrFiles(issueNumber: number | undefined): UsePrFilesResult {
|
|||||||
loading,
|
loading,
|
||||||
confirmed,
|
confirmed,
|
||||||
diffByPath,
|
diffByPath,
|
||||||
|
summariesByPath,
|
||||||
|
generatingPath,
|
||||||
getFileDiff,
|
getFileDiff,
|
||||||
setConfirmed,
|
setConfirmed,
|
||||||
|
generateSummary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,8 +115,9 @@ export type ExtensionToWebview
|
|||||||
| { type: 'issue/remove', issueNumber: number }
|
| { type: 'issue/remove', issueNumber: number }
|
||||||
| { type: 'profiles/show', data: ProfilesData }
|
| { type: 'profiles/show', data: ProfilesData }
|
||||||
| { type: 'managed-sessions/show', data: ManagedSessionsData }
|
| { type: 'managed-sessions/show', data: ManagedSessionsData }
|
||||||
| { type: 'pr-files/show', issueNumber: number, headSha: string, mergeBase: string, files: PrFile[], confirmed: string[], error?: string }
|
| { type: 'pr-files/show', issueNumber: number, headSha: string, mergeBase: string, files: PrFile[], confirmed: string[], summaries: Record<string, string>, error?: string }
|
||||||
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
||||||
|
| { type: 'pr-file-summary/show', issueNumber: number, path: string, summary?: string, error?: string }
|
||||||
|
|
||||||
export type WebviewToExtension
|
export type WebviewToExtension
|
||||||
= | { type: 'issues/refresh' }
|
= | { type: 'issues/refresh' }
|
||||||
@@ -189,4 +190,5 @@ export type WebviewToExtension
|
|||||||
| { type: 'managed-sessions/close-tab', sessionId: string }
|
| { type: 'managed-sessions/close-tab', sessionId: string }
|
||||||
| { type: 'pr-files/get', issueNumber: number }
|
| { type: 'pr-files/get', issueNumber: number }
|
||||||
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
||||||
|
| { type: 'pr-file-summary/generate', issueNumber: number, path: string, previousPath?: string }
|
||||||
| { type: 'pr-review/set', issueNumber: number, headSha: string, confirmed: string[] }
|
| { type: 'pr-review/set', issueNumber: number, headSha: string, confirmed: string[] }
|
||||||
|
|||||||
Reference in New Issue
Block a user