⚡ 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:
1 parent
b182e0ac5b
commit
70b826123c
12 files changed
+218
-71
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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[] }
|
||||
|
||||
Reference in new issue
Block a user