🐛 fix(vscode): 移交只对负责人显示;接管后审查会话立即可点
Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph
This commit is contained in:
@@ -11,14 +11,18 @@ import {
|
|||||||
} from './handoff'
|
} from './handoff'
|
||||||
|
|
||||||
describe('canStartHandoff', () => {
|
describe('canStartHandoff', () => {
|
||||||
it('gitea 工单、非 done、无待接管 → true', () => {
|
it('gitea 工单、非 done、无待接管、无 assignee 或 assignee 含我 → true', () => {
|
||||||
expect(canStartHandoff({ column: 'in-progress' })).toBe(true)
|
expect(canStartHandoff({ column: 'in-progress' }, 'me')).toBe(true)
|
||||||
expect(canStartHandoff({ column: 'todo', source: 'gitea' })).toBe(true)
|
expect(canStartHandoff({ column: 'todo', source: 'gitea' }, 'me')).toBe(true)
|
||||||
|
expect(canStartHandoff({ column: 'todo', assignees: [] }, 'me')).toBe(true)
|
||||||
|
expect(canStartHandoff({ column: 'todo', assignees: ['me', 'x'] }, 'me')).toBe(true)
|
||||||
})
|
})
|
||||||
it('done / youtrack / 已在移交中 → false', () => {
|
it('done / youtrack / 已在移交中 / 不是我负责 → false', () => {
|
||||||
expect(canStartHandoff({ column: 'done' })).toBe(false)
|
expect(canStartHandoff({ column: 'done' }, 'me')).toBe(false)
|
||||||
expect(canStartHandoff({ column: 'review', source: 'youtrack' })).toBe(false)
|
expect(canStartHandoff({ column: 'review', source: 'youtrack' }, 'me')).toBe(false)
|
||||||
expect(canStartHandoff({ column: 'review', handoffAttachmentId: '9' })).toBe(false)
|
expect(canStartHandoff({ column: 'review', handoffAttachmentId: '9' }, 'me')).toBe(false)
|
||||||
|
expect(canStartHandoff({ column: 'todo', assignees: ['x'] }, 'me')).toBe(false)
|
||||||
|
expect(canStartHandoff({ column: 'todo', assignees: ['me'] }, undefined)).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -162,4 +166,27 @@ describe('payloads', () => {
|
|||||||
worktreeExists: false,
|
worktreeExists: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
it('接管 UI patch:带 reviewSessionFileExists 时一并下发,UI 不用等下次刷新才能点链接', () => {
|
||||||
|
expect(handoffAcceptedUiPatch({
|
||||||
|
sessions: { reviewSessionId: 'r' },
|
||||||
|
profiles: {},
|
||||||
|
worktreeExists: false,
|
||||||
|
reviewSessionFileExists: true,
|
||||||
|
})).toEqual({
|
||||||
|
handoffAttachmentId: null,
|
||||||
|
handoffFrom: null,
|
||||||
|
reviewSessionId: 'r',
|
||||||
|
worktreePath: null,
|
||||||
|
worktreeExists: false,
|
||||||
|
reviewSessionFileExists: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('接管 UI patch:reviewSessionFileExists 未传时不出现在 patch 里', () => {
|
||||||
|
const patch = handoffAcceptedUiPatch({
|
||||||
|
sessions: { sessionId: 's' },
|
||||||
|
profiles: {},
|
||||||
|
worktreeExists: false,
|
||||||
|
})
|
||||||
|
expect(patch).not.toHaveProperty('reviewSessionFileExists')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ export function canStartHandoff(issue: {
|
|||||||
source?: 'gitea' | 'youtrack'
|
source?: 'gitea' | 'youtrack'
|
||||||
column: IssueColumn
|
column: IssueColumn
|
||||||
handoffAttachmentId?: string
|
handoffAttachmentId?: string
|
||||||
}): boolean {
|
assignees?: string[]
|
||||||
return issue.source !== 'youtrack' && issue.column !== 'done' && !issue.handoffAttachmentId
|
}, me: string | undefined): boolean {
|
||||||
|
const assignees = issue.assignees ?? []
|
||||||
|
const mine = assignees.length === 0 || (me !== undefined && assignees.includes(me))
|
||||||
|
return issue.source !== 'youtrack' && issue.column !== 'done' && !issue.handoffAttachmentId && mine
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canAcceptHandoff(issue: {
|
export function canAcceptHandoff(issue: {
|
||||||
@@ -105,6 +108,7 @@ export function handoffAcceptedUiPatch(local: {
|
|||||||
profiles: HandoffProfiles
|
profiles: HandoffProfiles
|
||||||
worktreePath?: string
|
worktreePath?: string
|
||||||
worktreeExists: boolean
|
worktreeExists: boolean
|
||||||
|
reviewSessionFileExists?: boolean
|
||||||
}): Record<string, unknown> {
|
}): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
handoffAttachmentId: null,
|
handoffAttachmentId: null,
|
||||||
@@ -115,5 +119,6 @@ export function handoffAcceptedUiPatch(local: {
|
|||||||
// 与 worktreeExists: false 对不上。
|
// 与 worktreeExists: false 对不上。
|
||||||
worktreePath: local.worktreePath ?? null,
|
worktreePath: local.worktreePath ?? null,
|
||||||
worktreeExists: local.worktreeExists,
|
worktreeExists: local.worktreeExists,
|
||||||
|
...(local.reviewSessionFileExists !== undefined ? { reviewSessionFileExists: local.reviewSessionFileExists } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,17 +152,8 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 前置复查:webview 按钮的显示条件可能已经过期(并发移交、工单已被拖到
|
// 先拉远端 issue:既要兜底「Gitea 已关闭但从没拖进看板」的工单,也要拿到
|
||||||
// 完成),用刚读到的最新 state 再判一次 canStartHandoff,避免重复移交。
|
// assignees 判断「是否我负责」——两者都得在 canStartHandoff 判定前就绪。
|
||||||
const knownColumns: IssueColumn[] = ['todo', 'in-progress', 'review', 'done']
|
|
||||||
const column = knownColumns.includes(state.column as IssueColumn) ? (state.column as IssueColumn) : 'todo'
|
|
||||||
if (!canStartHandoff({ source: 'gitea', column, handoffAttachmentId: str(state.handoffAttachmentId) })) {
|
|
||||||
toast(panel, 'error', `#${issueNumber} 已在移交中或已完成,不能再次移交`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// column 兜底成 'todo' 时挡不住「Gitea 已关闭但从没拖进看板」的工单——
|
|
||||||
// 再拉一次远端 issue.state 兜底。
|
|
||||||
const issue = await getIssue({ host, token, owner, repo, index: issueNumber })
|
const issue = await getIssue({ host, token, owner, repo, index: issueNumber })
|
||||||
if (!issue) {
|
if (!issue) {
|
||||||
toast(panel, 'error', `工单 #${issueNumber} 不存在或已删除`)
|
toast(panel, 'error', `工单 #${issueNumber} 不存在或已删除`)
|
||||||
@@ -173,6 +164,21 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 前置复查:webview 按钮的显示条件可能已经过期(并发移交、工单已被拖到
|
||||||
|
// 完成、指派已变更),用刚读到的最新 state + issue 再判一次 canStartHandoff,
|
||||||
|
// 避免重复移交或抢别人的工单。
|
||||||
|
const knownColumns: IssueColumn[] = ['todo', 'in-progress', 'review', 'done']
|
||||||
|
const column = knownColumns.includes(state.column as IssueColumn) ? (state.column as IssueColumn) : 'todo'
|
||||||
|
const assignees = (issue.assignees ?? []).map(a => a.login)
|
||||||
|
if (!canStartHandoff({ source: 'gitea', column, handoffAttachmentId: str(state.handoffAttachmentId), assignees }, me)) {
|
||||||
|
// 区分「不是我的工单」和「已在移交中/已完成」两种拒绝原因,措辞不同。
|
||||||
|
const notMine = assignees.length > 0 && !(me !== undefined && assignees.includes(me))
|
||||||
|
toast(panel, 'error', notMine
|
||||||
|
? `#${issueNumber} 不是你负责的工单,不能移交`
|
||||||
|
: `#${issueNumber} 已在移交中或已完成,不能再次移交`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const branch = str(state.branch)
|
const branch = str(state.branch)
|
||||||
const worktreePath = str(state.worktreePath)
|
const worktreePath = str(state.worktreePath)
|
||||||
const sessions: HandoffSessions = {
|
const sessions: HandoffSessions = {
|
||||||
@@ -456,6 +462,10 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ④ 本机字段 + 清共享字段;⑤ 删附件(失败只 warn)
|
// ④ 本机字段 + 清共享字段;⑤ 删附件(失败只 warn)
|
||||||
|
// reviewSessionFileExists 是计算字段,board 只在启动时算一次(annotateReviewSessionFileExists);
|
||||||
|
// 接管刚把 codex 会话文件装进本机,这里顺手算好一起下发,UI 不用等下次刷新才能点「审查会话」链接。
|
||||||
|
const reviewSessionFileExists = !!manifest.sessions.reviewSessionId
|
||||||
|
&& manifest.codex.some(c => c.id.toLowerCase() === manifest.sessions.reviewSessionId!.toLowerCase())
|
||||||
const local = { sessions: manifest.sessions, profiles: manifest.profiles, worktreePath: worktreeAbs }
|
const local = { sessions: manifest.sessions, profiles: manifest.profiles, worktreePath: worktreeAbs }
|
||||||
await panel.mergeIssueState(issueNumber, handoffAcceptedStateExtra(local))
|
await panel.mergeIssueState(issueNumber, handoffAcceptedStateExtra(local))
|
||||||
try {
|
try {
|
||||||
@@ -469,7 +479,7 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber
|
|||||||
panel.postMessage({
|
panel.postMessage({
|
||||||
type: 'issue/patch',
|
type: 'issue/patch',
|
||||||
issueNumber,
|
issueNumber,
|
||||||
patch: handoffAcceptedUiPatch({ ...local, worktreeExists: !!worktreeAbs }),
|
patch: handoffAcceptedUiPatch({ ...local, worktreeExists: !!worktreeAbs, reviewSessionFileExists }),
|
||||||
})
|
})
|
||||||
toast(panel, 'success', `已接管 #${issueNumber}(来自 ${manifest.from})${worktreeAbs ? `,worktree:${worktreeAbs}` : ''}`)
|
toast(panel, 'success', `已接管 #${issueNumber}(来自 ${manifest.from})${worktreeAbs ? `,worktree:${worktreeAbs}` : ''}`)
|
||||||
logger.add({ level: 'info', source: 'panel', message: `接管 #${issueNumber} 完成`, details: JSON.stringify({ manifest, worktreeAbs }) })
|
logger.add({ level: 'info', source: 'panel', message: `接管 #${issueNumber} 完成`, details: JSON.stringify({ manifest, worktreeAbs }) })
|
||||||
|
|||||||
@@ -597,7 +597,7 @@ export function IssueDetailPanel({
|
|||||||
接管
|
接管
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!issue.handoffAttachmentId && issue.column !== 'done' && (
|
{!issue.handoffAttachmentId && issue.column !== 'done' && ((issue.assignees ?? []).length === 0 || (me !== undefined && (issue.assignees ?? []).includes(me))) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isHandoffRunning(issue.number)}
|
disabled={isHandoffRunning(issue.number)}
|
||||||
|
|||||||
Reference in New Issue
Block a user