✨ feat(vscode): 多人协作二期:状态分层/写校验/签名/团队视图
- 本机状态(会话id/worktree/profile/prDiffFile)迁 workspaceState, 共享 state JSON 只留团队字段;读侧本地记录叠加,旧数据兜底 - mergeStateJsonComment 写后读回校验,并发覆盖时在最新状态上重放一次 - webhook 支持 Gitea secret,校验 X-Gitea-Signature(HMAC-SHA256), 不匹配 401;设置面板新增 Webhook Secret 字段 - 看板新增 我的/全部 范围切换(团队视图),按工作区持久化
This commit is contained in:
@@ -4,7 +4,9 @@ import type {
|
||||
TerminalEditorLocationOptions,
|
||||
WebviewPanel,
|
||||
} from 'vscode'
|
||||
import type { HookContext } from '../git/worktreeHooks'
|
||||
import type { Issue } from '../gitea/types'
|
||||
import type { IssueRef } from '../issues/stateRouter'
|
||||
import type { ExtensionToWebview, WebviewToExtension } from './messages'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import * as fs from 'node:fs'
|
||||
@@ -12,13 +14,11 @@ import * as path from 'node:path'
|
||||
import { env, TabInputTerminal, ThemeColor, Uri, ViewColumn, window, workspace } from 'vscode'
|
||||
import { deleteToken, getToken } from '../auth/secrets'
|
||||
import { detectRepo } from '../git/remote'
|
||||
import type { HookContext } from '../git/worktreeHooks'
|
||||
import {
|
||||
GiteaApiError,
|
||||
postIssueComment,
|
||||
} from '../gitea/api'
|
||||
import { loadIssues } from '../gitea/issueLoader'
|
||||
import type { IssueRef } from '../issues/stateRouter'
|
||||
import { overlayLocalIssueState } from '../issues/localState'
|
||||
import { closeIssueByRef, mergeIssueState, readIssueState } from '../issues/stateRouter'
|
||||
import { logger } from '../logging/logger'
|
||||
import { getEffectiveCommitProfilePath, getSettings } from '../settings/store'
|
||||
@@ -39,6 +39,9 @@ import { PALETTE, resolveIssueColor, themeColorIdToIconUri } from './issueColor'
|
||||
/** Resolved at runtime via getProfilesDir() — do not hardcode user paths. */
|
||||
export { getDefaultProfilePath, getPrDiffSummaryProfilePath } from '../cc/profiles'
|
||||
|
||||
/** workspaceState key:看板范围('mine' 只看我的 / 'all' 团队全部)。 */
|
||||
const SCOPE_KEY = 'superpowers.kanbanScope'
|
||||
|
||||
export class KanbanWebviewPanel {
|
||||
static readonly viewType = 'superpowers.kanbanPanel'
|
||||
|
||||
@@ -108,9 +111,11 @@ export class KanbanWebviewPanel {
|
||||
workspaceRoot: string
|
||||
inboxDir: string
|
||||
terminalName: string
|
||||
/** The brainstorm terminal created synchronously in `handleIssueCreate`.
|
||||
/**
|
||||
* The brainstorm terminal created synchronously in `handleIssueCreate`.
|
||||
* Stored here so `linkPendingTerminalToIssue` can promote it into
|
||||
* `newIssueTerminals` once the webhook tells us the issueNumber. */
|
||||
* `newIssueTerminals` once the webhook tells us the issueNumber.
|
||||
*/
|
||||
terminal: Terminal
|
||||
createdAt: number
|
||||
}>()
|
||||
@@ -360,6 +365,12 @@ export class KanbanWebviewPanel {
|
||||
void this.loadAndPush()
|
||||
return
|
||||
}
|
||||
if (msg.type === 'issues/set-scope') {
|
||||
// 视图偏好按工作区持久化;切换即重拉。
|
||||
void this.context.workspaceState.update(SCOPE_KEY, msg.scope)
|
||||
void this.loadAndPush()
|
||||
return
|
||||
}
|
||||
if (msg.type === 'settings/save') {
|
||||
void settings.handleSettingsSave(this, msg)
|
||||
return
|
||||
@@ -594,7 +605,6 @@ export class KanbanWebviewPanel {
|
||||
}
|
||||
if (msg.type === 'pr-diff-mode/set') {
|
||||
void prFiles.handleSetPrDiffMode(this, msg.mode)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -777,7 +787,6 @@ export class KanbanWebviewPanel {
|
||||
return worktree.dispatchImplTabPostCloseAsync(this, issueNumber)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Server-side lock check for prerequisite gating. Fetches a fresh issues
|
||||
* snapshot (we don't trust webview state) and reports whether
|
||||
@@ -846,6 +855,7 @@ export class KanbanWebviewPanel {
|
||||
host,
|
||||
tokenSaved: false,
|
||||
webhookPort: s.webhookPort,
|
||||
webhookSecret: s.webhookSecret,
|
||||
brainstormPrompt: s.brainstormPrompt,
|
||||
brainstormContinuePrompt: s.brainstormContinuePrompt,
|
||||
implementPlanPrompt: s.implementPlanPrompt,
|
||||
@@ -869,8 +879,9 @@ export class KanbanWebviewPanel {
|
||||
return
|
||||
}
|
||||
|
||||
const scope = this.context.workspaceState.get<'mine' | 'all'>(SCOPE_KEY) ?? 'mine'
|
||||
try {
|
||||
const giteaIssues = await loadIssues({ host, token, owner, repo, workspaceRoot })
|
||||
const giteaIssues = await loadIssues({ host, token, owner, repo, workspaceRoot, scope })
|
||||
// YouTrack is a best-effort second source: a failure here must never
|
||||
// break the gitea board, so swallow it into a toast + log.
|
||||
let youtrackList: Issue[] = []
|
||||
@@ -888,7 +899,10 @@ export class KanbanWebviewPanel {
|
||||
dismissOnTimer: 6000,
|
||||
})
|
||||
}
|
||||
const issues = this.withLiveTerminalTabState([...giteaIssues, ...youtrackList])
|
||||
// 本机记录(会话 id / worktree 等)叠加在共享状态之上,再补 live terminal 态。
|
||||
const issues = this.withLiveTerminalTabState(
|
||||
overlayLocalIssueState(this.context, [...giteaIssues, ...youtrackList], workspaceRoot),
|
||||
)
|
||||
this.issueRefs = new Map(
|
||||
issues.map(i => [i.number, { source: i.source ?? 'gitea', externalId: i.externalId }] as const),
|
||||
)
|
||||
@@ -896,6 +910,7 @@ export class KanbanWebviewPanel {
|
||||
this.postMessage({
|
||||
type: 'issues/update',
|
||||
issues,
|
||||
scope,
|
||||
globalAutoReview: ytSettings.autoReview,
|
||||
youtrackConfigured: ytSettings.youtrackBaseUrl.trim() !== '' && ytSettings.youtrackProjectShortName.trim() !== '',
|
||||
})
|
||||
@@ -914,6 +929,7 @@ export class KanbanWebviewPanel {
|
||||
errorMessage: 'Token 无效或已过期,请重新填写',
|
||||
tokenSaved: false,
|
||||
webhookPort: s.webhookPort,
|
||||
webhookSecret: s.webhookSecret,
|
||||
brainstormPrompt: s.brainstormPrompt,
|
||||
brainstormContinuePrompt: s.brainstormContinuePrompt,
|
||||
implementPlanPrompt: s.implementPlanPrompt,
|
||||
@@ -970,8 +986,10 @@ export class KanbanWebviewPanel {
|
||||
return mergeIssueState(this.context, this.refFor(issueNumber), extra)
|
||||
}
|
||||
|
||||
/** Resolve/close the issue in its tracker. Returns false when a youtrack
|
||||
* close command can't be determined. */
|
||||
/**
|
||||
* Resolve/close the issue in its tracker. Returns false when a youtrack
|
||||
* close command can't be determined.
|
||||
*/
|
||||
// internal: handler 模块访问
|
||||
closeIssueByNumber(issueNumber: number): Promise<boolean> {
|
||||
return closeIssueByRef(this.context, this.refFor(issueNumber))
|
||||
|
||||
Reference in New Issue
Block a user