✨ feat(vscode): 设置和看板按仓库类型区分 GitHub 与 Gitea,GitHub 显示 gh 登录状态,文案去 Gitea 化
This commit is contained in:
1 parent
ac3c882672
commit
ae06c8e5d1
42 files changed
+443
-195
No files matched your search
@@ -578,6 +578,8 @@ export function App() {
|
||||
<SettingsModal
|
||||
open={settings !== null}
|
||||
host={settings?.host ?? ''}
|
||||
forgeKind={settings?.forgeKind ?? 'gitea'}
|
||||
ghLogin={settings?.ghLogin}
|
||||
errorMessage={settings?.errorMessage}
|
||||
canCancel={settings?.canCancel}
|
||||
initialTokenSaved={settings?.tokenSaved ?? false}
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface BottomTabsProps {
|
||||
onUpdatePrototypeProfilePath: (issueNumber: number, prototypeProfilePath: string) => void
|
||||
onOpenLogs: () => void
|
||||
me?: string
|
||||
/** Gitea login → 头像 data URI。 */
|
||||
/** login → 头像 data URI。 */
|
||||
avatars?: Record<string, string>
|
||||
onStartHandoff: (issueNumber: number) => void
|
||||
onAcceptHandoff: (issueNumber: number) => void
|
||||
|
||||
@@ -22,7 +22,7 @@ interface IssueCardProps {
|
||||
lockedPrerequisite?: number
|
||||
/** 只读态:同事的工单。禁止拖拽,并在右上角贴归属人头像。 */
|
||||
readOnly?: boolean
|
||||
/** 归属人 Gitea login(只读卡片才显示)。 */
|
||||
/** 归属人 login(只读卡片才显示)。 */
|
||||
ownerLogin?: string
|
||||
/** 归属人头像 data URI;缺失时回退首字母圆点。 */
|
||||
ownerAvatar?: string
|
||||
|
||||
@@ -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({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-tooltip={issue.source === 'youtrack' ? '在 YouTrack 打开' : '在 Gitea 打开'}
|
||||
aria-label={issue.source === 'youtrack' ? '在 YouTrack 打开' : '在 Gitea 打开'}
|
||||
data-tooltip={`在 ${trackerName(issue)} 打开`}
|
||||
aria-label={`在 ${trackerName(issue)} 打开`}
|
||||
onClick={() => onOpenInBrowser(issue.htmlUrl)}
|
||||
className="inline-flex shrink-0 items-center justify-center rounded border border-[var(--vscode-panel-border)] p-1 text-xs hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
@@ -312,7 +313,11 @@ export function IssueChatPanel({
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center gap-2 p-6 text-[var(--vscode-descriptionForeground)]">
|
||||
<MessageCircle className="size-8 opacity-40" />
|
||||
<span className="text-sm font-medium">还没有讨论</span>
|
||||
<span className="text-xs opacity-60">在此发起对此工单的讨论,评论将双向同步至 Gitea</span>
|
||||
<span className="text-xs opacity-60">
|
||||
在此发起对此工单的讨论,评论将双向同步至
|
||||
{' '}
|
||||
{trackerName(issue)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{comments.map((comment) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { isIssueLocked } from '../lib/dependencies'
|
||||
import { isIssueMine, issueOwnerLogin } from '../lib/issueOwnership'
|
||||
import { isSessionMissing } from '../lib/missingSession'
|
||||
import { toReadOnlySchema } from '../lib/readOnlySchema'
|
||||
import { trackerName } from '../lib/trackerName'
|
||||
import { COLUMN_LABELS, COLUMN_ORDER } from '../types'
|
||||
import { PropertyGrid } from './property-grid'
|
||||
|
||||
@@ -80,7 +81,7 @@ interface IssueDetailPanelProps {
|
||||
onOpenPrototypeInFigma: (issueNumber: number, path: string) => void
|
||||
/** Kick off the end-to-end implementation flow for the given plan file. */
|
||||
onImplement: (issueNumber: number, planFile: string, profilePath?: string, sessionId?: string) => void
|
||||
/** Open the gitea PR page in the browser. */
|
||||
/** Open the PR page in the browser. */
|
||||
onOpenPr: (pr: string) => void
|
||||
/** Start a background Claude run that writes the PR diff summary markdown. */
|
||||
onGeneratePrDiffSummary: (issueNumber: number) => void
|
||||
@@ -100,7 +101,7 @@ interface IssueDetailPanelProps {
|
||||
* 顶部垃圾桶按钮触发,扩展端做 modal confirm,可选。
|
||||
*/
|
||||
onDeleteIssue?: (issueNumber: number) => void
|
||||
/** 关闭 Gitea 工单,不清理本地会话、worktree、PR 或分支。 */
|
||||
/** 关闭代码托管仓库上的工单,不清理本地会话、worktree、PR 或分支。 */
|
||||
onCloseIssue: (issueNumber: number) => void
|
||||
/** 将实施中 / 审查中的工单重置回待办(清 worktree / 未合并 PR / feature 分支)。 */
|
||||
onResetToTodo: (issueNumber: number) => void
|
||||
@@ -133,7 +134,7 @@ interface IssueDetailPanelProps {
|
||||
onUpdatePrototypeProfilePath: (issueNumber: number, prototypeProfilePath: string) => void
|
||||
/** Open the in-webview log modal. */
|
||||
onOpenLogs: () => void
|
||||
/** 当前 Gitea login;undefined = 身份未知(接管按钮不显示)。 */
|
||||
/** 当前登录者的 login;undefined = 身份未知(接管按钮不显示)。 */
|
||||
me?: string
|
||||
onStartHandoff: (issueNumber: number) => void
|
||||
onAcceptHandoff: (issueNumber: number) => void
|
||||
@@ -142,14 +143,14 @@ interface IssueDetailPanelProps {
|
||||
onAcceptHandoffRequest: (issueNumber: number) => void
|
||||
onRejectHandoffRequest: (issueNumber: number) => void
|
||||
isHandoffRunning: (issueNumber: number) => boolean
|
||||
/** Gitea login → 头像 data URI;只读工单在标题旁贴归属人头像。 */
|
||||
/** login → 头像 data URI;只读工单在标题旁贴归属人头像。 */
|
||||
avatars?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* 底部详情面板:以 PropertyGrid 形式展示当前选中 issue 的 state JSON。
|
||||
*
|
||||
* 顶部一行是 #编号/标题 + 「在 Gitea 打开」按钮;下面是 PropertyGrid
|
||||
* 顶部一行是 #编号/标题 + 「在 GitHub / Gitea / YouTrack 打开」按钮;下面是 PropertyGrid
|
||||
* 渲染的 state JSON 字段(当前为 column + sessionId)。sessionId 是
|
||||
* 一个 action 类型字段,双击或选中卡按 Enter 都会触发
|
||||
* `claude --resume <id>` 在新终端中恢复对话。
|
||||
@@ -860,8 +861,8 @@ export function IssueDetailPanel({
|
||||
: null}
|
||||
<button
|
||||
type="button"
|
||||
data-tooltip={issue.source === 'youtrack' ? '在 YouTrack 打开' : '在 Gitea 打开'}
|
||||
aria-label={issue.source === 'youtrack' ? '在 YouTrack 打开' : '在 Gitea 打开'}
|
||||
data-tooltip={`在 ${trackerName(issue)} 打开`}
|
||||
aria-label={`在 ${trackerName(issue)} 打开`}
|
||||
onClick={() => onOpenInBrowser(issue.htmlUrl)}
|
||||
className="inline-flex shrink-0 items-center justify-center rounded border border-[var(--vscode-panel-border)] p-1 text-xs hover:bg-black/10 dark:hover:bg-white/10"
|
||||
>
|
||||
@@ -914,8 +915,8 @@ export function IssueDetailPanel({
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{/* 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({
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
data-tooltip="关闭工单(Gitea 里可重开)"
|
||||
data-tooltip={`关闭工单(可在 ${trackerName(issue)} 重开)`}
|
||||
onClick={() => onCloseIssue(issue.number)}
|
||||
aria-label="关闭工单"
|
||||
className="inline-flex shrink-0 items-center justify-center rounded border border-[var(--vscode-panel-border)] p-1 text-xs text-[var(--vscode-foreground)] hover:border-yellow-500/60 hover:bg-yellow-500/10 hover:text-yellow-500"
|
||||
|
||||
@@ -42,9 +42,9 @@ interface KanbanBoardProps {
|
||||
onDependencySet?: (issueNumber: number, prerequisiteNumber: number) => void
|
||||
onDependencyClear?: (issueNumber: number, prerequisiteNumber: number) => void
|
||||
onSetReviewPassed?: (issueNumber: number, value: boolean) => void
|
||||
/** 当前 Gitea login;用于判定哪些卡是同事的(只读)。 */
|
||||
/** 当前登录者的 login;用于判定哪些卡是同事的(只读)。 */
|
||||
me?: string
|
||||
/** Gitea login → 头像 data URI。 */
|
||||
/** login → 头像 data URI。 */
|
||||
avatars?: Record<string, string>
|
||||
}
|
||||
|
||||
|
||||
@@ -254,8 +254,8 @@ export function PanelHeader({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEditAuth}
|
||||
data-tooltip="重新配置 Gitea Token"
|
||||
aria-label="重新配置 Gitea Token"
|
||||
data-tooltip="设置"
|
||||
aria-label="设置"
|
||||
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)]"
|
||||
>
|
||||
<Settings size={14} />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 「改动」tab(底部第四个):整个 PR 的改动以主从式呈现。
|
||||
*
|
||||
* - 左栏:统计条(共 N 文件 · +X −Y 为全量口径,含 tests/,与 Gitea 网页一致;已查看 M/N
|
||||
* - 左栏:统计条(共 N 文件 · +X −Y 为全量口径,含 tests/,与代码托管网页一致;已查看 M/N
|
||||
* 的分母是可见文件数)+ 文件树(目录树、状态徽标、tests/ 过滤、已查看勾)。
|
||||
* - 右栏:选中文件的内联 diff(git-diff-view,只渲染当前一个文件)。
|
||||
*
|
||||
* 自带 usePrFiles hook,直接经 lib/vscode 收发消息,不依赖上层回调。diff 左右两侧用
|
||||
* merge_base → head 的原文(与 Gitea 网页 diff 口径一致),已查看态按文件变更指纹落盘。
|
||||
* merge_base → head 的原文(与代码托管网页的 PR diff 口径一致),已查看态按文件变更指纹落盘。
|
||||
* 统一/分栏模式按 workspace 持久化,与当前工单无关。
|
||||
*/
|
||||
|
||||
@@ -87,7 +87,7 @@ export function PrFilesPanel({ issue }: PrFilesPanelProps) {
|
||||
// 路径 → 文件,选中时取 status/previousFilename。
|
||||
const fileByPath = useMemo(() => new Map(visibleFiles.map(f => [f.path, f])), [visibleFiles])
|
||||
|
||||
// total/±行数为全量口径(含 tests/,与 Gitea 网页一致);已查看的分母是可见文件数。
|
||||
// total/±行数为全量口径(含 tests/,与代码托管网页一致);已查看的分母是可见文件数。
|
||||
const stats = useMemo(() => {
|
||||
let additions = 0
|
||||
let deletions = 0
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { SettingsModalProps } from './SettingsModal'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SettingsModal } from './SettingsModal'
|
||||
import { SettingsModal, WebhookSecretHint, WebhookUrlHint } from './SettingsModal'
|
||||
|
||||
const PROPS: SettingsModalProps = {
|
||||
open: true,
|
||||
host: 'gitea.example.com',
|
||||
forgeKind: 'gitea',
|
||||
initialTokenSaved: true,
|
||||
initialWebhookPort: 8080,
|
||||
initialWebhookSecret: '',
|
||||
@@ -92,3 +93,51 @@ describe('设置面板的重写注释默认模型', () => {
|
||||
expect(rewriteField({ initialDefaultRewriteProfilePath: '/p/deleted.json' })).toContain('不可用:/p/deleted.json')
|
||||
})
|
||||
})
|
||||
|
||||
describe('设置面板的仓库连接', () => {
|
||||
const render = (props: Partial<SettingsModalProps>): string => renderToStaticMarkup(<SettingsModal {...PROPS} {...props} />)
|
||||
|
||||
it('gitea 仓库照旧填 Host 和 Token', () => {
|
||||
const html = render({})
|
||||
expect(html).toContain('Gitea Host')
|
||||
expect(html).toContain('Personal Access Token')
|
||||
})
|
||||
|
||||
it('gitHub 仓库不显示 Host、Token 输入框,显示 gh 已登录的账号', () => {
|
||||
const html = render({ host: 'github.com', forgeKind: 'github', ghLogin: 'cruldra', initialTokenSaved: false })
|
||||
expect(html).not.toContain('Gitea Host')
|
||||
expect(html).not.toContain('Personal Access Token')
|
||||
expect(html).not.toContain('type="password"')
|
||||
expect(html).toContain('已登录为')
|
||||
expect(html).toContain('cruldra')
|
||||
})
|
||||
|
||||
it('gitHub 仓库 gh 没登录时给出登录指引', () => {
|
||||
const html = render({ host: 'github.com', forgeKind: 'github', initialTokenSaved: false })
|
||||
expect(html).toContain('未登录')
|
||||
expect(html).toContain('gh auth login')
|
||||
})
|
||||
})
|
||||
|
||||
describe('webhook 配置说明', () => {
|
||||
it('gitHub:仓库 Settings → Webhooks,公网地址加 /webhook,JSON,勾四类事件', () => {
|
||||
const html = renderToStaticMarkup(<WebhookUrlHint kind="github" />)
|
||||
expect(html).toContain('Settings → Webhooks')
|
||||
expect(html).toContain('/webhook')
|
||||
expect(html).toContain('application/json')
|
||||
for (const event of ['Issues', 'Issue comments', 'Pull requests', 'Pushes'])
|
||||
expect(html).toContain(event)
|
||||
})
|
||||
|
||||
it('gitea 的说明里不出现 GitHub 的设置路径', () => {
|
||||
const html = renderToStaticMarkup(<WebhookUrlHint kind="gitea" />)
|
||||
expect(html).toContain('Gitea')
|
||||
expect(html).not.toContain('Settings → Webhooks')
|
||||
})
|
||||
|
||||
it.each(['gitea', 'github'] as const)('%s 的 Secret 说明都按 X-Hub-Signature-256 校验', (kind) => {
|
||||
const html = renderToStaticMarkup(<WebhookSecretHint kind={kind} />)
|
||||
expect(html).toContain('X-Hub-Signature-256')
|
||||
expect(html).not.toContain('X-Gitea-Signature')
|
||||
})
|
||||
})
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import type { ReactElement } from 'react'
|
||||
import type { ForgeKind } from '../../../src/forge/types'
|
||||
import type { ClaudeProfile } from '../hooks/useIssues'
|
||||
import { X } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -72,6 +73,10 @@ export interface YouTrackProjectsState {
|
||||
export interface SettingsModalProps {
|
||||
open: boolean
|
||||
host: string
|
||||
/** 当前工作区仓库是哪一家:GitHub 用 gh 的登录,不填 Host 和 Token。 */
|
||||
forgeKind: ForgeKind
|
||||
/** gh 当前登录的 GitHub 账号;没登录或登录失效时没有。只在 GitHub 仓库下有意义。 */
|
||||
ghLogin?: string
|
||||
errorMessage?: string
|
||||
canCancel?: boolean
|
||||
initialTokenSaved: boolean
|
||||
@@ -163,9 +168,121 @@ const FIELD_TO_GROUP: Record<ErrorKey, GroupKey> = {
|
||||
webhookPort: 'network',
|
||||
}
|
||||
|
||||
function GhLoginStatus({ login }: { login?: string }): ReactElement {
|
||||
return (
|
||||
<div className="mb-3 text-xs">
|
||||
<span className="mb-1 block opacity-80">GitHub 账号</span>
|
||||
{login
|
||||
? (
|
||||
<span>
|
||||
已登录为
|
||||
{' '}
|
||||
<b>{login}</b>
|
||||
</span>
|
||||
)
|
||||
: (
|
||||
<span className="text-state-red">
|
||||
未登录或登录已失效:请在终端运行
|
||||
{' '}
|
||||
<code>gh auth login</code>
|
||||
{' '}
|
||||
登录 github.com,然后回到看板点刷新
|
||||
</span>
|
||||
)}
|
||||
<span className="mt-1 block text-[10px] opacity-60">
|
||||
GitHub 仓库直接用 GitHub CLI(gh)的登录,插件不另存 token;换账号用
|
||||
{' '}
|
||||
<code>gh auth switch</code>
|
||||
。
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function WebhookUrlHint({ kind }: { kind: ForgeKind }): ReactElement {
|
||||
if (kind === 'github') {
|
||||
return (
|
||||
<>
|
||||
扩展 HTTP server 绑定的端口。GitHub 只能访问公网地址,先用隧道把公网地址转到此端口,再到 GitHub 仓库
|
||||
{' '}
|
||||
<b>Settings → Webhooks</b>
|
||||
{' '}
|
||||
添加 webhook:
|
||||
<b>Payload URL</b>
|
||||
{' '}
|
||||
填
|
||||
{' '}
|
||||
<code>{'https://<公网地址>/webhook'}</code>
|
||||
,
|
||||
<b>Content type</b>
|
||||
{' '}
|
||||
选
|
||||
{' '}
|
||||
<code>application/json</code>
|
||||
,事件选「Let me select individual events」并勾选
|
||||
{' '}
|
||||
<b>Issues</b>
|
||||
、
|
||||
<b>Issue comments</b>
|
||||
、
|
||||
<b>Pull requests</b>
|
||||
、
|
||||
<b>Pushes</b>
|
||||
。
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
扩展 HTTP server 绑定的端口。在 Gitea 仓库 Webhooks 配置里把目标 URL 指向
|
||||
{' '}
|
||||
<code>{'http://<你的本机/公网域名>:<此端口>/webhook'}</code>
|
||||
,事件需勾选:
|
||||
<b>工单</b>
|
||||
(issue opened/edited)、
|
||||
<b>合并请求</b>
|
||||
(PR opened/synchronize/closed)、
|
||||
<b>工单评论</b>
|
||||
(codex 审查回流)。
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function WebhookSecretHint({ kind }: { kind: ForgeKind }): ReactElement {
|
||||
return (
|
||||
<>
|
||||
与
|
||||
{kind === 'github'
|
||||
? (
|
||||
<>
|
||||
{' '}
|
||||
GitHub webhook 里填的
|
||||
{' '}
|
||||
<b>Secret</b>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{' '}
|
||||
Gitea Webhooks 配置里的
|
||||
{' '}
|
||||
<b>密钥文本</b>
|
||||
</>
|
||||
)}
|
||||
{' '}
|
||||
保持一致,用于校验
|
||||
{' '}
|
||||
<code>X-Hub-Signature-256</code>
|
||||
。webhook 走公网隧道时必填,否则任何人都能伪造事件;留空 = 不校验。
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsModal({
|
||||
open,
|
||||
host: initialHost,
|
||||
forgeKind,
|
||||
ghLogin,
|
||||
errorMessage,
|
||||
canCancel,
|
||||
initialTokenSaved,
|
||||
@@ -326,12 +443,14 @@ export function SettingsModal({
|
||||
event.preventDefault()
|
||||
const nextErrors: Partial<Record<ErrorKey, string>> = {}
|
||||
const trimmedToken = token.trim()
|
||||
if (!trimmedHost)
|
||||
// GitHub 仓库用 gh 的登录,没有 Host 和 Token 要填。
|
||||
const needsToken = forgeKind === 'gitea'
|
||||
if (needsToken && !trimmedHost)
|
||||
nextErrors.host = 'Host 不能为空'
|
||||
// When a token is already saved server-side, leaving the field blank
|
||||
// means "keep the existing token" — don't flag it as an error. Only
|
||||
// require a new token when nothing is saved yet.
|
||||
if (!trimmedToken && !initialTokenSaved)
|
||||
if (needsToken && !trimmedToken && !initialTokenSaved)
|
||||
nextErrors.token = 'Token 不能为空'
|
||||
const portNum = Number.parseInt(webhookPort, 10)
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
|
||||
@@ -399,7 +518,7 @@ export function SettingsModal({
|
||||
youtrackBaseUrl: youtrackBaseUrl.trim(),
|
||||
youtrackProjectShortName: youtrackProjectShortName.trim(),
|
||||
youtrackCloseCommand: youtrackCloseCommand.trim(),
|
||||
// Raw (may be empty = keep existing), mirroring the gitea token rule.
|
||||
// Raw (may be empty = keep existing), mirroring the repository token rule.
|
||||
youtrackToken: youtrackToken.trim(),
|
||||
})
|
||||
}
|
||||
@@ -461,54 +580,60 @@ export function SettingsModal({
|
||||
|
||||
{activeGroup === 'auth' && (
|
||||
<>
|
||||
<Field label="Gitea Host" error={errors.host}>
|
||||
<input
|
||||
type="text"
|
||||
value={host}
|
||||
onChange={e => setHost(e.target.value)}
|
||||
placeholder="gitea.example.com"
|
||||
className={inputClass}
|
||||
/>
|
||||
</Field>
|
||||
{forgeKind === 'github'
|
||||
? <GhLoginStatus login={ghLogin} />
|
||||
: (
|
||||
<>
|
||||
<Field label="Gitea Host" error={errors.host}>
|
||||
<input
|
||||
type="text"
|
||||
value={host}
|
||||
onChange={e => setHost(e.target.value)}
|
||||
placeholder="gitea.example.com"
|
||||
className={inputClass}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label="Personal Access Token"
|
||||
error={errors.token}
|
||||
hint={(
|
||||
<>
|
||||
在
|
||||
{' '}
|
||||
{tokenPageUrl
|
||||
? (
|
||||
<a
|
||||
href={tokenPageUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-[var(--vscode-textLink-foreground)] underline"
|
||||
>
|
||||
{tokenPageUrl}
|
||||
</a>
|
||||
)
|
||||
: <span className="opacity-60">填写 Host 后这里会显示生成 token 的链接</span>}
|
||||
{' '}
|
||||
生成一个 token
|
||||
{initialTokenSaved && (
|
||||
<>
|
||||
<br />
|
||||
已保存,留空则保留
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={e => setToken(e.target.value)}
|
||||
placeholder={initialTokenSaved ? '••••••••' : '例如 1a2b3c4d…'}
|
||||
className={inputClass}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Personal Access Token"
|
||||
error={errors.token}
|
||||
hint={(
|
||||
<>
|
||||
在
|
||||
{' '}
|
||||
{tokenPageUrl
|
||||
? (
|
||||
<a
|
||||
href={tokenPageUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-[var(--vscode-textLink-foreground)] underline"
|
||||
>
|
||||
{tokenPageUrl}
|
||||
</a>
|
||||
)
|
||||
: <span className="opacity-60">填写 Host 后这里会显示生成 token 的链接</span>}
|
||||
{' '}
|
||||
生成一个 token
|
||||
{initialTokenSaved && (
|
||||
<>
|
||||
<br />
|
||||
已保存,留空则保留
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
value={token}
|
||||
onChange={e => setToken(e.target.value)}
|
||||
placeholder={initialTokenSaved ? '••••••••' : '例如 1a2b3c4d…'}
|
||||
className={inputClass}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label="开发分支"
|
||||
@@ -830,20 +955,7 @@ export function SettingsModal({
|
||||
<Field
|
||||
label="本地端口"
|
||||
error={errors.webhookPort}
|
||||
hint={(
|
||||
<>
|
||||
扩展 HTTP server 绑定的端口。在 gitea 仓库 Webhooks 配置里把目标 URL 指向
|
||||
{' '}
|
||||
<code>{'http://<你的本机/公网域名>:<此端口>/webhook'}</code>
|
||||
,事件需勾选:
|
||||
<b>工单</b>
|
||||
(issue opened/edited)、
|
||||
<b>合并请求</b>
|
||||
(PR opened/synchronize/closed)、
|
||||
<b>工单评论</b>
|
||||
(codex 审查回流)。
|
||||
</>
|
||||
)}
|
||||
hint={<WebhookUrlHint kind={forgeKind} />}
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
@@ -857,18 +969,7 @@ export function SettingsModal({
|
||||
|
||||
<Field
|
||||
label="Webhook Secret"
|
||||
hint={(
|
||||
<>
|
||||
与 gitea Webhooks 配置里的
|
||||
{' '}
|
||||
<b>密钥文本</b>
|
||||
{' '}
|
||||
保持一致,用于校验
|
||||
{' '}
|
||||
<code>X-Gitea-Signature</code>
|
||||
。webhook 走公网隧道时必填,否则任何人都能伪造事件;留空 = 不校验。
|
||||
</>
|
||||
)}
|
||||
hint={<WebhookSecretHint kind={forgeKind} />}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
* usual "加载中…".
|
||||
*
|
||||
* `setIssues` is exposed so the Kanban board can reorder cards locally
|
||||
* after a drag. Persisting drag moves back to Gitea is step 3+.
|
||||
* after a drag. Persisting drag moves back to the forge is step 3+.
|
||||
*/
|
||||
|
||||
import type { ForgeKind } from '../../../src/forge/types'
|
||||
import type { ToastItem } from '../components/ToastStack'
|
||||
import type { ExtensionToWebview, LogEntry, ReleaseCommitGroup } from '../lib/messages'
|
||||
import type { Issue, IssueColumn } from '../types'
|
||||
@@ -68,6 +69,8 @@ export interface SettingsValues {
|
||||
|
||||
export interface SettingsOverlayState {
|
||||
host: string
|
||||
forgeKind: ForgeKind
|
||||
ghLogin?: string
|
||||
errorMessage?: string
|
||||
canCancel: boolean
|
||||
tokenSaved: boolean
|
||||
@@ -143,7 +146,7 @@ export interface UseIssuesResult {
|
||||
/** 看板范围:'mine' 只看我的(assigned/created),'all' 团队全部。 */
|
||||
scope: 'mine' | 'all'
|
||||
toggleScope: () => void
|
||||
/** Gitea login → 头像 data URI;同事工单的卡片按它贴头像。 */
|
||||
/** login → 头像 data URI;同事工单的卡片按它贴头像。 */
|
||||
avatars: Record<string, string>
|
||||
/**
|
||||
* Latest known global `autoReview` value pushed alongside `issues/update`.
|
||||
@@ -204,12 +207,12 @@ export interface UseIssuesResult {
|
||||
deleteWorktree: (issueNumber: number, path: string) => void
|
||||
mergeBranch: (issueNumber: number, branch: string) => void
|
||||
/**
|
||||
* 硬删 Gitea 工单及关联资源(worktree / PR / feature branch / cc tabs)。
|
||||
* 硬删代码托管仓库上的工单及关联资源(worktree / PR / feature branch / cc tabs)。
|
||||
* 用户在详情面板顶部点垃圾桶按钮触发。扩展端做 modal confirm + 串行清理,
|
||||
* 全部成功后 push `issue/remove`,webview 将该 issue 从 issues 数组移除。
|
||||
*/
|
||||
deleteIssue: (issueNumber: number) => void
|
||||
/** 关闭 Gitea 工单,不清理本地会话、worktree、PR 或分支。 */
|
||||
/** 关闭代码托管仓库上的工单,不清理本地会话、worktree、PR 或分支。 */
|
||||
closeIssue: (issueNumber: number) => void
|
||||
/**
|
||||
* Dispose the matching session terminal tab. Extension reacts via
|
||||
@@ -309,7 +312,7 @@ export interface UseIssuesResult {
|
||||
clearPendingSelect: () => void
|
||||
pendingSelectTab: 'issue' | 'chat' | 'profile' | 'sessions' | 'changes' | null
|
||||
clearPendingSelectTab: () => void
|
||||
/** 当前 Gitea login,来自最近一次 `issues/update`;undefined = 身份未知。 */
|
||||
/** 当前登录者的 login,来自最近一次 `issues/update`;undefined = 身份未知。 */
|
||||
me: string | undefined
|
||||
/** 最近一次 `requestHandoffUsers` 的结果;`null` = 仍在加载或尚未请求。 */
|
||||
handoffUsers: { issueNumber: number, users: string[] } | null
|
||||
@@ -956,6 +959,8 @@ export function useIssues(): UseIssuesResult {
|
||||
// usual spinner text.
|
||||
setSettings({
|
||||
host: msg.host,
|
||||
forgeKind: msg.forgeKind,
|
||||
ghLogin: msg.ghLogin,
|
||||
errorMessage: msg.errorMessage,
|
||||
canCancel: msg.canCancel === true,
|
||||
tokenSaved: msg.tokenSaved,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 「改动」tab 的订阅 hook:按工单号拉取整个 PR 的改动文件清单,并按需取单个文件
|
||||
* 改动前后的原文(供 git-diff-view 内联渲染)。数据全程实时从扩展侧(Gitea)取,
|
||||
* 改动前后的原文(供 git-diff-view 内联渲染)。数据全程实时从扩展侧(代码托管仓库)取,
|
||||
* 本 hook 只做缓存与订阅。
|
||||
*
|
||||
* - issueNumber 变化(含首次)时 postMessage `pr-files/get`,并清空上一工单缓存。
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Kept in sync manually; if the two ever diverge that's a step-3+ concern.
|
||||
*/
|
||||
|
||||
import type { ForgeKind } from '../../../src/forge/types'
|
||||
import type { Issue, IssueColumn, ManagedSessionsData } from '../types'
|
||||
|
||||
export interface ToastLink {
|
||||
@@ -107,6 +108,10 @@ export type ExtensionToWebview
|
||||
| {
|
||||
type: 'settings/show'
|
||||
host: string
|
||||
/** 当前工作区仓库是哪一家,决定设置里显示 Token 输入框还是 gh 登录状态。 */
|
||||
forgeKind: ForgeKind
|
||||
/** gh 当前登录的 GitHub 账号;没登录或登录失效时没有。 */
|
||||
ghLogin?: string
|
||||
errorMessage?: string
|
||||
canCancel?: boolean
|
||||
tokenSaved: boolean
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { trackerName } from './trackerName'
|
||||
|
||||
describe('trackerName', () => {
|
||||
it('youTrack 工单叫 YouTrack,代码托管工单按网页地址的 host 认出哪一家', () => {
|
||||
expect(trackerName({ source: 'youtrack', htmlUrl: 'https://yt.example/issue/A-1' })).toBe('YouTrack')
|
||||
expect(trackerName({ source: 'forge', htmlUrl: 'https://github.com/o/r/issues/1' })).toBe('GitHub')
|
||||
expect(trackerName({ htmlUrl: 'https://gitea.example/o/r/issues/1' })).toBe('Gitea')
|
||||
})
|
||||
|
||||
it('地址不合法时当 Gitea', () => {
|
||||
expect(trackerName({ source: 'forge', htmlUrl: '' })).toBe('Gitea')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Issue } from '../types'
|
||||
import { FORGE_NAMES, forgeKindForHost } from '../../../src/forge/types'
|
||||
|
||||
/** 工单所在系统的名字,用在「在 GitHub 打开」这类文案里;代码托管工单看网页地址的 host 认是哪一家。 */
|
||||
export function trackerName(issue: Pick<Issue, 'source' | 'htmlUrl'>): string {
|
||||
if (issue.source === 'youtrack')
|
||||
return 'YouTrack'
|
||||
let host = ''
|
||||
try {
|
||||
host = new URL(issue.htmlUrl).host
|
||||
}
|
||||
catch {}
|
||||
return FORGE_NAMES[forgeKindForHost(host)]
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export interface Issue {
|
||||
*/
|
||||
prototypeSessionId?: string
|
||||
htmlUrl: string
|
||||
/** 前置任务 issue number (Gitea dependencies 取第一个) */
|
||||
/** 前置任务 issue number(工单依赖里取第一个) */
|
||||
prerequisite?: number
|
||||
/** Terminal/tab color id chosen on first session open, persisted in state JSON. */
|
||||
color?: string
|
||||
@@ -97,13 +97,13 @@ export interface Issue {
|
||||
autoReview?: boolean
|
||||
/** 最近一次审查是否通过。只影响审查列卡片上的小字,不改列。 */
|
||||
reviewPassed?: boolean
|
||||
/** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
||||
/** 代码托管工单的 assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
||||
assignees?: string[]
|
||||
/** Gitea 工单创建者 login;无 assignee 的工单靠它判定归属与卡片头像。 */
|
||||
/** 代码托管工单的创建者 login;无 assignee 的工单靠它判定归属与卡片头像。 */
|
||||
poster?: string
|
||||
/** 移交会话包在 OSS 上的对象 key;存在即「待接管」。 */
|
||||
handoffBundleKey?: string
|
||||
/** 发起移交的 Gitea login。 */
|
||||
/** 发起移交的人的 login。 */
|
||||
handoffFrom?: string
|
||||
/** 谁在申请把这个审查工单移交给自己。 */
|
||||
handoffRequestFrom?: string
|
||||
|
||||
Reference in new issue
Block a user