feat: 添加服务快捷面板#60
Conversation
服务快捷面板是 OpenCodeUI 中提供跨服务器浏览和切换会话的快捷入口。它替换了侧边栏顶部 OpenCode 品牌标识原有的页面刷新行为,改为弹出一个浮动面板,以三级树形结构(服务器 → 工程 → 会话)展示所有已配置服务器及其活跃会话。
以下是spec.md服务快捷面板
1. 模块概述1.1 目的服务快捷面板是用户跨服务器浏览和切换会话的快捷入口。通过点击侧边栏顶部的 OpenCode 品牌标识触发,以浮动面板的形式展示所有已配置服务器及其下属工程和活跃会话的三级树形结构,使用户无需打开设置面板即可快速定位并切换到任意服务器上的任意会话。 1.2 解决的问题
1.3 范围本模块涵盖:
本模块不涵盖:
2. 用户故事
3. 功能需求FR-01:触发与弹出描述:侧边栏顶部的 OpenCode 品牌标识从链接改为按钮,点击后弹出服务快捷面板。 要求:
验收标准:
FR-02:三级树形结构描述:面板内容以三级可展开/收起的树形结构展示:服务器 → 工程 → 会话。 要求:
验收标准:
FR-03:数据获取与展示描述:面板打开时自动获取所有服务器的项目和会话数据,并实时展示加载状态。 要求:
验收标准:
FR-04:会话状态指示描述:每个会话条目通过状态指示点展示其当前运行状态。 要求:
验收标准:
FR-05:关闭行为描述:面板支持多种方式关闭,关闭时带有退出动画。 要求:
验收标准:
FR-06:会话切换描述:点击某个会话后,自动切换到该会话所属的服务器并打开该会话。 要求:
验收标准:
4. 界面元素4.1 面板头部
4.2 服务器节点
4.3 工程节点
4.4 会话节点
5. 关键实体5.1 服务器节点
5.2 工程分组
5.3 会话信息
5.4 会话状态映射后端
6. 子模块
7. 验收场景场景 1:点击 OpenCode 标识弹出面板前置条件:应用已启动,侧边栏可见 步骤:
预期结果:
场景 2:查看服务器树形结构前置条件:面板已打开,存在多个已配置服务器 步骤:
预期结果:
场景 3:跨服务器切换会话前置条件:面板已打开,存在非当前服务器的会话 步骤:
预期结果:
场景 4:关闭面板前置条件:面板已打开 步骤:
预期结果:
场景 5:服务器连接失败前置条件:面板已打开,某个服务器无法连接 步骤:
预期结果:
场景 6:会话状态展示前置条件:面板已打开,存在不同状态的会话 步骤:
预期结果:
8. 依赖关系8.1 外部依赖
8.2 内部依赖
8.3 被依赖模块
9. 架构决策记录ADR-001:为什么为每个服务器创建独立的 SDK 客户端决策:面板为每个服务器创建独立的 SDK 客户端实例,而非复用全局 active server 的客户端。 理由:
ADR-002:为什么项目列表获取失败时使用兜底分组决策:当 理由:
ADR-003:为什么 OpenCode 标识从链接改为按钮决策:侧边栏顶部的 OpenCode 标识从 理由:
10. 风险与缓解
|
以下是plan.md901-ext-server-quick-panel 技术方案(As-Built)
1. Technical Context1.1 模块定位服务快捷面板( 1.2 技术栈
1.3 源码目录结构1.4 文件规模
总计约 655 行代码。 2. Constitution Check对照项目宪法逐项验证:
3. Research Findings3.1 面板定位与渲染架构面板使用 定位计算逻辑: const rect = trigger.getBoundingClientRect()
const panelWidth = 320
const gap = 8
const left = Math.min(rect.left, window.innerWidth - panelWidth - 16)
setPanelPos({
top: rect.bottom + gap,
left: Math.max(8, left),
width: panelWidth,
})入场动画:通过
关闭防抖: 3.2 独立 SDK 客户端架构面板需要同时查询多个服务器的数据,每个服务器可能有不同的 URL 和认证信息。 客户端缓存策略: const clientCache = new Map<string, OpencodeClient>()
function getServerClient(server: ServerConfig): OpencodeClient {
const authPart = server.auth?.password ? `${server.auth.username}:${server.auth.password}` : 'no-auth'
const cacheKey = `${server.url}|${authPart}`
if (clientCache.has(cacheKey)) {
return clientCache.get(cacheKey)!
}
const headers: Record<string, string> = {}
if (server.auth?.password) {
headers['Authorization'] = makeBasicAuthHeader(server.auth)
}
const client = createOpencodeClient({ baseUrl: server.url, headers })
clientCache.set(cacheKey, client)
return client
}设计要点:
3.3 数据获取流程面板打开时的数据获取流程: 降级策略:
取消机制: 3.4 会话状态映射SDK 的 type SessionStatus =
| { type: 'idle' }
| { type: 'busy' }
| { type: 'retry'; attempt: number; message: string; next: number }面板内部简化为: function extractStatusType(status: SessionStatus | undefined): 'idle' | 'busy' | 'paused' | 'unknown' {
if (!status) return 'unknown'
return status.type as SessionInfo['status']
}映射关系:
3.5 关闭行为三种关闭方式:
外部点击检测逻辑: const handleClickOutside = (e: MouseEvent) => {
if (isClosingRef.current) return
const target = e.target as Node
if (triggerRef.current?.contains(target)) return // 排除触发按钮
if (panelRef.current?.contains(target)) return // 排除面板自身
handleClose()
}3.6 会话切换处理在 const handleQuickPanelSelectSession = useCallback(
(session: { id: string; title: string; directory?: string }, serverId: string) => {
const activeServer = serverStore.getActiveServer()
// 跨服务器切换
if (activeServer?.id !== serverId) {
messageStore.clearSession(selectedSessionId ?? '')
serverStore.setActiveServer(serverId)
}
// 添加工作目录
if (session.directory) {
addDirectory(session.directory)
}
// 获取会话详情并导航
getSession(session.id, session.directory)
.then(apiSession => {
onSelectSession(apiSession)
})
.catch(() => {})
},
[selectedSessionId, addDirectory, onSelectSession],
)4. Data Model4.1 面板内部数据结构/** 面板内展示的 session 信息(从 SDK 数据简化而来) */
interface SessionInfo {
id: string
title: string
directory?: string
status: 'idle' | 'busy' | 'paused' | 'unknown'
}
/** 单个服务器下的工程分组 */
interface ProjectGroup {
id: string
name: string
directory?: string
sessions: SessionInfo[]
}
/** 面板中每个服务器节点的数据结构 */
interface ServerNode {
server: ServerConfig // 服务器配置(来自 serverStore)
health: ServerHealth | null // 健康状态(来自 serverStore)
projects: ProjectGroup[] // 下属工程列表
isLoading: boolean // 是否正在加载数据
error: string | null // 加载错误信息
}4.2 组件 Props 接口interface ServerQuickPanelProps {
/** 触发按钮的 ref,用于定位面板弹出位置 */
triggerRef: React.RefObject<HTMLElement | null>
/** 关闭面板的回调 */
onClose: () => void
/** 选中 session 时的回调,传入 session 信息和所属 serverId */
onSelectSession: (session: SessionInfo, serverId: string) => void
}4.3 状态流转图面板生命周期: 服务器节点状态: 5. Interface Contracts5.1 ServerQuickPanel 组件契约function ServerQuickPanel({ triggerRef, onClose, onSelectSession }: ServerQuickPanelProps): React.ReactPortal行为约定:
5.2 内嵌子组件
5.3 消费的 Store 接口
5.4 消费的 API 接口
6. Implementation Strategy6.1 组件层次6.2 数据流6.3 样式策略面板使用 Tailwind CSS 工具类,无自定义 CSS:
7. Error Handling7.1 错误场景与处理
7.2 边界条件
8. Testing Considerations8.1 组件测试覆盖点
8.2 集成测试场景
8.3 手动测试清单
9. 依赖关系总结9.1 外部依赖
9.2 内部依赖
9.3 被依赖模块
10. 风险区域10.1 多服务器并发请求性能(低风险)每个服务器需要 1 + 2N 次 API 调用(1 次项目列表 + N 个工程 × 2 次会话列表/状态)。当服务器数量多、工程数量多时,总请求数可能较大。 缓解:
10.2 SDK 客户端缓存与认证过期(低风险)客户端按 "URL + 认证信息" 缓存,如果服务器密码在服务端被修改,客户端仍使用旧认证。 缓解:
10.3 面板定位在动态布局下的准确性(低风险)触发按钮位置可能因侧边栏折叠/展开而变化。 缓解:
生成时间: 2026-04-13 |
以下是task.md901-ext-server-quick-panel 实施任务清单
任务概览
T-1:创建 ServerQuickPanel 组件状态:✅ 已完成 目标:创建独立的浮动面板组件,以三级树形结构展示服务器 → 工程 → 会话。 子任务:
验收:
T-2:改造 SidePanel 触发器状态:✅ 已完成 目标:将 OpenCode 标识从 子任务:
验收:
T-3:补充 i18n 翻译状态:✅ 已完成 目标:为面板新增的 4 个 i18n 键值补充中英文翻译。 新增键值:
验收:
T-4:TypeScript 类型验证状态:✅ 已完成 目标:确保所有新增代码通过 TypeScript 类型检查。 验证结果:
依赖关系
产出清单
生成时间: 2026-04-13 |
|
大概明白你的需求了,也许我应该考虑直接在侧边栏支持直接渲染多服务器会话? |
对,其实就是希望可以同时看到多个opencode服务的各个工程以及session状态。 |
可以,后续应该会做,你这个放到OpenCode字样上做的话可能并不是很符合我想要的交互设计,你可以先这样自用着,我先试试其它的方案 |
# Conflicts: # src/features/chat/sidebar/SidePanel.tsx
服务快捷面板是 OpenCodeUI 中提供跨服务器浏览和切换会话的快捷入口。它替换了侧边栏顶部 OpenCode 品牌标识原有的页面刷新行为,改为弹出一个浮动面板,以三级树形结构(服务器 → 工程 → 会话)展示所有已配置服务器及其活跃会话。