Files
superwork/vscode/webview-ui/src/components/BottomTabs.tsx
T
cruldra 142af57d9d ✨ feat(vscode): 工单挂原型插件,详情面板一键导入 Figma
- prototypeFile 走通 state → Issue → webview 全链路(gitea/youtrack 两个
  loader、两份 KNOWN_STATE_FIELDS、issue/patch、webview types),按 planFile
  同款当共享字段;路径校验换成 .json 版,允许绝对路径
- 详情面板「计划文件」下加「原型插件路径」行:双击开 manifest.json,
  行尾按钮「在 Figma 中导入并打开」;没挂插件时只显示空态
- src/figma/:把 manifest 写进 Figma 桌面版的 localFileExtensions 再拉起
  Figma。坑是 Figma 退出会用内存状态覆盖 settings.json,所以写之前先
  osascript 优雅退出并轮询等它真没了(10s 等不到就让用户手动关,不强杀);
  写前备份 settings.json.bak-<时间戳>;已导入过就跳过写入直接打开;
  非 macOS 只把路径复制到剪贴板
- webhook 里 spec/plan 的 marker 同步改成表驱动,顺带认第三种 prototype
- 提示词:prototype.md 要求画完挂 marker;brainstorm-continue.md 新增
  {prototypeRef} 占位符,有原型插件时让续聊先读它别从零重画,没有时
  连同整段抹掉

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
2026-09-15 00:11:49 +08:00

251 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 底部 panel:左侧垂直 tab 切换四个内容区。
*
* - tab 1 「工单」:现有 IssueDetailPanel(基于 PropertyGrid)
* - tab 2 「Profile」:工作区 KV 表 ProfileGrid
* - tab 3 「会话」:ManagedSessionsPanel
* - tab 4 「改动」:PrFilesPanel
*
* 四个内容始终挂载,非当前用 hidden 隐藏,避免切 tab 卸载导致阅读态(选中文件、
* 树滚动/折叠、diff scroll)丢失。tab 状态用 useState,无持久化;激活态加左侧 2px 高亮条。
*/
import type { ClaudeProfile } from '../hooks/useIssues'
import type { ProfilesData, SessionHandoffPendingItem } from '../lib/messages'
import type { Issue, ManagedSessionsData } from '../types'
import { ClipboardList, FileDiff, Layers, MessagesSquare } from 'lucide-react'
import { useState } from 'react'
import { IssueDetailPanel } from './IssueDetailPanel'
import { ManagedSessionsPanel } from './ManagedSessionsPanel'
import { PrFilesPanel } from './PrFilesPanel'
import { ProfileGrid } from './ProfileGrid'
type TabKey = 'issue' | 'profile' | 'sessions' | 'changes'
interface BottomTabsProps {
// Issue tab props (pass-through to IssueDetailPanel)
issue: Issue | null
allIssues: Issue[]
globalAutoReview: boolean
onOpenInBrowser: (url: string) => void
onResumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void
isSessionTabPending: (sessionId: string) => boolean
onResumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void
onStartReviewSession: (issueNumber: number) => void
onStartTestSession: (issueNumber: number) => void
onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
onStartPrototypeSession: (issueNumber: number) => void
onResumePrototypeSession: (sessionId: string, issueNumber: number, cwd?: string) => void
onOpenFile: (path: string) => void
onOpenPrototypeInFigma: (issueNumber: number, path: string) => void
onImplement: (issueNumber: number, planFile: string, profilePath?: string, sessionId?: string) => void
onOpenPr: (pr: string) => void
onGeneratePrDiffSummary: (issueNumber: number) => void
isPrDiffSummaryRunning: (issueNumber: number) => boolean
onOpenWorktree: (path: string) => void
onDeleteWorktree: (issueNumber: number, path: string) => void
onMergeBranch: (issueNumber: number, branch: string) => void
onDeleteIssue?: (issueNumber: number) => void
onCloseIssue: (issueNumber: number) => void
onResetToTodo: (issueNumber: number) => void
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
onContinueBrainstorm: (issueNumber: number) => void
onClearBrainstormSession: (issueNumber: number) => void
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
onUpdateProfilePath: (issueNumber: number, profilePath: string) => void
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
onUpdateTestProfilePath: (issueNumber: number, testProfilePath: string) => void
onUpdatePrototypeProfilePath: (issueNumber: number, prototypeProfilePath: string) => void
onOpenLogs: () => void
me?: string
/** Gitea login → 头像 data URI。 */
avatars?: Record<string, string>
onStartHandoff: (issueNumber: number) => void
onAcceptHandoff: (issueNumber: number) => void
isHandoffRunning: (issueNumber: number) => boolean
// Profile tab props
profileData: ProfilesData
onProfileSave: (next: ProfilesData) => void
onProfileOpen: (value: string) => void
// 会话管理 tab props
managedSessions: ManagedSessionsData
profiles: ClaudeProfile[]
onManagedSessionCreate: (profilePath: string, name?: string, prompt?: string) => void
onManagedSessionRename: (id: string, name: string) => void
onManagedSessionResume: (id: string, profilePath?: string) => void
onManagedSessionDelete: (id: string) => void
onManagedSessionCloseTab: (id: string) => void
importableSessions: Array<{ id: string, name: string, mtimeMs: number }>
importableLoading: boolean
onListImportableSessions: () => void
onImportNamedSession: (sessionId: string, name: string) => void
pendingHandoffs: SessionHandoffPendingItem[]
onSessionHandoff: (sessionId: string) => void
onAcceptSessionHandoff: (attachmentId: number) => void
}
export function BottomTabs(props: BottomTabsProps) {
const [tab, setTab] = useState<TabKey>('issue')
return (
<div className="flex h-full w-full bg-[var(--vscode-editor-background)]">
{/* 左侧垂直 tab 栏 */}
<div className="flex w-9 shrink-0 flex-col items-stretch border-r border-[var(--vscode-panel-border)] bg-[var(--vscode-sideBar-background,var(--vscode-editor-background))]">
<TabButton
active={tab === 'issue'}
onClick={() => setTab('issue')}
icon={<ClipboardList className="size-4" />}
title="工单"
/>
<TabButton
active={tab === 'profile'}
onClick={() => setTab('profile')}
icon={<Layers className="size-4" />}
title="Profile"
/>
<TabButton
active={tab === 'sessions'}
onClick={() => setTab('sessions')}
icon={<MessagesSquare className="size-4" />}
title="会话"
badge={props.pendingHandoffs.length}
/>
<TabButton
active={tab === 'changes'}
onClick={() => setTab('changes')}
icon={<FileDiff className="size-4" />}
title="改动"
/>
</div>
{/* 右侧内容:始终挂载,非当前 hidden,保留阅读态 */}
<div className="relative min-w-0 flex-1 overflow-hidden">
<div
className={`h-full w-full ${tab === 'issue' ? '' : 'hidden'}`}
aria-hidden={tab !== 'issue'}
inert={tab !== 'issue' ? true : undefined}
>
<IssueDetailPanel
issue={props.issue}
allIssues={props.allIssues}
globalAutoReview={props.globalAutoReview}
onOpenInBrowser={props.onOpenInBrowser}
onResumeSession={props.onResumeSession}
isSessionTabPending={props.isSessionTabPending}
onResumeReviewSession={props.onResumeReviewSession}
onStartReviewSession={props.onStartReviewSession}
onStartTestSession={props.onStartTestSession}
onResumeTestSession={props.onResumeTestSession}
onStartPrototypeSession={props.onStartPrototypeSession}
onResumePrototypeSession={props.onResumePrototypeSession}
onOpenFile={props.onOpenFile}
onOpenPrototypeInFigma={props.onOpenPrototypeInFigma}
onImplement={props.onImplement}
onOpenPr={props.onOpenPr}
onGeneratePrDiffSummary={props.onGeneratePrDiffSummary}
isPrDiffSummaryRunning={props.isPrDiffSummaryRunning}
onOpenWorktree={props.onOpenWorktree}
onDeleteWorktree={props.onDeleteWorktree}
onMergeBranch={props.onMergeBranch}
onDeleteIssue={props.onDeleteIssue}
onCloseIssue={props.onCloseIssue}
onResetToTodo={props.onResetToTodo}
onCloseSessionTab={props.onCloseSessionTab}
onContinueBrainstorm={props.onContinueBrainstorm}
onClearBrainstormSession={props.onClearBrainstormSession}
onUpdateAutoReview={props.onUpdateAutoReview}
onUpdateProfilePath={props.onUpdateProfilePath}
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
onUpdateTestProfilePath={props.onUpdateTestProfilePath}
onUpdatePrototypeProfilePath={props.onUpdatePrototypeProfilePath}
profiles={props.profiles}
onOpenLogs={props.onOpenLogs}
me={props.me}
avatars={props.avatars}
onStartHandoff={props.onStartHandoff}
onAcceptHandoff={props.onAcceptHandoff}
isHandoffRunning={props.isHandoffRunning}
/>
</div>
<div
className={`h-full w-full ${tab === 'profile' ? '' : 'hidden'}`}
aria-hidden={tab !== 'profile'}
inert={tab !== 'profile' ? true : undefined}
>
<ProfileGrid
data={props.profileData}
onSave={props.onProfileSave}
onOpen={props.onProfileOpen}
/>
</div>
<div
className={`h-full w-full ${tab === 'sessions' ? '' : 'hidden'}`}
aria-hidden={tab !== 'sessions'}
inert={tab !== 'sessions' ? true : undefined}
>
<ManagedSessionsPanel
data={props.managedSessions}
profiles={props.profiles}
onCreate={props.onManagedSessionCreate}
onRename={props.onManagedSessionRename}
onResume={props.onManagedSessionResume}
onDelete={props.onManagedSessionDelete}
onCloseTab={props.onManagedSessionCloseTab}
importableSessions={props.importableSessions}
importableLoading={props.importableLoading}
onListImportable={props.onListImportableSessions}
onImport={props.onImportNamedSession}
pendingHandoffs={props.pendingHandoffs}
onHandoff={props.onSessionHandoff}
onAcceptHandoff={props.onAcceptSessionHandoff}
/>
</div>
<div
className={`h-full w-full ${tab === 'changes' ? '' : 'hidden'}`}
aria-hidden={tab !== 'changes'}
inert={tab !== 'changes' ? true : undefined}
>
<PrFilesPanel issue={props.issue} />
</div>
</div>
</div>
)
}
interface TabButtonProps {
active: boolean
onClick: () => void
icon: React.ReactNode
title: string
badge?: number
}
function TabButton({ active, onClick, icon, title, badge }: TabButtonProps) {
return (
<button
type="button"
onClick={onClick}
title={title}
aria-label={title}
aria-pressed={active}
className={`relative flex h-9 w-full items-center justify-center transition-colors ${
active
? 'text-[var(--vscode-foreground)]'
: 'text-[var(--vscode-descriptionForeground)] hover:text-[var(--vscode-foreground)]'
}`}
>
{active && (
<span className="absolute left-0 top-0 h-full w-[2px] bg-[var(--vscode-focusBorder)]" />
)}
{icon}
{badge != null && badge > 0 && (
<span className="absolute right-0.5 top-0.5 min-w-[14px] rounded-full bg-[var(--vscode-activityBarBadge-background)] px-0.5 text-center text-[9px] leading-[14px] text-[var(--vscode-activityBarBadge-foreground)]">
{badge > 9 ? '9+' : badge}
</span>
)}
</button>
)
}