From ae06c8e5d19a25c9a88ed8579532b7980d699b59 Mon Sep 17 00:00:00 2001 From: cruldra Date: Mon, 19 Oct 2026 08:25:10 +1100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E7=9C=8B=E6=9D=BF=E6=8C=89=E4=BB=93=E5=BA=93=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=8C=BA=E5=88=86=20GitHub=20=E4=B8=8E=20Gitea?= =?UTF-8?q?=EF=BC=8CGitHub=20=E6=98=BE=E7=A4=BA=20gh=20=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=8A=B6=E6=80=81=EF=BC=8C=E6=96=87=E6=A1=88=E5=8E=BB=20Gitea?= =?UTF-8?q?=20=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vscode/package.json | 10 +- vscode/src/auth/identity.ts | 37 ++- vscode/src/auth/secrets.ts | 4 +- vscode/src/cc/sessionHandoff.ts | 4 +- vscode/src/codeReader/review/reviewRules.ts | 2 +- vscode/src/extension.ts | 4 +- vscode/src/git/remote.ts | 4 +- vscode/src/git/worktree.ts | 2 +- vscode/src/git/worktreeHooks.ts | 2 +- vscode/src/logging/logger.ts | 2 +- vscode/src/panel/KanbanPanel.ts | 37 +-- vscode/src/panel/handlers/branchCleanup.ts | 4 +- vscode/src/panel/handlers/handoffFlow.ts | 4 +- vscode/src/panel/handlers/issues.ts | 22 +- vscode/src/panel/handlers/sessions.ts | 13 +- vscode/src/panel/handlers/settings.ts | 42 ++- vscode/src/panel/handlers/terminals.ts | 2 +- vscode/src/panel/handlers/youtrackIssues.ts | 2 +- vscode/src/panel/issueOwnership.ts | 6 +- vscode/src/panel/kanbanSnapshot.ts | 2 +- vscode/src/panel/messages.ts | 5 + vscode/src/panel/readOnlyGuard.ts | 2 +- vscode/src/sessions/prReviewStore.ts | 4 +- vscode/src/sessions/sessionFiles.ts | 2 +- vscode/src/youtrack/api.ts | 2 +- vscode/src/youtrack/issueLoader.ts | 4 +- vscode/webview-ui/src/App.tsx | 2 + .../webview-ui/src/components/BottomTabs.tsx | 2 +- .../webview-ui/src/components/IssueCard.tsx | 2 +- .../src/components/IssueChatPanel.tsx | 11 +- .../src/components/IssueDetailPanel.tsx | 21 +- .../webview-ui/src/components/KanbanBoard.tsx | 4 +- .../webview-ui/src/components/PanelHeader.tsx | 4 +- .../src/components/PrFilesPanel.tsx | 6 +- .../src/components/SettingsModal.test.tsx | 51 +++- .../src/components/SettingsModal.tsx | 253 ++++++++++++------ vscode/webview-ui/src/hooks/useIssues.ts | 15 +- vscode/webview-ui/src/hooks/usePrFiles.ts | 2 +- vscode/webview-ui/src/lib/messages.ts | 5 + vscode/webview-ui/src/lib/trackerName.test.ts | 14 + vscode/webview-ui/src/lib/trackerName.ts | 14 + vscode/webview-ui/src/types.ts | 8 +- 42 files changed, 443 insertions(+), 195 deletions(-) create mode 100644 vscode/webview-ui/src/lib/trackerName.test.ts create mode 100644 vscode/webview-ui/src/lib/trackerName.ts diff --git a/vscode/package.json b/vscode/package.json index 3258645..c75c3c8 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -32,7 +32,7 @@ "activationEvents": [ "onView:superpowers.kanban", "onCommand:superpowers.openKanban", - "onCommand:superpowers.setGiteaToken", + "onCommand:superpowers.openSettings", "onCommand:superpowers.configureHandoffStore", "onUri", "onView:superpowers.codeReader.files", @@ -255,12 +255,12 @@ }, { "command": "superpowers.refresh", - "title": "Refresh Issues (Gitea + YouTrack)", + "title": "Refresh Issues", "category": "Superpowers" }, { - "command": "superpowers.setGiteaToken", - "title": "Set Gitea Token", + "command": "superpowers.openSettings", + "title": "Open Settings", "category": "Superpowers" }, { @@ -1423,7 +1423,7 @@ "group": "navigation" }, { - "command": "superpowers.setGiteaToken", + "command": "superpowers.openSettings", "when": "view == superpowers.kanban", "group": "navigation" }, diff --git a/vscode/src/auth/identity.ts b/vscode/src/auth/identity.ts index 7dea73a..511740d 100644 --- a/vscode/src/auth/identity.ts +++ b/vscode/src/auth/identity.ts @@ -15,6 +15,9 @@ import { StatusBarAlignment, ThemeColor, window } from 'vscode' import { resolveForge } from '../forge/context' import { logger } from '../logging/logger' +/** 打开看板设置的命令:配凭据、看当前登录状态都在设置里,状态栏点进去就是它。 */ +export const OPEN_SETTINGS_COMMAND = 'superpowers.openSettings' + const userByHost = new Map() let statusBar: StatusBarItem | undefined @@ -42,7 +45,7 @@ export async function loginFor(forge: ForgeContext): Promise } } -/** token 变更后必须调用,否则状态栏和门禁还认旧身份。 */ +/** 凭据变更(换 token、gh 换账号或登录失效)后必须调用,否则状态栏和门禁还认旧身份。 */ export function clearIdentityCache(host?: string): void { if (host) userByHost.delete(host) @@ -51,7 +54,7 @@ export function clearIdentityCache(host?: string): void { } /** - * 激活时(以及 token 保存后)调用:解析当前工作区的身份,展示到状态栏,并与 git 提交邮箱比对。 + * 激活时(以及凭据变更后)调用:解析当前工作区的身份,展示到状态栏,并与 git 提交邮箱比对。 * token 被同步或复制到同事机器是发生过的真实事故(chw 机器),这里是最后一道人眼防线。 */ export async function initIdentity(ctx: ExtensionContext): Promise { @@ -59,6 +62,7 @@ export async function initIdentity(ctx: ExtensionContext): Promise { statusBar = window.createStatusBarItem(StatusBarAlignment.Left, 0) ctx.subscriptions.push(statusBar) } + statusBar.command = OPEN_SETTINGS_COMMAND const resolved = await resolveForge(ctx) if (!resolved.ok) { @@ -66,9 +70,14 @@ export async function initIdentity(ctx: ExtensionContext): Promise { statusBar.hide() return } - statusBar.text = '$(account) spx: 未配置 token' - statusBar.tooltip = `尚未为 ${resolved.host} 配置访问令牌,点击配置` - statusBar.command = 'superpowers.setGiteaToken' + if (resolved.kind === 'github') { + statusBar.text = '$(account) spx: 未登录 gh' + statusBar.tooltip = 'GitHub 仓库用 GitHub CLI 的登录,请在终端运行 gh auth login' + } + else { + statusBar.text = '$(account) spx: 未配置 token' + statusBar.tooltip = `尚未为 ${resolved.host} 配置访问令牌,点击配置` + } statusBar.backgroundColor = new ThemeColor('statusBarItem.warningBackground') statusBar.show() return @@ -77,9 +86,14 @@ export async function initIdentity(ctx: ExtensionContext): Promise { const { forge } = resolved const login = await loginFor(forge) if (!login) { - statusBar.text = '$(account) spx: token 失效' - statusBar.tooltip = `${forge.host} 的 token 无法解析身份,点击重新配置` - statusBar.command = 'superpowers.setGiteaToken' + if (forge.kind === 'github') { + statusBar.text = '$(account) spx: gh 登录失效' + statusBar.tooltip = 'GitHub CLI 的登录无法解析身份,请在终端运行 gh auth login 重新登录' + } + else { + statusBar.text = '$(account) spx: token 失效' + statusBar.tooltip = `${forge.host} 的 token 无法解析身份,点击重新配置` + } statusBar.backgroundColor = new ThemeColor('statusBarItem.errorBackground') statusBar.show() return @@ -87,7 +101,6 @@ export async function initIdentity(ctx: ExtensionContext): Promise { statusBar.text = `$(account) spx: ${login}` statusBar.tooltip = `身份 ${login} @ ${forge.host}` - statusBar.command = 'superpowers.setGiteaToken' statusBar.backgroundColor = undefined statusBar.show() logger.add({ @@ -100,7 +113,9 @@ export async function initIdentity(ctx: ExtensionContext): Promise { } /** - * 代码托管账号邮箱对不上 `git config user.email`,基本等于用了别人的 token 或 git 身份没配;只警告不拦截,同名多邮箱的合法场景存在。 + * 代码托管账号邮箱对不上 `git config user.email`,基本等于用了别人的凭据或 git 身份没配;只警告不拦截,同名多邮箱的合法场景存在。 + * + * 账号没公开邮箱时(GitHub 常见)没得比,直接跳过。 */ function checkGitIdentityMatch(workspaceRoot: string, host: string, login: string): void { const forgeEmail = userByHost.get(host)?.email?.trim().toLowerCase() @@ -116,6 +131,6 @@ function checkGitIdentityMatch(workspaceRoot: string, host: string, login: strin message: `代码托管身份与 git 提交邮箱不一致: ${login}(${forgeEmail}) vs git(${gitEmail})`, }) void window.showWarningMessage( - `token 身份是 ${login}(${forgeEmail}),但 git 提交邮箱是 ${gitEmail}。确认没有用错同事的 token。`, + `代码托管账号是 ${login}(${forgeEmail}),但 git 提交邮箱是 ${gitEmail}。确认没有用错同事的凭据。`, ) } diff --git a/vscode/src/auth/secrets.ts b/vscode/src/auth/secrets.ts index f51ef0e..cb50b8f 100644 --- a/vscode/src/auth/secrets.ts +++ b/vscode/src/auth/secrets.ts @@ -1,6 +1,6 @@ /** * Helpers around VS Code's SecretStorage for storing per-host Gitea personal - * access tokens. + * access tokens. GitHub repos borrow the gh CLI login and store nothing here. * * Tokens are keyed by host so the user can connect to multiple Gitea instances * without the extension confusing their credentials. @@ -26,7 +26,7 @@ export async function deleteToken(ctx: ExtensionContext, host: string): Promise< /** * YouTrack permanent tokens, keyed by host so they never collide with the - * gitea token of the same host. + * Gitea token of the same host. */ export function youtrackTokenKey(host: string): string { return `youtrack-token:${host}` diff --git a/vscode/src/cc/sessionHandoff.ts b/vscode/src/cc/sessionHandoff.ts index 0ce686f..bace3c8 100644 --- a/vscode/src/cc/sessionHandoff.ts +++ b/vscode/src/cc/sessionHandoff.ts @@ -6,7 +6,7 @@ */ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i -/** Gitea 的用户名、仓库名只有字母、数字和 `_` `.` `-`;`/`、`!` 这类会弄乱 key 的字符一律不收。 */ +/** Gitea 和 GitHub 的用户名、仓库名都只有字母、数字和 `_` `.` `-`;`/`、`!` 这类会弄乱 key 的字符一律不收。 */ const NAME_RE = /^[\w.-]+$/ const KEY_RE = /^spx-handoff\/([\w.-]+)\/([\w.-]+)\/session\/([\w.-]+)\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})!([\w.-]+)!([^!/]*)\.tar\.xz$/i @@ -47,7 +47,7 @@ export function slugSessionName(name: string): string { /** * `to` 在这个仓库的会话交接收件箱:OSS 上的目录前缀 `spx-handoff///session//`。 - * 结尾带 `/`,列 `bob` 的收件箱不会列到 `bobby` 的包。三段都转小写:Gitea 的用户名、仓库名 + * 结尾带 `/`,列 `bob` 的收件箱不会列到 `bobby` 的包。三段都转小写:代码托管的用户名、仓库名 * 不分大小写,各人 git remote 地址里的写法可能不一样,转小写两边才对得上同一个收件箱。 * * @throws owner / repo / to 含 `[\w.-]` 以外的字符。 diff --git a/vscode/src/codeReader/review/reviewRules.ts b/vscode/src/codeReader/review/reviewRules.ts index 874547e..56f1656 100644 --- a/vscode/src/codeReader/review/reviewRules.ts +++ b/vscode/src/codeReader/review/reviewRules.ts @@ -29,7 +29,7 @@ function inReviewColumns(column: IssueColumn): boolean { } /** - * 选工单弹框列哪些工单:只要 Gitea 的(PR 在 Gitea 上,YouTrack 的不算);「审查」列的都列(没 PR 的选中时说一声), + * 选工单弹框列哪些工单:只要代码托管仓库的(PR 在那里,YouTrack 的不算);「审查」列的都列(没 PR 的选中时说一声), * 「进行中」列的只列已经有 PR 的(没 PR 的还在开发,列出来只会添乱)。按列分组(REVIEW_COLUMNS 的次序), * 组里工单号大的在前(和看板列里一样)。 */ diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 2e08a5d..17dbaac 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -6,7 +6,7 @@ import type { } from 'vscode' import type { CodeReaderApi } from './codeReader/contract' import { commands, EventEmitter, window } from 'vscode' -import { initIdentity } from './auth/identity' +import { initIdentity, OPEN_SETTINGS_COMMAND } from './auth/identity' import { setProfilesDirOverride } from './cc/profiles' import { registerCodeReader } from './codeReader' import { migrateLegacySourceKeys } from './issues/localState' @@ -107,7 +107,7 @@ export function activate(context: ExtensionContext): SuperpowersApi { commands.registerCommand('superpowers.refresh', () => { KanbanWebviewPanel.refresh() }), - commands.registerCommand('superpowers.setGiteaToken', () => { + commands.registerCommand(OPEN_SETTINGS_COMMAND, () => { KanbanWebviewPanel.createOrShow(context) KanbanWebviewPanel.requestEditAuth() }), diff --git a/vscode/src/git/remote.ts b/vscode/src/git/remote.ts index 52cb6f6..04e236c 100644 --- a/vscode/src/git/remote.ts +++ b/vscode/src/git/remote.ts @@ -1,8 +1,8 @@ /** * Detects the workspace's forge repo coordinates (host / owner / repo) from `git remote get-url origin`. * - * Supports both HTTPS forms (`https://gitea.x.cn/owner/repo.git`) and SSH - * forms (`git@gitea.x.cn:owner/repo.git`, with or without a leading + * Supports both HTTPS forms (`https://git.example.com/owner/repo.git`) and SSH + * forms (`git@git.example.com:owner/repo.git`, with or without a leading * `ssh://`). The pure URL parser is exported separately so it's unit-testable * without spawning a child process. */ diff --git a/vscode/src/git/worktree.ts b/vscode/src/git/worktree.ts index df33113..c4ef571 100644 --- a/vscode/src/git/worktree.ts +++ b/vscode/src/git/worktree.ts @@ -166,7 +166,7 @@ export async function ensureWorktreeFromRemote(opts: WorktreeOpts): Promise/head`,PR 的分支本机没有也拉得到。 */ +/** 从 origin 拉 PR 的最新提交:Gitea 和 GitHub 都给每个 PR 留了 `refs/pull//head`,PR 的分支本机没有也拉得到。 */ export async function fetchPullRequestHead(workspaceRoot: string, pr: number): Promise { await runGit(workspaceRoot, ['fetch', 'origin', `refs/pull/${pr}/head`], `拉取 PR #${pr}`) } diff --git a/vscode/src/git/worktreeHooks.ts b/vscode/src/git/worktreeHooks.ts index 55a1e36..38cbd0d 100644 --- a/vscode/src/git/worktreeHooks.ts +++ b/vscode/src/git/worktreeHooks.ts @@ -14,7 +14,7 @@ * - WORKTREE_PATH — absolute path of the worktree * - WORKSPACE_ROOT — absolute path of the main workspace * - BRANCH — feature branch name - * - ISSUE_NUMBER — gitea issue number (decimal string) + * - ISSUE_NUMBER — forge issue number (decimal string) * - MAIN_BRANCH — settings.devBranch (default 'main') */ diff --git a/vscode/src/logging/logger.ts b/vscode/src/logging/logger.ts index adae101..98f7743 100644 --- a/vscode/src/logging/logger.ts +++ b/vscode/src/logging/logger.ts @@ -20,7 +20,7 @@ export interface LogEntry { /** Milliseconds since epoch. */ ts: number level: LogLevel - /** Short tag identifying the subsystem (e.g. 'webhook', 'gitea'). */ + /** Short tag identifying the subsystem (e.g. 'webhook', 'identity'). */ source: string message: string /** Optional extra blob, newline-safe (stack trace, JSON snippet, etc.). */ diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index 236db96..fcbb7e9 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -14,9 +14,11 @@ import type { KanbanScope } from './kanbanScope' import type { BoardIssues } from './kanbanSnapshot' import type { ExtensionToWebview, WebviewToExtension } from './messages' import { env, TabInputTerminal, ThemeColor, Uri, ViewColumn, window, workspace } from 'vscode' +import { clearIdentityCache } from '../auth/identity' import { deleteToken } from '../auth/secrets' import { importPluginIntoFigma } from '../figma/importPlugin' import { resolveForge } from '../forge/context' +import { GH_RELOGIN_HINT } from '../forge/github/ghAuth' import { loadIssues } from '../forge/issueLoader' import { ForgeApiError } from '../forge/types' import { overlayLocalIssueState } from '../issues/localState' @@ -69,7 +71,7 @@ export class KanbanWebviewPanel { private issueRefs = new Map() /** 工单号 → 归属信息,供写操作门禁判定「这是不是我的工单」。 */ private issueOwners = new Map() - /** 本机 token 对应的 Gitea login;解析失败为 undefined(此时不锁任何工单)。 */ + /** 本机凭据在代码托管仓库上对应的 login;解析失败为 undefined(此时不锁任何工单)。 */ private me: string | undefined /** 页面发过首次刷新请求后才推送卡片,避免页面启动时丢消息。 */ private webviewReady = false @@ -882,15 +884,14 @@ export class KanbanWebviewPanel { * 2. Move the issue to in-progress and stamp branch/worktreePath/status * into the state-JSON comment. * 3. Confirm the always-on webhook coordinator is bound to the current - * port, then register a gitea webhook scoped to the feature branch - * and hand the bookkeeping to the coordinator. + * port and hand the bookkeeping to the coordinator. * 4. Spawn `claude` in an editor-tab terminal with the implement prompt that * tells cc to create the worktree, implement the plan, and emit * `$pr_no` when done. * * The webhook coordinator (see src/webhook/coordinator.ts) receives the - * resulting PR event, writes back the PR number, and deletes the gitea - * hook — even if this panel has since been closed. + * resulting PR event and writes back the PR number — even if this panel + * has since been closed. */ // internal: handler 模块访问 async handleImplement( @@ -924,7 +925,7 @@ export class KanbanWebviewPanel { * `done` column. * * Fail-open: any error fetching issues is logged and we return - * `{ locked: false }` so a transient Gitea hiccup can't lock the user out + * `{ locked: false }` so a transient forge hiccup can't lock the user out * of starting a session. */ /** @@ -946,7 +947,7 @@ export class KanbanWebviewPanel { /** * 将当前 VS Code 里仍存活的会话终端同步回 issue 列表。 * - * `issues/update` 会用 Gitea 里持久化的 state JSON 整体替换 webview + * `issues/update` 会用代码托管仓库里持久化的 state JSON 整体替换 webview * 状态;tabOpen 是本地运行态,不在 state JSON 中,因此发送列表前要按 * live terminal 重新补齐,同时登记 terminalOrigin,确保详情面板的关闭 * 按钮能找到对应 terminal。 @@ -975,7 +976,7 @@ export class KanbanWebviewPanel { } /** - * Reloads issues from gitea and pushes to the webview. Public so the + * Reloads issues from the forge and YouTrack and pushes to the webview. Public so the * always-on webhook coordinator can refresh us when a PR event arrives. */ async loadAndPush(): Promise { @@ -1014,7 +1015,7 @@ export class KanbanWebviewPanel { if (this.activeLoad !== load || load.invalidated) return if (!resolved.ok) { - // 只有 Gitea 的 token 存在插件设置里,缺了就弹设置让用户填;别的原因直接显示。 + // 只有 Gitea 的 token 存在插件设置里,缺了就弹设置让用户填;GitHub 要用户自己去终端登录 gh,和别的原因一样直接显示,页面上有重试。 if (resolved.reason === 'no-credentials' && resolved.kind === 'gitea') await this.showTokenSettings(resolved.host) else @@ -1026,13 +1027,13 @@ export class KanbanWebviewPanel { const { workspaceRoot, host, owner, repo } = forge const scope = this.context.workspaceState.get(KANBAN_SCOPE_KEY) ?? DEFAULT_KANBAN_SCOPE try { - const [{ issues: giteaIssues, me, avatars }, youtrackList] = await Promise.all([ + const [{ issues: forgeIssues, me, avatars }, youtrackList] = await Promise.all([ loadIssues(forge, scope), this.loadYouTrackSafe(), ]) if (this.activeLoad !== load || load.invalidated) return - const recoveryIssues = load.updates.reduce(applyIssueUpdate, [...giteaIssues, ...youtrackList]) + const recoveryIssues = load.updates.reduce(applyIssueUpdate, [...forgeIssues, ...youtrackList]) this.issueRefs = new Map( recoveryIssues.map(i => [i.number, { source: i.source ?? 'forge', externalId: i.externalId }] as const), ) @@ -1043,7 +1044,7 @@ export class KanbanWebviewPanel { // 会话文件的会话 id,再补 live terminal 态。 const issues = this.withLiveTerminalTabState( await annotateMissingSessionIds( - overlayLocalIssueState(this.context, [...giteaIssues, ...youtrackList], workspaceRoot), + overlayLocalIssueState(this.context, [...forgeIssues, ...youtrackList], workspaceRoot), ), ) if (this.activeLoad !== load || load.invalidated) @@ -1075,7 +1076,12 @@ export class KanbanWebviewPanel { catch (err) { if (this.activeLoad !== load || load.invalidated) return - if (err instanceof ForgeApiError && err.status === 401 && forge.kind === 'gitea') { + if (err instanceof ForgeApiError && err.status === 401) { + clearIdentityCache(host) + if (forge.kind === 'github') { + this.postMessage({ type: 'issues/error', message: GH_RELOGIN_HINT }) + return + } await deleteToken(this.context, host) await this.showTokenSettings(host, 'Token 无效或已过期,请重新填写') return @@ -1092,6 +1098,7 @@ export class KanbanWebviewPanel { this.postMessage({ type: 'settings/show', host, + forgeKind: 'gitea', ...(errorMessage ? { errorMessage } : {}), tokenSaved: false, webhookPort: s.webhookPort, @@ -1164,13 +1171,13 @@ export class KanbanWebviewPanel { return this.issueRefs.get(issueNumber)?.source === 'youtrack' } - /** Read workflow state from the issue's tracker (gitea or youtrack). */ + /** Read workflow state from the issue's tracker (forge or youtrack). */ // internal: handler 模块访问 readIssueState(issueNumber: number): Promise> { return readIssueState(this.context, this.refFor(issueNumber)) } - /** Merge workflow state into the issue's tracker (gitea or youtrack). */ + /** Merge workflow state into the issue's tracker (forge or youtrack). */ // internal: handler 模块访问 mergeIssueState(issueNumber: number, extra: Record): Promise { return mergeIssueState(this.context, this.refFor(issueNumber), extra) diff --git a/vscode/src/panel/handlers/branchCleanup.ts b/vscode/src/panel/handlers/branchCleanup.ts index 4fff31c..e3cac39 100644 --- a/vscode/src/panel/handlers/branchCleanup.ts +++ b/vscode/src/panel/handlers/branchCleanup.ts @@ -6,14 +6,14 @@ import { logger } from '../../logging/logger' * 清理一个 feature 分支(远程 + 本地),供「拖到完成」和「手动删除 worktree」两处共用。 * * - 受保护分支(main/master/devBranch)直接跳过,绝不删。 - * - 远程删除走 gitea API,需 remote + token;缺任一则只删本地,不阻断。 + * - 远程删除走代码托管接口,需 remote + 凭据;缺任一则只删本地,不阻断。 * - 本地删除是强删(deleteLocalBranch 内部 `git branch -D`):调用方语义是「连分支一起删」, * 未合并也照删。 * - 全程 best-effort:远程 / 本地各自 try/catch,失败只记日志——分支清理从不回滚已完成的主流程 * (worktree 已删、PR 已合并)。branch 为空直接返回。 * * 放在 handlers 层而非 `git/branches.ts`:后者刻意不依赖 `vscode`(有单测覆盖 - * deleteLocalBranch),而本函数要用 logger(→ vscode)和 gitea API。 + * deleteLocalBranch),而本函数要用 logger(→ vscode)和代码托管接口。 */ export async function cleanupFeatureBranch(opts: { workspaceRoot: string diff --git a/vscode/src/panel/handlers/handoffFlow.ts b/vscode/src/panel/handlers/handoffFlow.ts index 7ff40a1..0230c7a 100644 --- a/vscode/src/panel/handlers/handoffFlow.ts +++ b/vscode/src/panel/handlers/handoffFlow.ts @@ -167,7 +167,7 @@ export async function handleHandoffStart(panel: KanbanWebviewPanel, issueNumber: return { ok: false, message } } - // 先拉远端 issue:既要兜底「Gitea 已关闭但从没拖进看板」的工单,也要拿到 + // 先拉远端 issue:既要兜底「远端已关闭但从没拖进看板」的工单,也要拿到 // assignees 判断「是否我负责」——两者都得在 canStartHandoff 判定前就绪。 const issue = await provider.getIssue(issueNumber) if (!issue) { @@ -507,7 +507,7 @@ export async function handleHandoffAccept(panel: KanbanWebviewPanel, issueNumber say('核对指派') // 前置复查:webview 按钮的显示条件(assignees 含 me)可能已经过期(工单被 - // 重新指派、被删),用刚从 Gitea 拉到的最新 issue 再判一次 canAcceptHandoff。 + // 重新指派、被删),用刚从代码托管仓库拉到的最新 issue 再判一次 canAcceptHandoff。 const issue = await provider.getIssue(issueNumber) if (!issue) { progress.dismiss() diff --git a/vscode/src/panel/handlers/issues.ts b/vscode/src/panel/handlers/issues.ts index 7b62924..d16d4ae 100644 --- a/vscode/src/panel/handlers/issues.ts +++ b/vscode/src/panel/handlers/issues.ts @@ -195,7 +195,7 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber: return } - // 3. Look up PR state on gitea. + // 3. Look up PR state on the forge. let pullRequest: ForgePullRequest try { pullRequest = await forge.provider.getPullRequest(prIndex) @@ -224,7 +224,7 @@ export async function handleColumnChange(panel: KanbanWebviewPanel, issueNumber: // 合并失败(冲突 / 已关闭 / 权限)回滚拖动到原列。 let outcome: MergeOutcome try { - outcome = await forge.provider.mergePullRequest(prIndex, 'merge') + outcome = await forge.provider.mergePullRequest(prIndex, forge.provider.capabilities.mergeMethods[0]) } catch (err) { outcome = { merged: false, reason: 'rejected', message: err instanceof Error ? err.message : String(err) } @@ -745,8 +745,8 @@ export async function handleResetToTodo(panel: KanbanWebviewPanel, issueNumber: * Validates the issue has a valid `planFile` recorded in its state, * and that the file actually exists on disk, then delegates the full * implementation pipeline to `handleImplement` (which owns issue locking, - * tab reuse, column/branch/worktreePath state writes, gitea webhook - * registration, cc spawn). On any pre-flight failure, rolls the optimistic + * tab reuse, column/branch/worktreePath state writes, webhook + * bookkeeping, cc spawn). On any pre-flight failure, rolls the optimistic * column move back to the source column. */ export async function handleDropToInProgress(panel: KanbanWebviewPanel, issueNumber: number): Promise { @@ -861,7 +861,7 @@ export async function handleDropToInProgress(panel: KanbanWebviewPanel, issueNum } /** - * 关闭 Gitea 工单,不清理本地会话、worktree、PR 或分支。 + * 关闭代码托管仓库上的工单,不清理本地会话、worktree、PR 或分支。 * 成功后复用 issue/remove 消息,让 open issues 看板立即移除该工单。 */ export async function handleCloseIssue(panel: KanbanWebviewPanel, issueNumber: number): Promise { @@ -921,7 +921,7 @@ export async function handleCloseIssue(panel: KanbanWebviewPanel, issueNumber: n } /** - * 硬删 Gitea 工单 + 关联资源(worktree / PR / feature branch / cc session + * 硬删代码托管仓库上的工单 + 关联资源(worktree / PR / feature branch / cc session * tabs)。每步独立 try/catch,任一步失败立即停下并 toast 报错,让用户手动 * 处理。前端在最后一步收到 `issue/remove` 后从 issues 数组移除该工单。 */ @@ -1029,7 +1029,7 @@ export async function handleDeleteIssue(panel: KanbanWebviewPanel, issueNumber: } } - // 6. 关 PR(gitea 把 PR 当 issue subtype 处理,PATCH /issues/{prNumber} 即可) + // 6. 关 PR if (prStr) { const prIndex = Number.parseInt(prStr, 10) if (Number.isFinite(prIndex)) { @@ -1257,7 +1257,7 @@ export async function handleIssueCreate( } /** - * 把 Gitea 工单状态改成 closed。failure non-fatal —— state JSON 已写、PR 已合、 + * 把代码托管仓库上的工单关掉。failure non-fatal —— state JSON 已写、PR 已合、 * 看板已切到 done 列,rollback 意义不大。失败时打 warn 日志 + warning toast, * 用户可手动关闭工单。 */ @@ -1295,7 +1295,7 @@ export async function syncCloseIssue(panel: KanbanWebviewPanel, forge: ForgeCont * 完)时才退回一次全量拉取。 * * Fail-open: any error fetching issues is logged and we return - * `{ locked: false }` so a transient Gitea hiccup can't lock the user out + * `{ locked: false }` so a transient forge hiccup can't lock the user out * of starting a session. */ export async function resolveLockedReason( @@ -1846,7 +1846,7 @@ export async function handleOpenFile(panel: KanbanWebviewPanel, relPath: string) } /** - * Resolve the gitea PR URL for the current workspace's remote and open it + * Resolve the PR URL for the current workspace's remote and open it * in the user's default browser. Called from the webview's pr-link * button. */ @@ -1949,7 +1949,7 @@ export async function handleGeneratePrDiffSummary(panel: KanbanWebviewPanel, iss return } const profilePath = resolution.profilePath - const prompt = `你在一个 VS Code 扩展启动的后台 Claude 会话中工作。\n\n硬性限制:\n- 不要修改代码。\n- 不要创建或切换分支。\n- 不要提交。\n- 不要 push。\n- 不要写任何文件。\n- 只分析当前 PR 的代码变动,并把 Markdown 摘要正文直接输出到 stdout。\n\n任务:\n- 当前工作目录是工单 #\${issueNumber} 的 worktree。\n- 分析 PR #\${pr} 与主分支的代码差异。\n- 用 git 拿差异:先 git fetch origin main,再 git diff origin/main...HEAD(概览可加 --stat)。注意 tea 没有 diff 子命令,不要尝试 tea pulls diff。\n\n输出必须是合法 Markdown,预览器要能渲染出标题层级与代码块高亮,结构如下:\n1. 第一行是一级标题:# PR #\${pr} 代码变更摘要\n2. 紧接着用 1~2 句话概述本次变更:做了什么、新增了哪些包/模块、关键依赖方向(例如「stockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入」)。\n3. 然后写一行图例:图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n4. 然后是一棵统一的目录树,放进单个围栏代码块(语言标注用 text):树前一行写三个反引号紧跟 text,树后一行写三个反引号。\n\n目录树规则:\n- 用 box-drawing 连线字符画树:├──、└──、│,缩进对齐,像 tree 命令的输出,不要只用纯空格缩进糊一棵没有连线的树。\n- 从仓库根开始保留真实目录层级。\n- 每个文件行以标记开头:新建文件用「+ 」、修改文件用「✎ 」,后接文件名 + 若干空格对齐 + 一句中文目的描述(简短,点出关键符号/端点/职责)。\n- 目录节点行尾可加可选的阶段/分组标签(如 [Phase N]),仅当本次变更本身有清晰阶段时才加;没有就不加,不要硬编。\n- 关键:整棵树一定要写在围栏代码块之内,否则 Markdown 预览器会折叠空白、丢掉层级与连线。\n\n排序与重点:\n- 按架构重要性排列:核心业务/领域代码(实现逻辑、入口、API routes、数据模型、service/repository)靠前。\n- alembic 数据库迁移文件、测试文件不算重点:放在各自模块或整棵树的靠后位置,照常用 +/✎ 标记列出,但不要放在最前、不要强调。\n- 测试文件判定:文件名形如 test_* / *_test.* / *.test.* / *.spec.*,或路径包含 tests/、__tests__/ 目录,或 conftest.py、fixture、mock 文件。\n- 迁移文件判定:路径在 alembic/、migrations/、versions/ 之下的文件。\n\n格式示例(请照此结构输出,不要照抄内容):\n\n# PR #123 代码变更摘要\n\nstockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入。\n\n图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n\n\\\`\\\`\\\`text\nbackend/\n└── src/luffy_agent/commerce/\n ├── models.py ✎ +StockPool;ProductCatalog 加 pool_id\n ├── stockpool/\n │ ├── service.py + reserve_pool_or_raise + compute_pool_views\n │ └── routes.py + /admin/commerce/stock-pools\n ├── orders/service.py ✎ create_order 绑定池分支\n └── alembic/versions/ + stock_pool 建表 + pool_id 加列\n\\\`\\\`\\\`\n\n要求:\n- 每个新增/修改文件一行,一句话说明新增/修改了什么及其目的。\n- 整棵树只用一个围栏代码块,用 box-drawing 连线保留真实目录层级。\n- 只输出上述 Markdown 正文本身,不要写文件,不要任何前导说明/寒暄/结尾总结,第一行就是 # PR #\${pr} 代码变更摘要 标题。` + const prompt = `你在一个 VS Code 扩展启动的后台 Claude 会话中工作。\n\n硬性限制:\n- 不要修改代码。\n- 不要创建或切换分支。\n- 不要提交。\n- 不要 push。\n- 不要写任何文件。\n- 只分析当前 PR 的代码变动,并把 Markdown 摘要正文直接输出到 stdout。\n\n任务:\n- 当前工作目录是工单 #\${issueNumber} 的 worktree。\n- 分析 PR #\${pr} 与主分支的代码差异。\n- 用 git 拿差异:先 git fetch origin main,再 git diff origin/main...HEAD(概览可加 --stat)。差异只用 git 取,不要调用代码托管平台的命令行工具或接口。\n\n输出必须是合法 Markdown,预览器要能渲染出标题层级与代码块高亮,结构如下:\n1. 第一行是一级标题:# PR #\${pr} 代码变更摘要\n2. 紧接着用 1~2 句话概述本次变更:做了什么、新增了哪些包/模块、关键依赖方向(例如「stockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入」)。\n3. 然后写一行图例:图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n4. 然后是一棵统一的目录树,放进单个围栏代码块(语言标注用 text):树前一行写三个反引号紧跟 text,树后一行写三个反引号。\n\n目录树规则:\n- 用 box-drawing 连线字符画树:├──、└──、│,缩进对齐,像 tree 命令的输出,不要只用纯空格缩进糊一棵没有连线的树。\n- 从仓库根开始保留真实目录层级。\n- 每个文件行以标记开头:新建文件用「+ 」、修改文件用「✎ 」,后接文件名 + 若干空格对齐 + 一句中文目的描述(简短,点出关键符号/端点/职责)。\n- 目录节点行尾可加可选的阶段/分组标签(如 [Phase N]),仅当本次变更本身有清晰阶段时才加;没有就不加,不要硬编。\n- 关键:整棵树一定要写在围栏代码块之内,否则 Markdown 预览器会折叠空白、丢掉层级与连线。\n\n排序与重点:\n- 按架构重要性排列:核心业务/领域代码(实现逻辑、入口、API routes、数据模型、service/repository)靠前。\n- alembic 数据库迁移文件、测试文件不算重点:放在各自模块或整棵树的靠后位置,照常用 +/✎ 标记列出,但不要放在最前、不要强调。\n- 测试文件判定:文件名形如 test_* / *_test.* / *.test.* / *.spec.*,或路径包含 tests/、__tests__/ 目录,或 conftest.py、fixture、mock 文件。\n- 迁移文件判定:路径在 alembic/、migrations/、versions/ 之下的文件。\n\n格式示例(请照此结构输出,不要照抄内容):\n\n# PR #123 代码变更摘要\n\nstockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入。\n\n图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n\n\\\`\\\`\\\`text\nbackend/\n└── src/luffy_agent/commerce/\n ├── models.py ✎ +StockPool;ProductCatalog 加 pool_id\n ├── stockpool/\n │ ├── service.py + reserve_pool_or_raise + compute_pool_views\n │ └── routes.py + /admin/commerce/stock-pools\n ├── orders/service.py ✎ create_order 绑定池分支\n └── alembic/versions/ + stock_pool 建表 + pool_id 加列\n\\\`\\\`\\\`\n\n要求:\n- 每个新增/修改文件一行,一句话说明新增/修改了什么及其目的。\n- 整棵树只用一个围栏代码块,用 box-drawing 连线保留真实目录层级。\n- 只输出上述 Markdown 正文本身,不要写文件,不要任何前导说明/寒暄/结尾总结,第一行就是 # PR #\${pr} 代码变更摘要 标题。` const result = await spawnClaude({ prompt, diff --git a/vscode/src/panel/handlers/sessions.ts b/vscode/src/panel/handlers/sessions.ts index a01bc83..461dcdf 100644 --- a/vscode/src/panel/handlers/sessions.ts +++ b/vscode/src/panel/handlers/sessions.ts @@ -61,7 +61,7 @@ function nonEmptyString(value: unknown): string | undefined { } /** - * 会话 id 来自 Gitea 共享状态,别的机器上开的会话本机没有会话文件,resume + * 会话 id 来自工单上的共享状态,别的机器上开的会话本机没有会话文件,resume * 必然报「No conversation found」。各 resume 入口在开新终端前查一次,没有就 * 用这条 toast 拦下(已开着的 tab 直接复用,不受影响)。 */ @@ -100,7 +100,7 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId: try { // Server-side prerequisite gate. Webview already disables the entry // visually (see `isIssueLocked` in the kanban UI), but a user who - // bypasses that — message tampering, editing Gitea directly, etc. — + // bypasses that — message tampering, editing the forge directly, etc. — // would otherwise still get a session. Skip when `issueNumber` is // unknown (legacy sessions) so we don't gate flows that never had a // prerequisite concept. @@ -572,15 +572,14 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: { * 2. Move the issue to in-progress and stamp branch/worktreePath/status * into the state-JSON comment. * 3. Confirm the always-on webhook coordinator is bound to the current - * port, then register a gitea webhook scoped to the feature branch - * and hand the bookkeeping to the coordinator. + * port and hand the bookkeeping to the coordinator. * 4. Spawn `claude` in an editor-tab terminal with the implement prompt that * tells cc to create the worktree, implement the plan, and emit * `$pr_no` when done. * * The webhook coordinator (see src/webhook/coordinator.ts) receives the - * resulting PR event, writes back the PR number, and deletes the gitea - * hook — even if this panel has since been closed. + * resulting PR event and writes back the PR number — even if this panel + * has since been closed. */ export async function handleImplement( panel: KanbanWebviewPanel, @@ -653,7 +652,7 @@ export async function handleImplement( // The webhook coordinator is started at extension activation; just // ensure the port matches the current setting (no-op if unchanged). The - // user must configure a single shared webhook in gitea pointing at + // user must configure a single shared webhook in the repo pointing at // `/webhook`; the extension no longer auto-creates per-issue // hooks, and PR→issue resolution happens via `Closes #N` in the PR body. try { diff --git a/vscode/src/panel/handlers/settings.ts b/vscode/src/panel/handlers/settings.ts index 162439c..f05c2c4 100644 --- a/vscode/src/panel/handlers/settings.ts +++ b/vscode/src/panel/handlers/settings.ts @@ -7,9 +7,11 @@ import * as os from 'node:os' import * as path from 'node:path' import process from 'node:process' import { commands, env, RelativePattern, Uri, workspace } from 'vscode' -import { clearIdentityCache, initIdentity } from '../../auth/identity' +import { clearIdentityCache, initIdentity, loginFor } from '../../auth/identity' import { getToken, getYouTrackToken, setToken, setYouTrackToken } from '../../auth/secrets' import { listClaudeProfiles, setProfilesDirOverride } from '../../cc/profiles' +import { resolveForge } from '../../forge/context' +import { forgeKindForHost } from '../../forge/types' import { detectRepo } from '../../git/remote' import { logger } from '../../logging/logger' import { readProfiles, writeProfiles } from '../../profiles/store' @@ -100,18 +102,23 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { // profiles 目录:空串有意义(= 自动探测),只 trim。 const trimmedProfilesDirectory = payload.profilesDirectory.trim() const prev = getSettings(panel.context) + const forgeKind = forgeKindForHost(trimmedHost) + // GitHub 仓库用 gh 的登录,设置里没有 token 要存。 + const usesToken = forgeKind === 'gitea' // Capture the previous token *for this host* before overwriting it, so // we can decide below whether the kanban needs a re-fetch. (Only host // and token affect the issue list — port/url-prefix/prompts don't.) - const oldToken = trimmedHost ? await getToken(panel.context, trimmedHost) : undefined + const oldToken = usesToken && trimmedHost ? await getToken(panel.context, trimmedHost) : undefined // Empty token + existing saved token = user wants to keep the existing // one (placeholder semantics in the modal). Skip rewriting and skip the // kanban refresh since auth didn't change. const keepExisting = trimmedToken === '' && !!oldToken - if (!trimmedHost || (!trimmedToken && !keepExisting)) { + const tokenChanged = usesToken && !keepExisting + if (usesToken && (!trimmedHost || (!trimmedToken && !keepExisting))) { panel.postMessage({ type: 'settings/show', host: trimmedHost, + forgeKind, errorMessage: 'Host 和 Token 都不能为空', tokenSaved: !!oldToken, webhookPort: payload.webhookPort, @@ -187,14 +194,14 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { youtrackCloseCommand: trimmedYtClose, }) void handleProfilesList(panel) - if (!keepExisting) { + if (tokenChanged) { await setToken(panel.context, trimmedHost, trimmedToken) // 新 token 可能换了人,身份缓存和状态栏都要跟着重解析。 clearIdentityCache(trimmedHost) void initIdentity(panel.context) } // YouTrack token: stored under the base URL's host. Empty input keeps the - // existing token (placeholder semantics), matching the gitea flow above. + // existing token (placeholder semantics), matching the repository token flow above. if (trimmedYtBase && trimmedYtToken) await setYouTrackToken(panel.context, youtrackHost(trimmedYtBase), trimmedYtToken) // Honor a port change without requiring a window reload. Restart the @@ -225,12 +232,12 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: { // actually changed. Saves a round-trip + visible loading flash when the // user just tweaked prompts or webhook settings. `keepExisting` already // guarantees no auth change. - // Re-fetch when the gitea credential OR any YouTrack config changed (base + // Re-fetch when the repository token OR any YouTrack config changed (base // URL / project / a freshly entered token) so newly-mirrored issues appear. const youtrackChanged = prev.youtrackBaseUrl !== trimmedYtBase || prev.youtrackProjectShortName !== trimmedYtProject || trimmedYtToken !== '' - if ((!keepExisting && oldToken !== trimmedToken) || youtrackChanged) + if ((tokenChanged && oldToken !== trimmedToken) || youtrackChanged) await panel.loadAndPush() // 开发分支 / 发布前缀可能变了,刷新工具栏发布按钮。 void panel.handleReleaseStatus() @@ -247,8 +254,10 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom const s = getSettings(panel.context) // 打开设置时同步 override,避免未 activate 路径或旧值漂移。 setProfilesDirOverride(s.profilesDirectory || undefined) - const tok = host ? await getToken(panel.context, host) : undefined + const forgeKind = forgeKindForHost(host) + const tok = forgeKind === 'gitea' && host ? await getToken(panel.context, host) : undefined const tokenSaved = !!tok && tok.length > 0 + const ghLogin = forgeKind === 'github' ? await currentGhLogin(panel) : undefined const ytTok = s.youtrackBaseUrl.trim() ? await getYouTrackToken(panel.context, youtrackHost(s.youtrackBaseUrl.trim())) : undefined @@ -259,6 +268,8 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom panel.postMessage({ type: 'settings/show', host, + forgeKind, + ...(ghLogin ? { ghLogin } : {}), canCancel: true, tokenSaved, webhookPort: s.webhookPort, @@ -300,6 +311,21 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom }) } +/** + * 现查 gh 登录的 GitHub 账号,没登录或登录失效返回 `undefined`。 + * + * 用户可能刚在终端登录或换了账号,所以先丢掉身份缓存再查,顺带刷新状态栏。 + */ +async function currentGhLogin(panel: KanbanWebviewPanel): Promise { + const resolved = await resolveForge(panel.context) + if (!resolved.ok) + return undefined + clearIdentityCache(resolved.forge.host) + const login = await loginFor(resolved.forge) + void initIdentity(panel.context) + return login +} + /** Forces the open panel (if any) into the setup-auth state. */ export function requestEditAuth(panel: KanbanWebviewPanel): void { void handleEditSettingsRequest(panel) diff --git a/vscode/src/panel/handlers/terminals.ts b/vscode/src/panel/handlers/terminals.ts index 8970894..d07657f 100644 --- a/vscode/src/panel/handlers/terminals.ts +++ b/vscode/src/panel/handlers/terminals.ts @@ -202,7 +202,7 @@ export function handleActiveTerminalChanged(panel: KanbanWebviewPanel, terminal: /** * 将当前 VS Code 里仍存活的会话终端同步回 issue 列表。 * - * `issues/update` 会用 Gitea 里持久化的 state JSON 整体替换 webview + * `issues/update` 会用代码托管仓库里持久化的 state JSON 整体替换 webview * 状态;tabOpen 是本地运行态,不在 state JSON 中,因此发送列表前要按 * live terminal 重新补齐,同时登记 terminalOrigin,确保详情面板的关闭 * 按钮能找到对应 terminal。 diff --git a/vscode/src/panel/handlers/youtrackIssues.ts b/vscode/src/panel/handlers/youtrackIssues.ts index 499cc38..dc6d8c6 100644 --- a/vscode/src/panel/handlers/youtrackIssues.ts +++ b/vscode/src/panel/handlers/youtrackIssues.ts @@ -1,7 +1,7 @@ /** * Panel handlers for YouTrack-sourced cards. * - * Kept separate from the gitea `issues.ts` handlers: these route workflow-state + * Kept separate from the forge `issues.ts` handlers: these route workflow-state * writes to the issue's YouTrack comment (see `youtrack/stateComment.ts`) and * resolve the issue in YouTrack when its card lands in the 完成 column — the * only write-back the user asked for. diff --git a/vscode/src/panel/issueOwnership.ts b/vscode/src/panel/issueOwnership.ts index d50127c..8974f63 100644 --- a/vscode/src/panel/issueOwnership.ts +++ b/vscode/src/panel/issueOwnership.ts @@ -8,9 +8,9 @@ */ export interface IssueOwnershipFacts { - /** Gitea 工单创建者 login;YouTrack 工单没有该字段。 */ + /** 代码托管工单的创建者 login;YouTrack 工单没有该字段。 */ poster?: string - /** Gitea assignee login 列表;空/缺省表示未指派。 */ + /** 代码托管工单的 assignee login 列表;空/缺省表示未指派。 */ assignees?: string[] } @@ -21,7 +21,7 @@ export function isIssueMine(facts: IssueOwnershipFacts, me: string | undefined): const assignees = facts.assignees ?? [] if (assignees.length > 0) return assignees.includes(me) - // 无 assignee:创建者说了算;poster 缺省(非 Gitea 来源)不参与判定。 + // 无 assignee:创建者说了算;poster 缺省(YouTrack 来源)不参与判定。 return facts.poster === undefined || facts.poster === me } diff --git a/vscode/src/panel/kanbanSnapshot.ts b/vscode/src/panel/kanbanSnapshot.ts index 101d183..c54cb01 100644 --- a/vscode/src/panel/kanbanSnapshot.ts +++ b/vscode/src/panel/kanbanSnapshot.ts @@ -6,7 +6,7 @@ export interface KanbanSnapshot { issues: Issue[] scope: 'mine' | 'all' me?: string - /** Gitea login → 头像 data URI;团队视图下卡片按人贴头像。 */ + /** 代码托管 login → 头像 data URI;团队视图下卡片按人贴头像。 */ avatars?: Record globalAutoReview: boolean youtrackConfigured: boolean diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index c8f00d8..c4edc8d 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -1,3 +1,4 @@ +import type { ForgeKind } from '../forge/types' import type { ReleaseCommitGroup } from '../git/release' import type { Issue, IssueColumn } from '../issues/types' import type { LogEntry } from '../logging/logger' @@ -68,6 +69,10 @@ export type ExtensionToWebview | { type: 'settings/show' host: string + /** 当前工作区仓库是哪一家,决定设置里显示 Token 输入框还是 gh 登录状态。 */ + forgeKind: ForgeKind + /** gh 当前登录的 GitHub 账号;没登录或登录失效时没有。 */ + ghLogin?: string errorMessage?: string canCancel?: boolean tokenSaved: boolean diff --git a/vscode/src/panel/readOnlyGuard.ts b/vscode/src/panel/readOnlyGuard.ts index b3c9a5d..9cce6e3 100644 --- a/vscode/src/panel/readOnlyGuard.ts +++ b/vscode/src/panel/readOnlyGuard.ts @@ -21,7 +21,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet = new Set([ 'session/close-tab', 'brainstorm/continue', 'issue/implement', - // 工单状态:写回 Gitea + // 工单状态:写回代码托管仓库 'column/change', 'issue/delete', 'issue/close', diff --git a/vscode/src/sessions/prReviewStore.ts b/vscode/src/sessions/prReviewStore.ts index 972d79b..612d632 100644 --- a/vscode/src/sessions/prReviewStore.ts +++ b/vscode/src/sessions/prReviewStore.ts @@ -6,8 +6,8 @@ * `{ [filePath]: sig }`,sig 是该文件被标记已查看时记下的「变更指纹」。 * * 为什么用指纹而非 blob sha:要做到 GitHub 式「文件没变就一直算已查看,只有它在新 - * push 里真改动了才重新标未查看」,需要一个能判定文件是否变过的标识。但 Gitea 的 - * ChangedFile 不返回每文件 blob sha、compare 端点也不支持任意 commit sha 比较,拿 + * push 里真改动了才重新标未查看」,需要一个能判定文件是否变过的标识。但 + * [PR 改动文件](vscode/src/forge/types.ts#ForgePrFile) 里没有每个文件的 blob sha(Gitea 的接口不给),拿 * 不到现成的内容标识。故退而用 (status|additions|deletions) 作指纹——文件一旦改动, * 这三者几乎必然变其一,据此判断是否需要重看,够用且零额外请求。 * diff --git a/vscode/src/sessions/sessionFiles.ts b/vscode/src/sessions/sessionFiles.ts index edc9e26..f0ac2ca 100644 --- a/vscode/src/sessions/sessionFiles.ts +++ b/vscode/src/sessions/sessionFiles.ts @@ -1,7 +1,7 @@ /** * 枚举本机 claude / codex 会话文件,判定工单上的会话 id 能否 resume。 * - * 会话 id 存在 Gitea 共享状态里,别的机器上开的会话本机没有 transcript, + * 会话 id 存在工单的共享状态里,别的机器上开的会话本机没有 transcript, * resume 必然失败;多人协作换机后只要把会话文件拷到本机就能 resume。 * * - claude:`~/.claude/projects/<编码后的 cwd>/.jsonl`。新版 `claude --resume` diff --git a/vscode/src/youtrack/api.ts b/vscode/src/youtrack/api.ts index 47a3000..d014bc3 100644 --- a/vscode/src/youtrack/api.ts +++ b/vscode/src/youtrack/api.ts @@ -1,7 +1,7 @@ /** * Fetch-based YouTrack REST client. * - * Mirrors the role of the [Gitea provider](vscode/src/forge/gitea/provider.ts) for the second issue source. All + * Mirrors the role of a [forge provider](vscode/src/forge/types.ts#ForgeProvider) for the second issue source. All * requests authenticate with a YouTrack permanent token via * `Authorization: Bearer `. Non-2xx responses surface as * `YouTrackApiError` so callers can distinguish 401 (token invalid) from other diff --git a/vscode/src/youtrack/issueLoader.ts b/vscode/src/youtrack/issueLoader.ts index 9a6f83b..15542ba 100644 --- a/vscode/src/youtrack/issueLoader.ts +++ b/vscode/src/youtrack/issueLoader.ts @@ -29,9 +29,9 @@ export function youtrackHost(baseUrl: string): string { /** * Offset added to a YouTrack issue's numeric id so its board `number` never - * collides with a gitea issue number (which is the routing key for + * collides with a forge issue number (which is the routing key for * webview↔extension messages and the `number → IssueRef` map). - * ponytail: assumes gitea numbers < 1,000,000 and a single mirrored project + * ponytail: assumes forge issue numbers < 1,000,000 and a single mirrored project * (readable ids are unique within one project). Widen / hash if either breaks. */ const YOUTRACK_NUMBER_OFFSET = 1_000_000 diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index 9a1da04..5b6f3cd 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -578,6 +578,8 @@ export function App() { void onOpenLogs: () => void me?: string - /** Gitea login → 头像 data URI。 */ + /** login → 头像 data URI。 */ avatars?: Record onStartHandoff: (issueNumber: number) => void onAcceptHandoff: (issueNumber: number) => void diff --git a/vscode/webview-ui/src/components/IssueCard.tsx b/vscode/webview-ui/src/components/IssueCard.tsx index 567953a..a418017 100644 --- a/vscode/webview-ui/src/components/IssueCard.tsx +++ b/vscode/webview-ui/src/components/IssueCard.tsx @@ -22,7 +22,7 @@ interface IssueCardProps { lockedPrerequisite?: number /** 只读态:同事的工单。禁止拖拽,并在右上角贴归属人头像。 */ readOnly?: boolean - /** 归属人 Gitea login(只读卡片才显示)。 */ + /** 归属人 login(只读卡片才显示)。 */ ownerLogin?: string /** 归属人头像 data URI;缺失时回退首字母圆点。 */ ownerAvatar?: string diff --git a/vscode/webview-ui/src/components/IssueChatPanel.tsx b/vscode/webview-ui/src/components/IssueChatPanel.tsx index d37a388..ba4e1d8 100644 --- a/vscode/webview-ui/src/components/IssueChatPanel.tsx +++ b/vscode/webview-ui/src/components/IssueChatPanel.tsx @@ -4,6 +4,7 @@ import type { ParsedCommentQuote } from './commentQuote' import { ExternalLink, Loader2, MessageCircle, Quote, RotateCw, Send, X } from 'lucide-react' import { useEffect, useRef, useState } from 'react' +import { trackerName } from '../lib/trackerName' import { onMessage, postMessage } from '../lib/vscode' import { parseCommentQuote } from './commentQuote' @@ -297,8 +298,8 @@ export function IssueChatPanel({ )} - {/* Close/delete write to gitea by number — hide for youtrack cards to - avoid acting on a gitea issue of the same number. */} + {/* Close/delete write to the forge by number — hide for youtrack cards to + avoid acting on a forge issue of the same number. */} {issue.source !== 'youtrack' && !readOnly && ( <> {issue.handoffBundleKey && me !== undefined && (issue.assignees ?? []).includes(me) && ( @@ -964,7 +965,7 @@ export function IssueDetailPanel({ )}