From 4a1ae454251058aa454ee666f5c990b257065bd3 Mon Sep 17 00:00:00 2001 From: cruldra Date: Wed, 19 Aug 2026 18:10:51 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E6=A0=BC=E5=8A=A8=E4=BD=9C=E5=80=BC=E6=94=B9=E5=8F=8C=E5=87=BB?= =?UTF-8?q?=E8=A7=A6=E5=8F=91+spinner=E5=8F=8D=E9=A6=88=EF=BC=8C=E7=83=AD?= =?UTF-8?q?=E5=8C=BA=E6=94=B6=E7=AA=84=E5=88=B0=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/IssueDetailPanel.tsx | 26 ++--- .../src/components/property-grid.tsx | 100 ++++++++++++------ 2 files changed, 80 insertions(+), 46 deletions(-) diff --git a/vscode/webview-ui/src/components/IssueDetailPanel.tsx b/vscode/webview-ui/src/components/IssueDetailPanel.tsx index 3f3d3be..289bb3c 100644 --- a/vscode/webview-ui/src/components/IssueDetailPanel.tsx +++ b/vscode/webview-ui/src/components/IssueDetailPanel.tsx @@ -95,7 +95,7 @@ interface IssueDetailPanelProps { * * 顶部一行是 #编号/标题 + 「在 Gitea 打开」按钮;下面是 PropertyGrid * 渲染的 state JSON 字段(当前为 column + sessionId)。sessionId 是 - * 一个 action 类型字段,点击或选中卡按 Enter 都会触发 + * 一个 action 类型字段,双击或选中卡按 Enter 都会触发 * `claude --resume ` 在新终端中恢复对话。 */ export function IssueDetailPanel({ @@ -137,7 +137,7 @@ export function IssueDetailPanel({ const prDiffSummaryRunning = issue ? isPrDiffSummaryRunning(issue.number) : false // 三个会话 id 行(头脑风暴/实施/审查)共用一个"上次触发时间戳"表, - // 防止用户连续快速点击同一行时触发多次 resume —— extension 端 findExistingTerminal + // 防止用户连续快速双击同一行时触发多次 resume —— extension 端 findExistingTerminal // 在前一个 createTerminal 尚未完成时查不到已存在的终端,会重复开 tab。 // key 用 sessionId 字符串本身(足够区分三种行 + 不同工单)。 const lastFiredAtRef = useRef>(new Map()) @@ -211,7 +211,7 @@ export function IssueDetailPanel({ label: '头脑风暴会话id', type: 'action' as const, description: hasSession - ? '点击在新终端运行 claude --resume 恢复对话' + ? '双击在新终端运行 claude --resume 恢复对话' : '工单尚未关联 cc 会话;点击右侧 ▶ 启动一个新的规划 cc tab', actionIcon: , onAction: (v: unknown) => { @@ -226,7 +226,7 @@ export function IssueDetailPanel({ onSecondaryAction, } })(), - // 实施会话即使 worktree 已被清掉也保留可点击的 resume 入口: + // 实施会话即使 worktree 已被清掉也保留可双击的 resume 入口: // extension 端 handleResumeSession 会在 worktree 路径不存在时退回 // 工作区根目录,并通过 toast 告知用户;description 在两种状态下 // 给出不同的提示文案。 @@ -235,7 +235,7 @@ export function IssueDetailPanel({ label: '实施会话id', type: 'action', description: issue?.worktreeExists - ? '点击在新终端运行 claude --resume 恢复实施对话(cwd 为 worktree)' + ? '双击在新终端运行 claude --resume 恢复实施对话(cwd 为 worktree)' : 'worktree 已清理,将在工作区根目录恢复(cc 可能提示原 cwd 不存在)', actionIcon: , onAction: (v) => { @@ -259,7 +259,7 @@ export function IssueDetailPanel({ key: 'reviewSessionId', label: '审查会话id', type: 'action', - description: '点击在新终端运行 codex resume 查看审查会话(cwd 优先用 worktree)', + description: '双击在新终端运行 codex resume 查看审查会话(cwd 优先用 worktree)', actionIcon: , onAction: (v) => { if (typeof v !== 'string' || v.length === 0 || !issue) @@ -287,7 +287,7 @@ export function IssueDetailPanel({ (() => { // 测试会话 id 行有三种状态(照搬头脑风暴行的三态): // 1. testSessionId 已存在 + testTabOpen=true → 显示 id + X 关闭 tab - // 2. testSessionId 已存在 + testTabOpen=false → 显示 id,点击 resume + // 2. testSessionId 已存在 + testTabOpen=false → 显示 id,双击 resume // 3. testSessionId 为空 + issue.pr 存在 → 显示 Play 启动新测试 cc tab // (prompt 依赖已合并的 PR 号,没有 PR 不显示启动按钮) const hasSession = !!(issue?.testSessionId && issue.testSessionId.length > 0) @@ -316,7 +316,7 @@ export function IssueDetailPanel({ label: '测试会话id', type: 'action' as const, description: hasSession - ? '点击在新终端运行 claude --resume 在工作区根(main)恢复测试对话' + ? '双击在新终端运行 claude --resume 在工作区根(main)恢复测试对话' : '工单 PR 合并后点击右侧 ▶ 在工作区根(main)启动一个测试 cc tab(让 cc 了解代码后告诉你怎么测试)', actionIcon: , onAction: (v: unknown) => { @@ -391,14 +391,14 @@ export function IssueDetailPanel({ key: 'specFile', label: '规格文件', type: 'file-link', - description: '点击文件名在编辑器打开;cc 通过 issue body 的 注释实时同步', + description: '双击文件名在编辑器打开;cc 通过 issue body 的 注释实时同步', onOpen: (p: string) => onOpenFile(p), }, { key: 'planFile', label: '计划文件', type: 'file-link', - description: '点击文件名在编辑器打开;点击实施按钮启动实施流程;cc 通过 issue body 的 注释实时同步', + description: '双击文件名在编辑器打开;点击实施按钮启动实施流程;cc 通过 issue body 的 注释实时同步', onOpen: (p: string) => onOpenFile(p), // Hide the "实施" button entirely when there's no plan file. secondaryActionIcon: issue?.planFile @@ -426,7 +426,7 @@ export function IssueDetailPanel({ key: 'pr', label: '合并请求', type: 'pr-link', - description: '点击在浏览器打开关联的 PR;点击右侧按钮生成 PR 变更摘要;已合并时编号后追加 "(已合并)" 标识', + description: '双击在浏览器打开关联的 PR;点击右侧按钮生成 PR 变更摘要;已合并时编号后追加 "(已合并)" 标识', onAction: (v) => { if (typeof v !== 'string' || v.length === 0) return @@ -451,7 +451,7 @@ export function IssueDetailPanel({ key: 'prDiffFile', label: 'PR 变更摘要', type: 'file-link', - description: '点击文件名在编辑器打开生成的 PR 代码变更摘要', + description: '双击文件名在编辑器打开生成的 PR 代码变更摘要', onOpen: (p: string) => onOpenFile(p), }, { @@ -472,7 +472,7 @@ export function IssueDetailPanel({ key: 'worktreePath', label: '工作树', type: 'file-link', - description: '点击文件名在新 VS Code 窗口打开;点击垃圾桶删除 worktree', + description: '双击文件名在新 VS Code 窗口打开;点击垃圾桶删除 worktree', onOpen: (p: string) => onOpenWorktree(p), onSecondaryAction: () => issue && onDeleteWorktree(issue.number, issue.worktreePath ?? ''), secondaryActionIcon: , diff --git a/vscode/webview-ui/src/components/property-grid.tsx b/vscode/webview-ui/src/components/property-grid.tsx index eb169d7..2c2679e 100644 --- a/vscode/webview-ui/src/components/property-grid.tsx +++ b/vscode/webview-ui/src/components/property-grid.tsx @@ -17,6 +17,7 @@ import { Eye, EyeOff, Info, + Loader2, RefreshCw, RotateCcw, Save, @@ -54,11 +55,11 @@ export interface PropertyDef { options?: Array<{ label: string, value: string | number }> /** 是否只读 */ readOnly?: boolean - /** type='action' / 'pr-link' 专用:点击值时的回调 */ + /** type='action' / 'pr-link' 专用:双击值时的回调 */ onAction?: (value: unknown) => void /** type='action' 专用:值前的图标 */ actionIcon?: ReactNode - /** type='file-link' 专用:点击文件名时调用,参数为 value(workspace 相对路径) */ + /** type='file-link' 专用:双击文件名时调用,参数为 value(workspace 相对路径) */ onOpen?: (path: string) => void /** type='file-link' 专用:点击右侧图标按钮时调用,用于重新扫描/加载 */ onReload?: () => void @@ -145,8 +146,27 @@ function PropertyRow({ registerCellRef: (fullKey: string, el: HTMLDivElement | null) => void }) { const [showPassword, setShowPassword] = useState(false) + const [actionPending, setActionPending] = useState(false) + const pendingTimerRef = useRef(null) const ro = propDef.readOnly + useEffect(() => () => { + if (pendingTimerRef.current != null) + window.clearTimeout(pendingTimerRef.current) + }, []) + + /** 触发主动作并给出即时反馈:行内图标临时切换为 spinner,约 1.5s 后恢复 */ + function firePrimaryAction(fire: () => void): void { + fire() + setActionPending(true) + if (pendingTimerRef.current != null) + window.clearTimeout(pendingTimerRef.current) + pendingTimerRef.current = window.setTimeout(() => { + setActionPending(false) + pendingTimerRef.current = null + }, 1500) + } + function renderControl(): ReactNode { switch (propDef.type) { case 'boolean': @@ -293,18 +313,23 @@ function PropertyRow({
{hasValue ? ( - + <> + +