⚡ perf(vscode): 开会话去掉多余远程往返,spinner 跟随真实进度

开会话原先要先全量拉工单(含每张工单的评论、依赖、PR)做前置锁检查,
再各读一次 state JSON 给 pre-create 钩子和终端图标,几秒后才 createTerminal。

- 锁检查改用主进程已算好的工单快照,快照里没有该工单才回落全量拉取
- state JSON 一次读两处用,且提前发起与本地准备重叠
- 会话行 spinner 由扩展的 session/tab-settled 消息驱动,不再是固定 1.5s 假动画,
  消息丢失时 60s 超时兜底

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
This commit is contained in:
cruldra committed 2026-09-13 10:14:48 +08:00
1 parent b182e0ac5b
commit 70b826123c
12 files changed
+203 -56

No files matched your search

+24 -2
View File
@@ -696,7 +696,10 @@ export class KanbanWebviewPanel {
* Persistence failures are logged and swallowed.
*/
// internal: handler 模块访问
async resolveIssueIcon(issueNumber: number): Promise<{ themeColor: ThemeColor, iconUri: Uri }> {
async resolveIssueIcon(
issueNumber: number,
presetState?: Record<string, unknown> | null,
): Promise<{ themeColor: ThemeColor, iconUri: Uri }> {
// Helper: produce both representations from a palette id.
const pack = (id: string) => ({
themeColor: new ThemeColor(id),
@@ -707,6 +710,16 @@ export class KanbanWebviewPanel {
// pick so the icon is still stable across sessions.
const fallbackId = PALETTE[((issueNumber % PALETTE.length) + PALETTE.length) % PALETTE.length]
let stored: string | undefined
// 调用方已经读过 state(开会话热路径就一次)时直接复用,省一次远程往返;
// presetState 为 null 表示那次读取失败,按 tracker 不可达处理。
if (presetState !== undefined) {
if (presetState === null)
return pack(fallbackId)
if (typeof presetState.color === 'string' && presetState.color.length > 0)
stored = presetState.color
}
else {
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot)
return pack(fallbackId)
@@ -717,7 +730,6 @@ export class KanbanWebviewPanel {
if (!token)
return pack(fallbackId)
let stored: string | undefined
try {
const state = await this.readIssueState(issueNumber)
if (typeof state.color === 'string' && state.color.length > 0)
@@ -726,6 +738,7 @@ export class KanbanWebviewPanel {
catch (err) {
console.warn('[superpowers] failed to read state JSON for color:', err)
}
}
const { id, isNew } = resolveIssueColor(stored)
if (isNew) {
@@ -871,6 +884,15 @@ export class KanbanWebviewPanel {
* `{ locked: false }` so a transient Gitea hiccup can't lock the user out
* of starting a session.
*/
/**
* 主进程上一次算好的工单快照(issues/update 的 payload)。锁检查等只读
* 判断直接用它,省掉一次全量 loadIssues 往返;面板还没加载过时为 undefined。
*/
// internal: handler 模块访问
snapshotIssues(): Issue[] | undefined {
return this.lastIssuesUpdate?.issues
}
// internal: handler 模块访问
async resolveLockedReason(
issueNumber: number,
+12 -22
View File
@@ -1,4 +1,5 @@
import type { IssueColumn } from '../../gitea/types'
import type { LockedReason } from '../issueLock'
import type { KanbanWebviewPanel } from '../KanbanPanel'
import { Buffer } from 'node:buffer'
import { randomUUID } from 'node:crypto'
@@ -35,6 +36,7 @@ import { logger } from '../../logging/logger'
import { getEffectivePrDiffSummaryProfilePath, getSettings } from '../../settings/store'
import { webhookCoordinator } from '../../webhook/coordinator'
import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor'
import { evaluateLockedReason } from '../issueLock'
import { makeNonce } from '../KanbanPanel'
import { cleanupFeatureBranch } from './branchCleanup'
import * as sessions from './sessions'
@@ -1572,12 +1574,10 @@ export async function syncCloseGiteaIssue(panel: KanbanWebviewPanel, opts: {
}
/**
* Server-side lock check for prerequisite gating. Fetches a fresh issues
* snapshot (we don't trust webview state) and reports whether
* `issueNumber` is blocked by an unfinished prerequisite. Mirrors the
* webview-side `isIssueLocked` predicate: locked iff the issue has a
* prerequisite that exists in the current snapshot and is not yet in the
* `done` column.
* Server-side lock check for prerequisite gating(不信 webview 传来的状态)。
* 优先用主进程自己算好的工单快照——开会话是热路径,全量 loadIssues 要拉工单
* 列表、每张工单的评论、依赖和 PR,几秒起步;快照没有该工单(面板还没加载
* 完)时才退回一次全量拉取。
*
* Fail-open: any error fetching issues is logged and we return
* `{ locked: false }` so a transient Gitea hiccup can't lock the user out
@@ -1586,8 +1586,12 @@ export async function syncCloseGiteaIssue(panel: KanbanWebviewPanel, opts: {
export async function resolveLockedReason(
panel: KanbanWebviewPanel,
issueNumber: number,
): Promise<{ locked: boolean, prerequisiteNumber?: number, prerequisiteColumn?: string }> {
): Promise<LockedReason> {
try {
const snapshot = panel.snapshotIssues()
if (snapshot?.some(i => i.number === issueNumber))
return evaluateLockedReason(snapshot, issueNumber)
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
if (!workspaceRoot)
return { locked: false }
@@ -1604,21 +1608,7 @@ export async function resolveLockedReason(
repo: remote.repo,
workspaceRoot,
})
const issue = issues.find(i => i.number === issueNumber)
if (!issue)
return { locked: false }
if (issue.prerequisite === undefined)
return { locked: false }
const prereq = issues.find(i => i.number === issue.prerequisite)
if (!prereq)
return { locked: false }
if (prereq.column === 'done')
return { locked: false }
return {
locked: true,
prerequisiteNumber: prereq.number,
prerequisiteColumn: prereq.column,
}
return evaluateLockedReason(issues, issueNumber)
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
+23 -23
View File
@@ -73,6 +73,20 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
existing.show(false)
return
}
// state JSON 一次读、两处用(钩子的 branch + 终端图标颜色)。提前发起,
// 让这趟远程往返和后面的本地准备工作重叠;读失败为 null,两处各自退化。
const statePromise: Promise<Record<string, unknown> | null> | undefined
= issueNumber !== undefined
? panel.readIssueState(issueNumber).catch((err) => {
logger.add({
level: 'warn',
source: 'panel',
message: `resume #${issueNumber} 读 state JSON 失败`,
details: err instanceof Error ? err.message : String(err),
})
return null
})
: undefined
// 实施 / 头脑风暴各自独立:调用方传入对应字段,空 = DEFAULT。
const effectiveProfilePath
= sessionKind === 'implement'
@@ -127,28 +141,10 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
// (issueNumber === undefined) 钩子签名要 issueNumber,同样跳过。
if (sessionKind === 'implement' && !worktreeMissing && issueNumber !== undefined && workspaceRoot) {
const settingsForHook = getSettings(panel.context)
// 从 state JSON 读 branch;任何一步读不到就用空字符串(脚本里
// BRANCH env 仍能传,只是为空),不阻塞 resume 主流程。
let branchForHook = ''
try {
const remote = await detectRepo(workspaceRoot)
if (remote) {
const token = await getToken(panel.context, remote.host)
if (token) {
const stateObj = await panel.readIssueState(issueNumber)
if (stateObj && typeof stateObj.branch === 'string')
branchForHook = stateObj.branch
}
}
}
catch (err) {
logger.add({
level: 'warn',
source: 'panel',
message: `resume #${issueNumber} 读 state JSON 失败,钩子 branch 用空字符串`,
details: err instanceof Error ? err.message : String(err),
})
}
// 从 state JSON 读 branch;读不到就用空字符串(脚本里 BRANCH env 仍能
// 传,只是为空),不阻塞 resume 主流程。
const stateObj = await statePromise
const branchForHook = typeof stateObj?.branch === 'string' ? stateObj.branch : ''
await panel.dispatchWorktreeHook('impl-tab-pre-create', {
workspaceRoot,
worktreePath: effectiveCwd!,
@@ -164,7 +160,7 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
// harmless in the editor tab, still useful if the same terminal ever
// gets rendered in panel-view mode.
const issueIcon = issueNumber !== undefined
? await panel.resolveIssueIcon(issueNumber)
? await panel.resolveIssueIcon(issueNumber, await statePromise)
: undefined
const terminal = window.createTerminal({
name: terminalName,
@@ -198,6 +194,8 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
finally {
if (lockKey !== undefined)
panel.resumeInFlight.delete(lockKey)
// 无论开成、复用已有 tab 还是被前置锁拒绝,都要告诉 webview 收 spinner。
panel.postMessage({ type: 'session/tab-settled', sessionId })
}
}
@@ -303,6 +301,7 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
}
finally {
panel.resumeInFlight.delete(lockKey)
panel.postMessage({ type: 'session/tab-settled', sessionId })
}
}
@@ -1379,6 +1378,7 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
}
finally {
panel.resumeInFlight.delete(lockKey)
panel.postMessage({ type: 'session/tab-settled', sessionId })
}
}
+35
View File
@@ -0,0 +1,35 @@
import type { Issue } from '../gitea/types'
import { describe, expect, it } from 'vitest'
import { evaluateLockedReason } from './issueLock'
function issue(number: number, column: Issue['column'], prerequisite?: number): Issue {
return { number, title: `#${number}`, column, prerequisite } as Issue
}
describe('evaluateLockedReason', () => {
it('没有前置 → 放行', () => {
expect(evaluateLockedReason([issue(1, 'todo')], 1)).toEqual({ locked: false })
})
it('前置未完成 → 锁定并带上前置号和所在列', () => {
const issues = [issue(2, 'todo', 1), issue(1, 'in-progress')]
expect(evaluateLockedReason(issues, 2)).toEqual({
locked: true,
prerequisiteNumber: 1,
prerequisiteColumn: 'in-progress',
})
})
it('前置已完成 → 放行', () => {
const issues = [issue(2, 'todo', 1), issue(1, 'done')]
expect(evaluateLockedReason(issues, 2)).toEqual({ locked: false })
})
it('快照里没有前置工单 → 放行(不误锁)', () => {
expect(evaluateLockedReason([issue(2, 'todo', 99)], 2)).toEqual({ locked: false })
})
it('快照里没有这张工单 → 放行', () => {
expect(evaluateLockedReason([issue(1, 'todo')], 7)).toEqual({ locked: false })
})
})
+25
View File
@@ -0,0 +1,25 @@
import type { Issue } from '../gitea/types'
export interface LockedReason {
locked: boolean
prerequisiteNumber?: number
prerequisiteColumn?: string
}
/**
* 纯判定:工单被未完成的前置挡住才算锁定。快照里找不到工单或前置一律放行,
* 与 webview 的 `isIssueLocked` 保持同一语义。
*/
export function evaluateLockedReason(issues: Issue[], issueNumber: number): LockedReason {
const issue = issues.find(i => i.number === issueNumber)
if (!issue || issue.prerequisite === undefined)
return { locked: false }
const prereq = issues.find(i => i.number === issue.prerequisite)
if (!prereq || prereq.column === 'done')
return { locked: false }
return {
locked: true,
prerequisiteNumber: prereq.number,
prerequisiteColumn: prereq.column,
}
}
+2
View File
@@ -53,6 +53,8 @@ export type ExtensionToWebview
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
| { type: 'issues/error', message: string }
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
| { type: 'session/tab-settled', sessionId: string }
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
| { type: 'handoff/users-result', issueNumber: number, users: string[] }
| { type: 'handoff/done', issueNumber: number }
+2
View File
@@ -40,6 +40,7 @@ export function App() {
dismissToast,
openUrl,
resumeSession,
isSessionTabPending,
resumeReviewSession,
startTestSession,
resumeTestSession,
@@ -373,6 +374,7 @@ export function App() {
globalAutoReview={globalAutoReview}
onOpenInBrowser={openUrl}
onResumeSession={resumeSession}
isSessionTabPending={isSessionTabPending}
onResumeReviewSession={resumeReviewSession}
onStartTestSession={startTestSession}
onResumeTestSession={resumeTestSession}
@@ -29,6 +29,7 @@ interface BottomTabsProps {
globalAutoReview: boolean
onOpenInBrowser: (url: string) => void
onResumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void
isSessionTabPending: (sessionId: string) => boolean
onResumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void
onStartTestSession: (issueNumber: number) => void
onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
@@ -127,6 +128,7 @@ export function BottomTabs(props: BottomTabsProps) {
globalAutoReview={props.globalAutoReview}
onOpenInBrowser={props.onOpenInBrowser}
onResumeSession={props.onResumeSession}
isSessionTabPending={props.isSessionTabPending}
onResumeReviewSession={props.onResumeReviewSession}
onStartTestSession={props.onStartTestSession}
onResumeTestSession={props.onResumeTestSession}
@@ -39,6 +39,8 @@ interface IssueDetailPanelProps {
globalAutoReview: boolean
onOpenInBrowser: (url: string) => void
onResumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void
/** 会话 id 行的 spinner:该会话的开 tab 请求还没被扩展处理完就为 true。 */
isSessionTabPending: (sessionId: string) => boolean
/**
* Open a new terminal that runs `codex resume <id>` for the auto-review
* conversation associated with this issue.
@@ -153,6 +155,7 @@ export function IssueDetailPanel({
onUpdateTestProfilePath,
onOpenLogs,
me,
isSessionTabPending,
onStartHandoff,
onAcceptHandoff,
isHandoffRunning,
@@ -252,6 +255,7 @@ export function IssueDetailPanel({
key: 'sessionId',
label: '头脑风暴会话id',
type: 'action' as const,
pending: hasSession ? isSessionTabPending(issue!.sessionId!) : false,
description: hasSession
? '双击在新终端运行 claude --resume <id> 恢复对话;点 ▶ 续聊'
: '工单尚未关联 cc 会话;点 ▶ 新开一个规划会话并续聊',
@@ -274,6 +278,7 @@ export function IssueDetailPanel({
key: 'implementSessionId',
label: '实施会话id',
type: 'action',
pending: issue?.implementSessionId ? isSessionTabPending(issue.implementSessionId) : false,
description: issue?.worktreeExists
? '双击在新终端运行 claude --resume <id> 恢复实施对话(cwd 为 worktree)'
: 'worktree 已清理,将在工作区根目录恢复(cc 可能提示原 cwd 不存在)',
@@ -298,6 +303,7 @@ export function IssueDetailPanel({
key: 'reviewSessionId',
label: '审查会话id',
type: 'action',
pending: issue.reviewSessionId ? isSessionTabPending(issue.reviewSessionId) : false,
description: '双击在新终端运行 codex resume <id> 查看审查会话(worktree 在则用之,否则工作区根目录)',
actionIcon: <Terminal className="size-3.5" />,
onAction: (v) => {
@@ -355,6 +361,7 @@ export function IssueDetailPanel({
key: 'testSessionId',
label: '测试会话id',
type: 'action' as const,
pending: hasSession ? isSessionTabPending(issue!.testSessionId!) : false,
description: hasSession
? '双击在新终端运行 claude --resume <id> 在工作区根(main)恢复测试对话'
: '工单 PR 合并后点击右侧 ▶ 在工作区根(main)启动一个测试 cc tab(让 cc 了解代码后告诉你怎么测试)',
@@ -535,7 +542,7 @@ export function IssueDetailPanel({
],
},
],
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles],
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles, isSessionTabPending],
)
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
@@ -60,6 +60,11 @@ export interface PropertyDef {
onAction?: (value: unknown) => void
/** type='action' 专用:值前的图标 */
actionIcon?: ReactNode
/**
* 主动作是否还在进行中。给了就由外部决定 spinner 何时收(真实进度),
* 不给则沿用点击后固定 1.5s 的即时反馈。
*/
pending?: boolean
/** type='file-link' 专用:双击文件名时调用,参数为 value(workspace 相对路径) */
onOpen?: (path: string) => void
/** type='file-link' 专用:点击右侧图标按钮时调用,用于重新扫描/加载 */
@@ -174,7 +179,8 @@ function PropertyRow({
registerCellRef: (fullKey: string, el: HTMLDivElement | null) => void
}) {
const [showPassword, setShowPassword] = useState(false)
const [actionPending, setActionPending] = useState(false)
const [localActionPending, setLocalActionPending] = useState(false)
const actionPending = propDef.pending ?? localActionPending
const pendingTimerRef = useRef<number | null>(null)
const ro = propDef.readOnly
@@ -183,14 +189,16 @@ function PropertyRow({
window.clearTimeout(pendingTimerRef.current)
}, [])
/** 触发主动作并给出即时反馈:行内图标临时切换为 spinner,约 1.5s 后恢复 */
/** 触发主动作:外部受控时 spinner 交给 propDef.pending,否则本地转约 1.5s */
function firePrimaryAction(fire: () => void): void {
fire()
setActionPending(true)
if (propDef.pending !== undefined)
return
setLocalActionPending(true)
if (pendingTimerRef.current != null)
window.clearTimeout(pendingTimerRef.current)
pendingTimerRef.current = window.setTimeout(() => {
setActionPending(false)
setLocalActionPending(false)
pendingTimerRef.current = null
}, 1500)
}
+56 -4
View File
@@ -184,6 +184,8 @@ export interface UseIssuesResult {
dismissToast: (id: string) => void
openUrl: (url: string) => void
resumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void
/** 该会话 id 的开 tab 请求还没被扩展处理完 → 行内转 spinner。 */
isSessionTabPending: (sessionId: string) => boolean
resumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void
startTestSession: (issueNumber: number) => void
resumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
@@ -479,6 +481,50 @@ export function useIssues(): UseIssuesResult {
postMessage({ type: 'toast/open-url', url })
}, [])
// 开 tab 是秒级操作(要读 state JSON、跑 pre-create 钩子),行内 spinner 要
// 真实反映进度:发请求时标 pending,扩展回 session/tab-settled 时清。消息万一
// 丢了就靠 PENDING_TIMEOUT_MS 兜底,spinner 不会永远转下去。
const [pendingSessionTabs, setPendingSessionTabs] = useState<Set<string>>(() => new Set())
const pendingTimersRef = useRef<Map<string, number>>(new Map())
const PENDING_TIMEOUT_MS = 60_000
const clearSessionTabPending = useCallback((sessionId: string): void => {
const timer = pendingTimersRef.current.get(sessionId)
if (timer !== undefined) {
window.clearTimeout(timer)
pendingTimersRef.current.delete(sessionId)
}
setPendingSessionTabs((prev) => {
if (!prev.has(sessionId))
return prev
const next = new Set(prev)
next.delete(sessionId)
return next
})
}, [])
const markSessionTabPending = useCallback((sessionId: string): void => {
const existing = pendingTimersRef.current.get(sessionId)
if (existing !== undefined)
window.clearTimeout(existing)
pendingTimersRef.current.set(
sessionId,
window.setTimeout(() => clearSessionTabPending(sessionId), PENDING_TIMEOUT_MS),
)
setPendingSessionTabs(prev => new Set(prev).add(sessionId))
}, [clearSessionTabPending])
const isSessionTabPending = useCallback(
(sessionId: string): boolean => pendingSessionTabs.has(sessionId),
[pendingSessionTabs],
)
useEffect(() => () => {
for (const timer of pendingTimersRef.current.values())
window.clearTimeout(timer)
pendingTimersRef.current.clear()
}, [])
const resumeSession = useCallback((
sessionId: string,
kind: 'implement' | 'brainstorm',
@@ -486,24 +532,27 @@ export function useIssues(): UseIssuesResult {
cwd?: string,
issueNumber?: number,
): void => {
markSessionTabPending(sessionId)
postMessage({ type: 'session/resume', sessionId, kind, profilePath, cwd, issueNumber })
}, [])
}, [markSessionTabPending])
const focusSession = useCallback((issueNumber: number): void => {
postMessage({ type: 'session/focus', issueNumber })
}, [])
const resumeReviewSession = useCallback((sessionId: string, issueNumber: number, cwd?: string): void => {
markSessionTabPending(sessionId)
postMessage({ type: 'session/resume-review', sessionId, issueNumber, cwd })
}, [])
}, [markSessionTabPending])
const startTestSession = useCallback((issueNumber: number): void => {
postMessage({ type: 'session/start-test', issueNumber })
}, [])
const resumeTestSession = useCallback((sessionId: string, issueNumber: number, cwd?: string): void => {
markSessionTabPending(sessionId)
postMessage({ type: 'session/resume-test', sessionId, issueNumber, cwd })
}, [])
}, [markSessionTabPending])
const openFile = useCallback((path: string): void => {
postMessage({ type: 'editor/open-file', path })
@@ -722,6 +771,9 @@ export function useIssues(): UseIssuesResult {
useEffect(() => {
const cleanup = onMessage((msg) => {
switch (msg.type) {
case 'session/tab-settled':
clearSessionTabPending(msg.sessionId)
break
case 'issues/loading':
setRefreshing(true)
setState(prev => prev.status === 'ready' ? prev : { status: 'loading' })
@@ -938,5 +990,5 @@ export function useIssues(): UseIssuesResult {
return cleanup
}, [clearPrDiffSummaryRunning, markHandoffRunning])
return { state, settings, avatars, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearBrainstormSession, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
return { state, settings, avatars, isSessionTabPending, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearBrainstormSession, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
}
+2
View File
@@ -84,6 +84,8 @@ export type ExtensionToWebview
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
| { type: 'issues/error', message: string }
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
| { type: 'session/tab-settled', sessionId: string }
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
| { type: 'handoff/users-result', issueNumber: number, users: string[] }
| { type: 'session-handoff/users-result', users: string[] }