✨ 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:
@@ -16,10 +16,10 @@
|
||||
* after a drag. Persisting drag moves back to Gitea is step 3+.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { Issue, IssueColumn } from '../types'
|
||||
import type { ToastItem } from '../components/ToastStack'
|
||||
import type { LogEntry } from '../lib/messages'
|
||||
import type { Issue, IssueColumn } from '../types'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { compareIssuesInColumn } from '../lib/issueSort'
|
||||
import { onMessage, postMessage } from '../lib/vscode'
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface SettingsValues {
|
||||
host: string
|
||||
token: string
|
||||
webhookPort: number
|
||||
webhookSecret: string
|
||||
brainstormPrompt: string
|
||||
brainstormContinuePrompt: string
|
||||
implementPlanPrompt: string
|
||||
@@ -68,6 +69,7 @@ export interface SettingsOverlayState {
|
||||
canCancel: boolean
|
||||
tokenSaved: boolean
|
||||
webhookPort: number
|
||||
webhookSecret: string
|
||||
brainstormPrompt: string
|
||||
brainstormContinuePrompt: string
|
||||
implementPlanPrompt: string
|
||||
@@ -107,10 +109,10 @@ export interface YouTrackProjectsState {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type UseIssuesState =
|
||||
| { status: 'loading' }
|
||||
| { status: 'ready', issues: Issue[] }
|
||||
| { status: 'error', message: string }
|
||||
export type UseIssuesState
|
||||
= | { status: 'loading' }
|
||||
| { status: 'ready', issues: Issue[] }
|
||||
| { status: 'error', message: string }
|
||||
|
||||
/** Mirror of the extension-side ClaudeProfile in src/cc/profiles.ts. */
|
||||
export interface ClaudeProfile {
|
||||
@@ -121,23 +123,34 @@ export interface ClaudeProfile {
|
||||
export interface UseIssuesResult {
|
||||
state: UseIssuesState
|
||||
settings: SettingsOverlayState | null
|
||||
/** Latest known global `autoReview` value pushed alongside `issues/update`.
|
||||
/** 看板范围:'mine' 只看我的(assigned/created),'all' 团队全部。 */
|
||||
scope: 'mine' | 'all'
|
||||
toggleScope: () => void
|
||||
/**
|
||||
* Latest known global `autoReview` value pushed alongside `issues/update`.
|
||||
* Used by the detail panel to display the fallback value when an issue has
|
||||
* no per-issue override. */
|
||||
* no per-issue override.
|
||||
*/
|
||||
globalAutoReview: boolean
|
||||
/** Whether YouTrack is configured (base URL + project set), pushed alongside
|
||||
* `issues/update`. Drives whether the「导入 YouTrack 工单」toolbar button shows. */
|
||||
/**
|
||||
* Whether YouTrack is configured (base URL + project set), pushed alongside
|
||||
* `issues/update`. Drives whether the「导入 YouTrack 工单」toolbar button shows.
|
||||
*/
|
||||
youtrackConfigured: boolean
|
||||
/** Open the native multi-select dialog to pick which YouTrack issues to
|
||||
* mirror onto the board. */
|
||||
/**
|
||||
* Open the native multi-select dialog to pick which YouTrack issues to
|
||||
* mirror onto the board.
|
||||
*/
|
||||
importYouTrack: () => void
|
||||
toasts: ToastItem[]
|
||||
profiles: ClaudeProfile[]
|
||||
setIssues: (issues: Issue[]) => void
|
||||
refresh: () => void
|
||||
saveSettings: (values: SettingsValues) => void
|
||||
/** Fetch the YouTrack project list for the settings dropdown using the
|
||||
* in-form base URL + token (so the user can pick before saving). */
|
||||
/**
|
||||
* Fetch the YouTrack project list for the settings dropdown using the
|
||||
* in-form base URL + token (so the user can pick before saving).
|
||||
*/
|
||||
listYouTrackProjects: (baseUrl: string, token: string) => void
|
||||
/** Result of the most recent `listYouTrackProjects` call. */
|
||||
youtrackProjects: YouTrackProjectsState
|
||||
@@ -159,81 +172,115 @@ export interface UseIssuesResult {
|
||||
openWorktree: (path: string) => void
|
||||
deleteWorktree: (issueNumber: number, path: string) => void
|
||||
mergeBranch: (issueNumber: number, branch: string) => void
|
||||
/** 硬删 Gitea 工单及关联资源(worktree / PR / feature branch / cc tabs)。
|
||||
/**
|
||||
* 硬删 Gitea 工单及关联资源(worktree / PR / feature branch / cc tabs)。
|
||||
* 用户在详情面板顶部点垃圾桶按钮触发。扩展端做 modal confirm + 串行清理,
|
||||
* 全部成功后 push `issue/remove`,webview 将该 issue 从 issues 数组移除。 */
|
||||
* 全部成功后 push `issue/remove`,webview 将该 issue 从 issues 数组移除。
|
||||
*/
|
||||
deleteIssue: (issueNumber: number) => void
|
||||
/** 关闭 Gitea 工单,不清理本地会话、worktree、PR 或分支。 */
|
||||
closeIssue: (issueNumber: number) => void
|
||||
/** Dispose the matching session terminal tab. Extension reacts via
|
||||
* onDidCloseTerminal and clears `*TabOpen` so the X button disappears. */
|
||||
/**
|
||||
* Dispose the matching session terminal tab. Extension reacts via
|
||||
* onDidCloseTerminal and clears `*TabOpen` so the X button disappears.
|
||||
*/
|
||||
closeSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
|
||||
/** Spawn a fresh "规划" cc tab for an existing issue whose sessionId is
|
||||
/**
|
||||
* Spawn a fresh "规划" cc tab for an existing issue whose sessionId is
|
||||
* still empty. Extension 用工单级 brainstormProfilePath(空 = 默认),
|
||||
* watches `~/.claude/projects/<encoded-workspaceRoot>` for the new
|
||||
* session jsonl, then writes the id back as `sessionId`. */
|
||||
* session jsonl, then writes the id back as `sessionId`.
|
||||
*/
|
||||
startBrainstormSession: (issueNumber: number) => void
|
||||
changeColumn: (issueNumber: number, toColumn: IssueColumn, source?: 'gitea' | 'youtrack', externalId?: string) => void
|
||||
setDependency: (issueNumber: number, prerequisiteNumber: number) => void
|
||||
clearDependency: (issueNumber: number, prerequisiteNumber: number) => void
|
||||
updateIssueAutoReview: (issueNumber: number, value: boolean) => void
|
||||
/** 覆盖该工单实施会话使用的 Claude 配置文件,乐观更新本地 state 后
|
||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。 */
|
||||
/**
|
||||
* 覆盖该工单实施会话使用的 Claude 配置文件,乐观更新本地 state 后
|
||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
||||
*/
|
||||
updateIssueProfilePath: (issueNumber: number, profilePath: string) => void
|
||||
/** 覆盖该工单头脑风暴会话专用的 Claude 配置文件,乐观更新本地 state 后
|
||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。 */
|
||||
/**
|
||||
* 覆盖该工单头脑风暴会话专用的 Claude 配置文件,乐观更新本地 state 后
|
||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
||||
*/
|
||||
updateIssueBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
||||
/** 覆盖该工单测试会话专用的 Claude 配置文件,乐观更新本地 state 后
|
||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。 */
|
||||
/**
|
||||
* 覆盖该工单测试会话专用的 Claude 配置文件,乐观更新本地 state 后
|
||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
||||
*/
|
||||
updateIssueTestProfilePath: (issueNumber: number, testProfilePath: string) => void
|
||||
logs: LogEntry[]
|
||||
fetchLogs: () => void
|
||||
clearLogs: () => void
|
||||
/** True while the "提交当前代码" claude -p run is in flight. Used to
|
||||
* disable the toolbar button and swap its icon to a spinner. */
|
||||
/**
|
||||
* True while the "提交当前代码" claude -p run is in flight. Used to
|
||||
* disable the toolbar button and swap its icon to a spinner.
|
||||
*/
|
||||
commitRunning: boolean
|
||||
/** Trigger a background `claude -p "提交下代码"` run in the workspace
|
||||
/**
|
||||
* Trigger a background `claude -p "提交下代码"` run in the workspace
|
||||
* root, gated by `commitRunning`. The extension responds with
|
||||
* `commit/state` messages and a `toast/show` on completion. */
|
||||
* `commit/state` messages and a `toast/show` on completion.
|
||||
*/
|
||||
runCommit: () => void
|
||||
/** Whether the workspace git working tree currently has any uncommitted
|
||||
/**
|
||||
* Whether the workspace git working tree currently has any uncommitted
|
||||
* changes (working tree / index / untracked). Pushed by the extension via
|
||||
* `commit/has-changes` and used by `PanelHeader` to skip rendering the
|
||||
* commit button when the tree is clean. Defaults to `false` until the
|
||||
* extension reports its first observation. */
|
||||
* extension reports its first observation.
|
||||
*/
|
||||
hasChanges: boolean
|
||||
/** Commits the remote auto-build branch is behind the remote dev branch
|
||||
* (per latest `branch-sync/status`). 0 ⇒ in sync ⇒ disable button. */
|
||||
/**
|
||||
* Commits the remote auto-build branch is behind the remote dev branch
|
||||
* (per latest `branch-sync/status`). 0 ⇒ in sync ⇒ disable button.
|
||||
*/
|
||||
branchSyncBehind: number
|
||||
/** True while a `branch-sync/run` request is in flight. The extension
|
||||
/**
|
||||
* True while a `branch-sync/run` request is in flight. The extension
|
||||
* doesn't echo a "running" state itself — we set this on click and clear
|
||||
* it when the next `branch-sync/status` arrives. */
|
||||
* it when the next `branch-sync/status` arrives.
|
||||
*/
|
||||
branchSyncRunning: boolean
|
||||
/** True when sync is structurally unavailable (same branch, missing
|
||||
* remote, fetch error, …). Disables the button regardless of behind. */
|
||||
/**
|
||||
* True when sync is structurally unavailable (same branch, missing
|
||||
* remote, fetch error, …). Disables the button regardless of behind.
|
||||
*/
|
||||
branchSyncDisabled: boolean
|
||||
/** Tooltip text for the sync button (already includes branch names /
|
||||
* behind count / unavailable reason). */
|
||||
/**
|
||||
* Tooltip text for the sync button (already includes branch names /
|
||||
* behind count / unavailable reason).
|
||||
*/
|
||||
branchSyncTitle: string
|
||||
/** Trigger a fast-forward push from remote dev to remote auto-build. */
|
||||
runBranchSync: () => void
|
||||
/** Whether the workspace's `.env*` files are currently chmod-locked
|
||||
* according to `workspaceState`. Persisted per-workspace. */
|
||||
/**
|
||||
* Whether the workspace's `.env*` files are currently chmod-locked
|
||||
* according to `workspaceState`. Persisted per-workspace.
|
||||
*/
|
||||
envLocked: boolean
|
||||
/** File count from the latest `env-lock/status`. 0 disables the toggle. */
|
||||
envFileCount: number
|
||||
/** True while a `env-lock/toggle` chmod batch is in flight. Cleared when
|
||||
* the next `env-lock/status` arrives. */
|
||||
/**
|
||||
* True while a `env-lock/toggle` chmod batch is in flight. Cleared when
|
||||
* the next `env-lock/status` arrives.
|
||||
*/
|
||||
envLockRunning: boolean
|
||||
/** Tooltip text for the env-lock button, pre-composed from lock state +
|
||||
* file count. */
|
||||
/**
|
||||
* Tooltip text for the env-lock button, pre-composed from lock state +
|
||||
* file count.
|
||||
*/
|
||||
envLockTitle: string
|
||||
/** Flip the env-lock state: chmod all `.env*` files to 444 or 644. */
|
||||
toggleEnvLock: () => void
|
||||
/** ID of an issue that should be auto-selected after an `issue/append`
|
||||
/**
|
||||
* ID of an issue that should be auto-selected after an `issue/append`
|
||||
* with `select: true` (i.e. user-initiated webhook creation). Consumers
|
||||
* read this in a `useEffect`, apply the selection, then call
|
||||
* `clearPendingSelect()` to reset. */
|
||||
* `clearPendingSelect()` to reset.
|
||||
*/
|
||||
pendingSelectId: string | null
|
||||
clearPendingSelect: () => void
|
||||
}
|
||||
@@ -243,6 +290,7 @@ export function useIssues(): UseIssuesResult {
|
||||
const [settings, setSettings] = useState<SettingsOverlayState | null>(null)
|
||||
const [youtrackProjects, setYoutrackProjects] = useState<YouTrackProjectsState>({ status: 'idle', projects: [] })
|
||||
const [globalAutoReview, setGlobalAutoReview] = useState<boolean>(true)
|
||||
const [scope, setScope] = useState<'mine' | 'all'>('mine')
|
||||
const [youtrackConfigured, setYoutrackConfigured] = useState<boolean>(false)
|
||||
const [toasts, setToasts] = useState<ToastItem[]>([])
|
||||
const [profiles, setProfiles] = useState<ClaudeProfile[]>([])
|
||||
@@ -271,6 +319,12 @@ export function useIssues(): UseIssuesResult {
|
||||
setPendingSelectId(null)
|
||||
}, [])
|
||||
|
||||
const toggleScope = useCallback((): void => {
|
||||
setState({ status: 'loading' })
|
||||
setScope(prev => prev === 'mine' ? 'all' : 'mine')
|
||||
postMessage({ type: 'issues/set-scope', scope: scope === 'mine' ? 'all' : 'mine' })
|
||||
}, [scope])
|
||||
|
||||
const refresh = useCallback((): void => {
|
||||
setState({ status: 'loading' })
|
||||
postMessage({ type: 'issues/refresh' })
|
||||
@@ -290,6 +344,7 @@ export function useIssues(): UseIssuesResult {
|
||||
host: values.host,
|
||||
token: values.token,
|
||||
webhookPort: values.webhookPort,
|
||||
webhookSecret: values.webhookSecret,
|
||||
brainstormPrompt: values.brainstormPrompt,
|
||||
brainstormContinuePrompt: values.brainstormContinuePrompt,
|
||||
implementPlanPrompt: values.implementPlanPrompt,
|
||||
@@ -557,6 +612,7 @@ export function useIssues(): UseIssuesResult {
|
||||
break
|
||||
case 'issues/update':
|
||||
setState({ status: 'ready', issues: msg.issues })
|
||||
setScope(msg.scope)
|
||||
setGlobalAutoReview(msg.globalAutoReview)
|
||||
setYoutrackConfigured(msg.youtrackConfigured)
|
||||
break
|
||||
@@ -638,6 +694,7 @@ export function useIssues(): UseIssuesResult {
|
||||
canCancel: msg.canCancel === true,
|
||||
tokenSaved: msg.tokenSaved,
|
||||
webhookPort: msg.webhookPort,
|
||||
webhookSecret: msg.webhookSecret,
|
||||
brainstormPrompt: msg.brainstormPrompt,
|
||||
brainstormContinuePrompt: msg.brainstormContinuePrompt,
|
||||
implementPlanPrompt: msg.implementPlanPrompt,
|
||||
@@ -760,5 +817,5 @@ export function useIssues(): UseIssuesResult {
|
||||
return cleanup
|
||||
}, [clearPrDiffSummaryRunning])
|
||||
|
||||
return { state, settings, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, startBrainstormSession, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, branchSyncBehind, branchSyncRunning, branchSyncDisabled, branchSyncTitle, runBranchSync, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock }
|
||||
return { state, settings, 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, startBrainstormSession, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, branchSyncBehind, branchSyncRunning, branchSyncDisabled, branchSyncTitle, runBranchSync, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user