🐛 fix(vscode): 刷新按钮加载态可见,图标随重拉转圈

刷新按钮此前只发消息重拉数据,看板已有列表时界面毫无变化,用户
无法判断点击是否生效。消息协议里的 issues/loading 从未被发送。

loadAndPush 现在在开跑前推 issues/loading,并在 finally 里推新增的
issues/loaded,保证成功、报错、缺 token 提前返回都能收尾;webhook
触发的自动重拉同样走这条链路。webview 据此维护 refreshing 状态,
刷新图标转圈并在期间禁用按钮。

Claude-Session: https://claude.ai/code/session_01REdhpbaNyqWASF2HWsnTFv
This commit is contained in:
cruldra committed 2026-09-13 08:22:38 +08:00
1 parent 9167b86708
commit ef0750b4ef
7 files changed
+30 -5

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
"publisher": "clurdra",
"name": "superpowers-vscode-clurdra",
"displayName": "Superpowers-clurdra",
"version": "0.2.125",
"version": "0.2.126",
"packageManager": "pnpm@10.27.0",
"description": "Superpowers specs and plans Kanban explorer",
"author": "clurdra",
+4
View File
@@ -912,8 +912,12 @@ export class KanbanWebviewPanel {
async loadAndPush(): Promise<void> {
if (this.loadInFlight)
return this.loadInFlight
// 每次真正开跑都成对推 loading/loaded,webview 据此转刷新图标;
// 无论成功、报错还是缺 token 中途返回,finally 都会收尾。
this.postMessage({ type: 'issues/loading' })
this.loadInFlight = this.loadAndPushInner().finally(() => {
this.loadInFlight = undefined
this.postMessage({ type: 'issues/loaded' })
})
return this.loadInFlight
}
+1
View File
@@ -49,6 +49,7 @@ export type ToastLevel = 'info' | 'success' | 'error'
export type ExtensionToWebview
= | { type: 'issues/loading' }
| { type: 'issues/loaded' }
| { 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 } }
+4
View File
@@ -30,6 +30,7 @@ export function App() {
avatars,
setIssues,
refresh,
refreshing,
saveSettings,
listYouTrackProjects,
youtrackProjects,
@@ -305,6 +306,7 @@ export function App() {
<>
<PanelHeader
onRefresh={refresh}
refreshing={refreshing}
scope={scope}
onToggleScope={toggleScope}
onEditAuth={requestEditAuth}
@@ -331,6 +333,7 @@ export function App() {
<>
<PanelHeader
onRefresh={refresh}
refreshing={refreshing}
scope={scope}
onToggleScope={toggleScope}
onEditAuth={requestEditAuth}
@@ -429,6 +432,7 @@ export function App() {
<>
<PanelHeader
onRefresh={refresh}
refreshing={refreshing}
scope={scope}
onToggleScope={toggleScope}
onEditAuth={requestEditAuth}
@@ -4,6 +4,8 @@ import { useEffect, useRef, useState } from 'react'
interface Props {
onRefresh: () => void
/** host 正在重拉看板;为 true 时刷新图标转起来。 */
refreshing: boolean
/** 看板范围:'mine' 只看我的(assigned/created),'all' 团队全部工单。 */
scope: 'mine' | 'all'
onToggleScope: () => void
@@ -65,6 +67,7 @@ function releaseTitle(release: ReleaseStatus | undefined): string {
export function PanelHeader({
onRefresh,
refreshing,
scope,
onToggleScope,
onEditAuth,
@@ -207,11 +210,13 @@ export function PanelHeader({
<button
type="button"
onClick={onRefresh}
title="刷新"
disabled={refreshing}
title={refreshing ? '正在刷新…' : '刷新'}
aria-label="刷新"
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)]"
aria-busy={refreshing}
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)] disabled:cursor-not-allowed"
>
<RefreshCw size={14} />
<RefreshCw size={14} className={refreshing ? 'animate-spin' : undefined} />
</button>
<button
type="button"
+11 -1
View File
@@ -159,6 +159,11 @@ export interface UseIssuesResult {
profiles: ClaudeProfile[]
setIssues: (issues: Issue[]) => void
refresh: () => void
/**
* True while the host is reloading the board (手动刷新、切换范围、webhook
* 触发都算)。看板已有列表时不会退回 loading 空屏,靠它转刷新图标给反馈。
*/
refreshing: boolean
saveSettings: (values: SettingsValues) => void
/**
* Fetch the YouTrack project list for the settings dropdown using the
@@ -344,6 +349,7 @@ export function useIssues(): UseIssuesResult {
// 默认团队全部,与扩展侧 DEFAULT_KANBAN_SCOPE 一致(两个包不共享源码)。
const [scope, setScope] = useState<'mine' | 'all'>(injected?.scope ?? 'all')
const [youtrackConfigured, setYoutrackConfigured] = useState<boolean>(injected?.youtrackConfigured ?? false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [toasts, setToasts] = useState<ToastItem[]>([])
const [profiles, setProfiles] = useState<ClaudeProfile[]>([])
const [logs, setLogs] = useState<LogEntry[]>([])
@@ -700,8 +706,12 @@ export function useIssues(): UseIssuesResult {
const cleanup = onMessage((msg) => {
switch (msg.type) {
case 'issues/loading':
setRefreshing(true)
setState(prev => prev.status === 'ready' ? prev : { status: 'loading' })
break
case 'issues/loaded':
setRefreshing(false)
break
case 'issues/update':
setState({ status: 'ready', issues: msg.issues })
setScope(msg.scope)
@@ -908,5 +918,5 @@ export function useIssues(): UseIssuesResult {
return cleanup
}, [clearPrDiffSummaryRunning, markHandoffRunning])
return { state, settings, avatars, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, 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, 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, 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, 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 }
}
+1
View File
@@ -80,6 +80,7 @@ export interface ProfilesData {
export type ExtensionToWebview
= | { type: 'issues/loading' }
| { type: 'issues/loaded' }
| { 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 } }