Skip to content

feat: 添加服务快捷面板#60

Open
fellow99 wants to merge 6 commits into
lehhair:mainfrom
fellow99:fellow99/901-ext-server-quick-panel
Open

feat: 添加服务快捷面板#60
fellow99 wants to merge 6 commits into
lehhair:mainfrom
fellow99:fellow99/901-ext-server-quick-panel

Conversation

@fellow99
Copy link
Copy Markdown

服务快捷面板是 OpenCodeUI 中提供跨服务器浏览和切换会话的快捷入口。它替换了侧边栏顶部 OpenCode 品牌标识原有的页面刷新行为,改为弹出一个浮动面板,以三级树形结构(服务器 → 工程 → 会话)展示所有已配置服务器及其活跃会话。

OpenCodeUI-ext

服务快捷面板是 OpenCodeUI 中提供跨服务器浏览和切换会话的快捷入口。它替换了侧边栏顶部 OpenCode 品牌标识原有的页面刷新行为,改为弹出一个浮动面板,以三级树形结构(服务器 → 工程 → 会话)展示所有已配置服务器及其活跃会话。
@fellow99
Copy link
Copy Markdown
Author

以下是spec.md

服务快捷面板

模块编号:901-ext-server-quick-panel
状态:已实现
最后更新:2026-04-13


1. 模块概述

1.1 目的

服务快捷面板是用户跨服务器浏览和切换会话的快捷入口。通过点击侧边栏顶部的 OpenCode 品牌标识触发,以浮动面板的形式展示所有已配置服务器及其下属工程和活跃会话的三级树形结构,使用户无需打开设置面板即可快速定位并切换到任意服务器上的任意会话。

1.2 解决的问题

  • 切换成本高:此前点击 OpenCode 标识会刷新整个页面,中断当前工作流
  • 跨服务器不可见:用户无法在不切换活动服务器的情况下查看其他服务器上的会话
  • 会话定位困难:当存在多个服务器、多个工程时,用户难以快速找到目标会话

1.3 范围

本模块涵盖:

  • 侧边栏顶部 OpenCode 标识的点击行为改造(从页面刷新改为弹出快捷面板)
  • 快捷面板的弹出定位、入场/退出动画
  • 三级树形数据展示:服务器 → 工程 → 活跃会话
  • 每个服务器的独立数据获取(项目列表、会话列表、会话状态)
  • 服务器健康状态图标展示
  • 会话状态指示(忙碌、空闲、暂停)
  • 点击外部区域和 ESC 键关闭面板
  • 选中会话后自动切换服务器并导航到目标会话

本模块不涵盖:

  • 服务器的增删改查(由设置面板的服务器管理负责)
  • 会话的创建、删除、重命名(由会话管理模块负责)
  • 工程的创建、删除(由后端 API 负责)

2. 用户故事

编号 用户故事 优先级
US-01 作为用户,我希望点击侧边栏顶部的 OpenCode 标识后弹出快捷面板,而不是刷新整个页面 P0
US-02 作为用户,我希望在快捷面板中看到所有已配置服务器的列表,包括它们的健康状态 P0
US-03 作为用户,我希望展开某个服务器后查看其下的所有工程 P0
US-04 作为用户,我希望展开某个工程后查看其下的活跃会话列表 P0
US-05 作为用户,我希望点击某个会话后自动切换到该会话所属的服务器并打开该会话 P0
US-06 作为用户,我希望在面板中看到每个会话的运行状态(忙碌/空闲/暂停) P1
US-07 作为用户,我希望通过 ESC 键或点击面板外部区域关闭快捷面板 P1
US-08 作为用户,我希望当前活动的服务器在面板中被特殊标识并默认展开 P1
US-09 作为用户,我希望在面板头部看到服务器总数和会话总数的概览信息 P2
US-10 作为用户,我希望忙碌中的会话在列表中排在前面,方便我快速找到正在工作的会话 P2

3. 功能需求

FR-01:触发与弹出

描述:侧边栏顶部的 OpenCode 品牌标识从链接改为按钮,点击后弹出服务快捷面板。

要求

  • OpenCode 标识以按钮形式呈现,去除默认链接样式,保留原有视觉外观
  • 面板以浮动层形式出现在触发按钮正下方,与按钮间距 8 像素
  • 面板宽度固定为 320 像素,最大高度为 520 像素或视口剩余高度(取较小值)
  • 面板水平位置跟随触发按钮,但不超出视口左边界(最小左边距 8 像素)
  • 面板弹出时带有淡入和缩放动画(opacity 从 0 到 1,scale 从 0.95 到 1)
  • 动画时长 150 毫秒,缓动函数为 ease-out
  • 面板渲染在 document.body 层级,z-index 为 9999

验收标准

  • 给定点击 OpenCode 标识,面板在按钮下方弹出
  • 给定面板弹出位置超出视口右边界,面板向左偏移以确保完全可见
  • 给定面板弹出,动画流畅无闪烁

FR-02:三级树形结构

描述:面板内容以三级可展开/收起的树形结构展示:服务器 → 工程 → 会话。

要求

  • 第一级为服务器节点,显示服务器名称、URL、认证标识、健康状态图标和会话总数
  • 第二级为工程节点,显示工程名称、会话总数和忙碌会话数
  • 第三级为会话节点,显示会话标题和状态指示点
  • 每个层级通过 Chevron 图标控制展开/收起,收起时图标旋转 -90 度
  • 当前活动服务器以主题色高亮,并显示"当前"标签
  • 服务器节点之间以分隔线区分
  • 工程节点相对于服务器节点有额外的左侧缩进
  • 会话节点相对于工程节点有进一步的左侧缩进

验收标准

  • 给定展开服务器节点,下方显示该服务器的工程列表
  • 给定展开工程节点,下方显示该工程的会话列表
  • 给定收起节点,子级内容隐藏
  • 给定当前活动服务器,显示特殊标识

FR-03:数据获取与展示

描述:面板打开时自动获取所有服务器的项目和会话数据,并实时展示加载状态。

要求

  • 面板打开时立即显示所有服务器节点,初始状态为加载中
  • 为每个服务器独立创建 SDK 客户端,使用该服务器自己的 URL 和认证信息
  • 客户端实例按 "URL + 认证信息" 缓存,避免重复创建
  • 获取项目列表失败时,使用一个兜底的"全部项目"分组
  • 获取会话状态失败时不影响会话列表展示,状态标记为未知
  • 单个服务器数据获取失败时,该节点显示错误信息,不影响其他服务器
  • 面板关闭时取消进行中的数据请求
  • 活跃会话按状态排序:忙碌会话排在前面

验收标准

  • 给定面板打开,所有服务器节点显示加载中状态
  • 给定数据加载完成,加载中状态消失,显示工程列表
  • 给定某个服务器连接失败,该节点显示错误信息
  • 给定面板关闭后重新打开,数据重新获取

FR-04:会话状态指示

描述:每个会话条目通过状态指示点展示其当前运行状态。

要求

  • 状态分为四种:忙碌(busy)、空闲(idle)、暂停(paused)、未知(unknown)
  • 忙碌状态:绿色圆点,带脉冲动画
  • 空闲状态:灰色圆点
  • 暂停状态:黄色圆点
  • 未知状态:深灰色圆点
  • 状态信息来源于后端 session.status API

验收标准

  • 给定会话正在处理请求,显示绿色脉冲圆点
  • 给定会话空闲,显示灰色圆点
  • 给定会话暂停,显示黄色圆点

FR-05:关闭行为

描述:面板支持多种方式关闭,关闭时带有退出动画。

要求

  • 点击面板右上角的关闭按钮关闭面板
  • 点击面板外部任意区域关闭面板
  • 按下 ESC 键关闭面板
  • 关闭时先触发退出动画(opacity 从 1 到 0,scale 从 1 到 0.95)
  • 动画完成后(150 毫秒后)从 DOM 中移除面板组件
  • 关闭动画进行中时,不响应重复的关闭操作

验收标准

  • 给定点击面板外部区域,面板关闭
  • 给定按下 ESC 键,面板关闭
  • 给定点击关闭按钮,面板关闭
  • 给定关闭动画进行中,再次点击外部区域不会触发二次关闭

FR-06:会话切换

描述:点击某个会话后,自动切换到该会话所属的服务器并打开该会话。

要求

  • 如果目标会话不属于当前活动服务器,先切换活动服务器
  • 切换服务器时清理当前会话的消息存储状态
  • 获取目标会话的详细信息
  • 导航到目标会话
  • 如果目标会话有工作目录信息,自动将该目录添加到项目列表
  • 面板在会话切换完成后自动关闭

验收标准

  • 给定点击当前服务器的会话,直接打开该会话
  • 给定点击其他服务器的会话,先切换服务器再打开会话
  • 给定面板在会话切换后自动关闭

4. 界面元素

4.1 面板头部

元素 说明
标题 "服务器"
概览信息 服务器数量 · 会话数量
关闭按钮 点击关闭面板

4.2 服务器节点

元素 说明
展开/收起图标 Chevron 图标,旋转 -90 度表示收起
服务器图标 地球图标,当前服务器为主题色
服务器名称 显示配置的服务器名称
当前标签 仅当前活动服务器显示
URL 等宽字体显示服务器地址
认证标识 配置了密码时显示钥匙图标
会话计数 该服务器下所有会话总数
健康状态图标 在线/离线/未授权/检查中

4.3 工程节点

元素 说明
展开/收起图标 Chevron 图标
文件夹图标 工程标识
工程名称 显示工程名称或工作目录名
忙碌计数 忙碌中的会话数(绿色标签)
会话计数 该工程下的会话总数

4.4 会话节点

元素 说明
状态指示点 彩色圆点,表示会话运行状态
会话图标 消息方块图标
会话标题 显示会话标题,截断过长文本

5. 关键实体

5.1 服务器节点

属性 类型 说明
server ServerConfig 服务器配置信息
health ServerHealth|null 健康状态
projects ProjectGroup[] 下属工程列表
isLoading boolean 是否正在加载数据
error string|null 加载错误信息

5.2 工程分组

属性 类型 说明
id string 工程唯一标识
name string 工程显示名称
directory string? 工程工作目录路径
sessions SessionInfo[] 下属会话列表

5.3 会话信息

属性 类型 说明
id string 会话唯一标识
title string 会话标题
directory string? 会话工作目录
status 'idle' | 'busy' | 'paused' | 'unknown' 会话运行状态

5.4 会话状态映射

后端 session.status API 返回的原始状态类型到面板内部状态的映射:

后端状态 type 面板内部状态
idle idle
busy busy
retry unknown
无响应/未定义 unknown

6. 子模块

文件 领域 主要职责
ServerQuickPanel.tsx 面板组件 主面板容器、数据获取、定位计算、关闭行为
ServerTreeItem(内嵌) 服务器节点 服务器信息展示、健康状态、展开/收起
ProjectTreeItem(内嵌) 工程节点 工程信息展示、会话计数、展开/收起
SessionItem(内嵌) 会话节点 会话标题、状态指示点、点击选择
SidePanel.tsx(修改) 触发器 OpenCode 标识改造、面板状态管理、会话切换处理

7. 验收场景

场景 1:点击 OpenCode 标识弹出面板

前置条件:应用已启动,侧边栏可见

步骤

  1. 点击侧边栏顶部的 OpenCode 标识
  2. 观察面板弹出位置和动画效果

预期结果

  • 面板在 OpenCode 标识正下方 8 像素处弹出
  • 面板宽度 320 像素,带有淡入和缩放动画
  • 面板标题显示"服务器",概览信息正确

场景 2:查看服务器树形结构

前置条件:面板已打开,存在多个已配置服务器

步骤

  1. 观察服务器列表
  2. 展开当前活动服务器
  3. 展开某个工程
  4. 观察会话列表

预期结果

  • 所有服务器出现在列表中,当前活动服务器默认展开
  • 当前活动服务器显示"当前"标签和主题色图标
  • 展开服务器后显示加载状态,完成后显示工程列表
  • 展开工程后显示该工程的会话列表
  • 忙碌会话排在列表前面

场景 3:跨服务器切换会话

前置条件:面板已打开,存在非当前服务器的会话

步骤

  1. 展开非当前活动服务器
  2. 展开某个工程
  3. 点击某个会话

预期结果

  • 活动服务器切换为目标会话所属服务器
  • 当前会话消息被清理
  • 目标会话被打开
  • 面板自动关闭

场景 4:关闭面板

前置条件:面板已打开

步骤

  1. 点击面板外部区域
  2. 重新打开面板
  3. 按下 ESC 键

预期结果

  • 面板带有退出动画关闭
  • 重新打开后数据重新获取
  • ESC 键关闭面板

场景 5:服务器连接失败

前置条件:面板已打开,某个服务器无法连接

步骤

  1. 观察无法连接的服务器节点

预期结果

  • 该服务器节点显示错误信息
  • 其他服务器节点不受影响,正常加载数据

场景 6:会话状态展示

前置条件:面板已打开,存在不同状态的会话

步骤

  1. 展开包含多个会话的工程
  2. 观察每个会话的状态指示点

预期结果

  • 忙碌会话显示绿色脉冲圆点,排在列表前面
  • 空闲会话显示灰色圆点
  • 暂停会话显示黄色圆点

8. 依赖关系

8.1 外部依赖

依赖 用途 必需性
OpenCode SDK 项目列表、会话列表、会话状态 API 调用 必需
国际化模块 面板标签、状态文本的多语言支持 必需

8.2 内部依赖

依赖模块 用途
服务器存储 获取所有服务器配置、活动服务器、健康状态
API 通信层 创建独立 SDK 客户端、调用项目和会话 API
消息存储 切换服务器时清理当前会话消息
会话上下文 选中会话后的导航和状态更新

8.3 被依赖模块

依赖本模块的模块 使用内容
侧边栏 触发按钮集成、面板状态管理

9. 架构决策记录

ADR-001:为什么为每个服务器创建独立的 SDK 客户端

决策:面板为每个服务器创建独立的 SDK 客户端实例,而非复用全局 active server 的客户端。

理由

  • 面板需要同时获取多个服务器的数据,每个服务器可能有不同的 URL 和认证信息
  • 全局客户端绑定到当前活动服务器,无法用于查询其他服务器
  • 客户端实例按 "URL + 认证信息" 缓存,避免每次渲染都重建连接

ADR-002:为什么项目列表获取失败时使用兜底分组

决策:当 project.list API 调用失败时,使用一个名为"全部项目"的兜底分组,而非显示错误。

理由

  • 部分服务器可能不支持项目 API 或返回格式不一致
  • 兜底分组确保用户仍能看到会话列表,不会因单个 API 失败而完全阻断
  • 错误信息仅在服务器级别展示,不影响子级数据获取

ADR-003:为什么 OpenCode 标识从链接改为按钮

决策:侧边栏顶部的 OpenCode 标识从 <a href="/"> 改为 <button> 元素。

理由

  • 原有行为是刷新整个页面,中断用户当前工作流
  • 改为按钮后触发快捷面板,提供更流畅的会话切换体验
  • 按钮语义更准确,表示触发动作而非导航

10. 风险与缓解

风险 影响 缓解措施
多服务器并发请求导致性能问题 服务器数量多时面板打开延迟 Promise.allSettled 并行请求,单个失败不影响整体
面板定位在极端窗口尺寸下溢出 小窗口或高分屏下面板超出视口 计算时取 min(rect.left, window.innerWidth - panelWidth - 16)
SDK 客户端缓存导致认证信息过期 密码修改后仍使用旧认证 缓存 key 包含认证信息,密码变化时自动创建新客户端
面板关闭时异步请求仍在进行 已卸载组件尝试更新状态 useEffect cleanup 中设置 cancelled 标志
会话状态 API 不支持或部分返回 部分会话状态显示为未知 状态获取失败时降级为 unknown,不影响列表展示

@fellow99
Copy link
Copy Markdown
Author

以下是plan.md

901-ext-server-quick-panel 技术方案(As-Built)

本文档是对已完成的服务快捷面板模块的回溯性技术规划,记录"实际建成"的架构设计、数据模型与集成策略。


1. Technical Context

1.1 模块定位

服务快捷面板(901-ext-server-quick-panel)是 OpenCodeUI 中提供跨服务器浏览和切换会话的快捷入口。它替换了侧边栏顶部 OpenCode 品牌标识原有的页面刷新行为,改为弹出一个浮动面板,以三级树形结构(服务器 → 工程 → 会话)展示所有已配置服务器及其活跃会话。

1.2 技术栈

维度 选型
框架 React 19 + TypeScript
样式 Tailwind CSS v4
国际化 i18next + react-i18next
状态管理 自定义 Store 模式(serverStore
渲染 createPortal 渲染到 document.body
API 通信 @opencode-ai/sdk/v2/client 独立 SDK 客户端

1.3 源码目录结构

src/features/chat/sidebar/
├── ServerQuickPanel.tsx              # 主面板组件(含 3 个内嵌子组件)
└── SidePanel.tsx                     # 触发器集成(修改)

src/locales/
├── zh-CN/
│   ├── chat.json                     # 新增 3 个键值
│   └── common.json                   # 新增 1 个键值
└── en/
    ├── chat.json                     # 新增 3 个键值
    └── common.json                   # 新增 1 个键值

1.4 文件规模

文件 行数 职责
ServerQuickPanel.tsx 617 面板容器、数据获取、树形渲染、关闭行为
SidePanel.tsx(修改部分) ~30 触发按钮改造、状态管理、会话切换处理
locales/zh-CN/chat.json(新增) 3 面板中文翻译
locales/en/chat.json(新增) 3 面板英文翻译
locales/zh-CN/common.json(新增) 1 服务器计数中文翻译
locales/en/common.json(新增) 1 服务器计数英文翻译

总计约 655 行代码。


2. Constitution Check

对照项目宪法逐项验证:

宪法原则 符合情况 说明
原则 2:OpenCode 兼容性优先 符合 直接使用 @opencode-ai/sdk/v2/client 调用项目/会话 API,无自定义协议
原则 3:多平台统一代码库 符合 纯 React 组件,无平台特定代码,Web 端和桌面端共享同一套实现
原则 4:自定义优于框架依赖 符合 未引入第三方弹出层库(如 Popper/Floating UI),使用原生 createPortal + 绝对定位实现
原则 6:中文优先文档 符合 代码注释、i18n 键名均以中文为第一语言
原则 9:主题与可访问性 符合 使用 CSS 变量主题系统(glass-altborder-border-200/60),ESC 键关闭,点击外部关闭
原则 10:模块化功能架构 符合 自包含于 ServerQuickPanel.tsx,通过 props 与 SidePanel 通信,无全局副作用
约束 C3:构建校验 符合 代码通过 TypeScript 类型检查(tsc --noEmit 零错误)
约束 C4:依赖最小化 符合 零新增 npm 依赖,仅复用已有 SDK、Store 和 Icon 组件

3. Research Findings

3.1 面板定位与渲染架构

面板使用 createPortal 渲染到 document.body,通过绝对定位实现浮动效果。

定位计算逻辑

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,
})

入场动画:通过 isVisible 状态控制 CSS transition:

  • 弹出时:opacity: 0 → 1, scale: 0.95 → 1
  • 关闭时:opacity: 1 → 0, scale: 1 → 0.95
  • 动画时长 150ms,缓动 ease-out
  • 使用 requestAnimationFrame 确保初始状态渲染后再触发动画

关闭防抖isClosingRef 标记防止动画进行中重复触发关闭。

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
}

设计要点

  • 缓存 key 包含 URL 和认证信息,密码变化时自动创建新客户端
  • 认证信息通过 makeBasicAuthHeader 生成 Basic Auth 头
  • 无密码的服务器使用统一的 no-auth key,共享同一个匿名客户端

3.3 数据获取流程

面板打开时的数据获取流程:

1. 获取所有服务器配置和当前健康状态
2. 为每个服务器创建/复用 SDK 客户端
3. 并行执行(Promise.allSettled):
   a. 调用 client.project.list() 获取项目列表
   b. 对每个项目,并行调用:
      - client.session.list() 获取会话列表
      - client.session.status() 获取会话状态
4. 合并数据,更新对应服务器节点状态
5. 单个服务器失败不影响其他服务器

降级策略

  • project.list 失败 → 使用兜底 "全部项目" 分组
  • session.status 失败 → 状态标记为 unknown,不影响列表展示
  • 整个服务器请求失败 → 节点显示错误信息

取消机制useEffect cleanup 中设置 cancelled = true,避免已卸载组件的状态更新。

3.4 会话状态映射

SDK 的 SessionStatus 类型定义:

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']
}

映射关系

  • idleidle(空闲,灰色圆点)
  • busybusy(忙碌,绿色脉冲圆点)
  • retryunknown(重试中,深灰色圆点)
  • 无响应 → unknown(未知,深灰色圆点)

3.5 关闭行为

三种关闭方式:

方式 实现方式
关闭按钮 onClick={handleClose}
点击外部区域 document.addEventListener('mousedown', handler)
ESC 键 document.addEventListener('keydown', handler)

外部点击检测逻辑

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 会话切换处理

SidePanel.tsx 中的处理逻辑:

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 Model

4.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 状态流转图

面板生命周期

[未挂载] → 点击触发按钮 → [挂载 + 加载中] → 数据加载完成 → [展示中]
                                                    ↓
                                          点击会话 → [关闭动画] → [卸载]
                                          点击外部 → [关闭动画] → [卸载]
                                          按 ESC   → [关闭动画] → [卸载]

服务器节点状态

[初始化: isLoading=true] → 数据获取成功 → [展示数据: isLoading=false, error=null]
                        → 数据获取失败 → [显示错误: isLoading=false, error=消息]

5. Interface Contracts

5.1 ServerQuickPanel 组件契约

function ServerQuickPanel({ triggerRef, onClose, onSelectSession }: ServerQuickPanelProps): React.ReactPortal

行为约定

  • 挂载时自动获取所有服务器数据
  • 关闭时触发 onClose 回调(含 150ms 动画延迟)
  • 选中会话时先调用 onSelectSession,再调用 onClose

5.2 内嵌子组件

组件 Props 职责
ServerTreeItem node, isExpanded, expandedProjects, onToggleServer, onToggleProject, onSelectSession 服务器节点展示与展开控制
ProjectTreeItem project, serverId, isExpanded, onToggle, onSelectSession 工程节点展示与展开控制
SessionItem session, serverId, onSelect 会话条目展示与点击选择

5.3 消费的 Store 接口

Store 方法 用途
serverStore getServers() 获取所有服务器配置
serverStore getAllHealth() 获取所有服务器健康状态
serverStore getActiveServer() 获取当前活动服务器(自动展开用)
serverStore makeBasicAuthHeader(auth) 生成 Basic Auth 请求头
messageStore clearSession(sessionId) 切换服务器时清理消息

5.4 消费的 API 接口

API 方法 用途 失败处理
client.project.list() 获取项目列表 使用兜底 "全部项目"
client.session.list() 获取会话列表 该工程会话列表为空
client.session.status() 获取会话状态 状态标记为 unknown

6. Implementation Strategy

6.1 组件层次

SidePanel
├── <button ref={serverPanelTriggerRef} onClick={() => setServerPanelOpen(true)}>
│       OpenCode
│   </button>
│
└── {serverPanelOpen && (
      <ServerQuickPanel
        triggerRef={serverPanelTriggerRef}
        onClose={() => setServerPanelOpen(false)}
        onSelectSession={handleQuickPanelSelectSession}
      />
    )}

ServerQuickPanel (Portal → document.body)
├── Header (标题 + 概览 + 关闭按钮)
└── Scrollable Content
    ├── ServerTreeItem × N
    │   ├── Server Header (名称 + URL + 状态 + 计数)
    │   └── [expanded]
    │       ├── Loading State
    │       ├── Error State
    │       ├── Empty State
    │       └── ProjectTreeItem × M
    │           ├── Project Header (名称 + 计数)
    │           └── [expanded]
    │               └── SessionItem × K
    │                   ├── Status Dot
    │                   ├── Icon
    │                   └── Title

6.2 数据流

用户点击 OpenCode 标识
  ↓
SidePanel.setServerPanelOpen(true)
  ↓
ServerQuickPanel 挂载 (createPortal)
  ↓
useEffect: 获取 serverStore.getServers() + getAllHealth()
  ↓
初始化 ServerNode[] (isLoading=true)
  ↓
fetchAll(): Promise.allSettled(servers.map(...))
  ├── getServerClient(server) → 创建/复用 SDK 客户端
  ├── client.project.list() → 获取项目列表
  ├── Promise.allSettled(projects.map(...))
  │   ├── client.session.list() → 获取会话列表
  │   └── client.session.status() → 获取会话状态
  └── setServerNodes(更新数据)
  ↓
用户点击会话
  ↓
onSelectSession(session, serverId)
  ↓
SidePanel.handleQuickPanelSelectSession
  ├── 跨服务器?→ messageStore.clearSession + serverStore.setActiveServer
  ├── 有 directory?→ addDirectory
  └── getSession → onSelectSession
  ↓
handleClose → 退出动画 → onClose → 卸载

6.3 样式策略

面板使用 Tailwind CSS 工具类,无自定义 CSS:

视觉元素 Tailwind 类名
面板容器 fixed z-[9999] rounded-xl border border-border-200/60 glass-alt shadow-lg
入场动画 transition-all duration-150 ease-out + 条件 opacity/scale
头部 flex items-center justify-between px-3 py-2.5 border-b border-border-200/40 bg-bg-100/60
服务器行 w-full flex items-center gap-2 px-3 py-2 hover:bg-bg-200/40
工程行 w-full flex items-center gap-2 px-3 py-1.5 pl-8 hover:bg-bg-200/40
会话行 w-full flex items-center gap-2 px-3 py-1.5 pl-12 hover:bg-bg-200/50
忙碌状态点 bg-success-100 animate-pulse
当前服务器标签 text-[9px] font-medium text-accent-main-100 bg-accent-main-100/10 rounded-full

7. Error Handling

7.1 错误场景与处理

场景 处理方式
服务器连接失败 节点显示错误信息,不影响其他服务器
项目 API 不可用 使用兜底 "全部项目" 分组
会话状态 API 返回异常 状态标记为 unknown,不影响列表展示
会话列表 API 失败 该工程下显示空列表
面板关闭时请求仍在进行 cancelled 标志阻止 setState
触发按钮 ref 为空 定位 useEffect 提前 return
关闭动画中重复触发关闭 isClosingRef 标志阻止重复执行
面板弹出位置超出视口 Math.min(rect.left, window.innerWidth - 336) 限制

7.2 边界条件

  • 无服务器配置:面板显示空列表(header 仍可见)
  • 服务器无项目:展开后显示"暂无项目"提示
  • 工程无会话:Chevron 图标不可见,按钮 disabled
  • 窗口尺寸极小:面板最大高度取 min(520, window.innerHeight - top - 16)

8. Testing Considerations

8.1 组件测试覆盖点

组件 测试场景
ServerQuickPanel 弹出定位、数据加载、关闭行为、ESC 键、外部点击
ServerTreeItem 展开/收起、健康状态图标、当前服务器标识
ProjectTreeItem 展开/收起、忙碌计数、无会话时 disabled
SessionItem 状态点颜色、点击选择、hover 效果
SidePanel 集成 触发按钮点击、会话切换、跨服务器切换

8.2 集成测试场景

场景 验证点
跨服务器切换 消息清理 + 服务器切换 + 会话打开 + 面板关闭
同服务器切换 直接打开会话,不清理消息
多服务器并发加载 所有服务器独立加载,失败互不影响
面板快速开关 快速点击打开/关闭,不出现状态混乱

8.3 手动测试清单

  • 面板在触发按钮正下方弹出,间距 8px
  • 面板不超出视口右边界
  • 当前活动服务器默认展开并高亮
  • 忙碌会话显示绿色脉冲并排在前面
  • 点击面板外部区域关闭
  • 按 ESC 键关闭
  • 跨服务器切换后会话正确打开
  • 关闭动画流畅无闪烁

9. 依赖关系总结

9.1 外部依赖

依赖 用途 必需性
@opencode-ai/sdk/v2 项目/会话 API 调用 必需
i18next 面板文本国际化 必需

9.2 内部依赖

依赖模块 使用内容
serverStore 服务器配置、健康状态、Basic Auth 生成
messageStore 切换服务器时清理会话消息
getSession (API) 获取会话详细信息
Icons Globe、Folder、ChevronDown、Wifi、MessageSquare 等
getDirectoryName 从工作目录路径提取目录名
formatPathForApi 路径格式化为 API 兼容格式

9.3 被依赖模块

模块 使用内容
SidePanel 触发按钮、面板状态管理、会话切换

10. 风险区域

10.1 多服务器并发请求性能(低风险)

每个服务器需要 1 + 2N 次 API 调用(1 次项目列表 + N 个工程 × 2 次会话列表/状态)。当服务器数量多、工程数量多时,总请求数可能较大。

缓解

  • 所有请求使用 Promise.allSettled 并行执行
  • 会话列表限制 limit: 50
  • 单个服务器失败不影响整体

10.2 SDK 客户端缓存与认证过期(低风险)

客户端按 "URL + 认证信息" 缓存,如果服务器密码在服务端被修改,客户端仍使用旧认证。

缓解

  • 健康状态检测会暴露认证失败(unauthorized 状态)
  • 用户在设置面板修改服务器配置后,缓存 key 变化,自动创建新客户端

10.3 面板定位在动态布局下的准确性(低风险)

触发按钮位置可能因侧边栏折叠/展开而变化。

缓解

  • 定位计算在组件挂载时执行一次,面板打开期间触发按钮位置不变
  • 如需响应窗口 resize,可在后续迭代中添加 resize 监听

生成时间: 2026-04-13
模块版本: OpenCodeUI v0.4.8

@fellow99
Copy link
Copy Markdown
Author

以下是task.md

901-ext-server-quick-panel 实施任务清单

模块编号:901-ext-server-quick-panel
状态:✅ 已完成
最后更新:2026-04-13


任务概览

# 任务 状态 产出文件
T-1 创建 ServerQuickPanel ✅ 完成 ServerQuickPanel.tsx (617 行)
T-2 改造 SidePanel 触发器 ✅ 完成 SidePanel.tsx 修改 (~30 行)
T-3 补充 i18n 翻译 ✅ 完成 chat.json + common.json (中英各 4 键)
T-4 TypeScript 类型验证 ✅ 完成 tsc --noEmit 零错误

T-1:创建 ServerQuickPanel 组件

状态:✅ 已完成

目标:创建独立的浮动面板组件,以三级树形结构展示服务器 → 工程 → 会话。

子任务

# 子任务 状态 说明
T-1.1 定义内部数据结构 ✅ 完成 SessionInfoProjectGroupServerNode 接口
T-1.2 实现 SDK 客户端缓存 ✅ 完成 clientCache Map + getServerClient() 函数
T-1.3 实现面板定位计算 ✅ 完成 基于 trigger.getBoundingClientRect() 计算弹出位置
T-1.4 实现数据获取逻辑 ✅ 完成 fetchAll() 并行请求,含降级处理和取消机制
T-1.5 实现状态映射函数 ✅ 完成 extractStatusType() 将 SDK SessionStatus 映射为简化枚举
T-1.6 实现面板头部 ✅ 完成 标题、概览信息、关闭按钮
T-1.7 实现 ServerTreeItem 子组件 ✅ 完成 服务器名称、URL、健康状态、展开/收起
T-1.8 实现 ProjectTreeItem 子组件 ✅ 完成 工程名称、会话计数、忙碌计数、展开/收起
T-1.9 实现 SessionItem 子组件 ✅ 完成 会话标题、状态指示点(四色)、点击选择
T-1.10 实现关闭行为 ✅ 完成 外部点击检测、ESC 键、关闭按钮、退出动画
T-1.11 添加完整注释 ✅ 完成 所有函数、接口、变量均有必要注释

验收

  • 面板在触发按钮下方弹出,定位正确
  • 三级树形结构可展开/收起
  • 数据加载显示 loading/error/empty 状态
  • 会话状态指示点颜色正确
  • 关闭动画流畅
  • TypeScript 编译通过

T-2:改造 SidePanel 触发器

状态:✅ 已完成

目标:将 OpenCode 标识从 <a href="/"> 改为 <button>,集成 ServerQuickPanel。

子任务

# 子任务 状态 说明
T-2.1 添加导入 ✅ 完成 导入 ServerQuickPanelserverStoremessageStore
T-2.2 添加面板状态 ✅ 完成 serverPanelOpen state + serverPanelTriggerRef ref
T-2.3 改造触发按钮 ✅ 完成 <a href="/"><button>,保留视觉样式
T-2.4 实现会话切换处理 ✅ 完成 handleQuickPanelSelectSession 回调
T-2.5 渲染 ServerQuickPanel ✅ 完成 条件渲染,传入 triggerRef/onClose/onSelectSession

验收

  • 点击 OpenCode 标识弹出面板(不刷新页面)
  • 跨服务器切换时清理消息并导航
  • 同服务器切换时直接打开会话
  • TypeScript 编译通过

T-3:补充 i18n 翻译

状态:✅ 已完成

目标:为面板新增的 4 个 i18n 键值补充中英文翻译。

新增键值

键值 zh-CN en 文件
chat.sidebar.servers 服务器 Servers chat.json
chat.sidebar.sessions 会话 Sessions chat.json
chat.sidebar.noProjects 暂无项目 No projects chat.json
common.servers {{count}} 个服务器 {{count}} server(s) common.json

验收

  • 所有 JSON 文件语法正确
  • 中英文键值一一对应
  • 面板中所有文本正确显示

T-4:TypeScript 类型验证

状态:✅ 已完成

目标:确保所有新增代码通过 TypeScript 类型检查。

验证结果

  • tsc --noEmit 零错误
  • LSP 诊断无错误
  • as any / @ts-ignore / @ts-expect-error 类型压制

依赖关系

T-1 (ServerQuickPanel) ──┐
                          ├──→ T-2 (SidePanel 集成)
T-3 (i18n) ──────────────┘
                                    ↓
                              T-4 (类型验证)
  • T-1 和 T-3 可并行执行
  • T-2 依赖 T-1 和 T-3 完成
  • T-4 在所有代码完成后执行

产出清单

文件 行数 类型
src/features/chat/sidebar/ServerQuickPanel.tsx 617 新增
src/features/chat/sidebar/SidePanel.tsx ~30 修改
src/locales/zh-CN/chat.json 3 修改
src/locales/en/chat.json 3 修改
src/locales/zh-CN/common.json 1 修改
src/locales/en/common.json 1 修改
specs/901-ext-server-quick-panel/spec.md ~440 新增
specs/901-ext-server-quick-panel/plan.md ~400 新增
specs/901-ext-server-quick-panel/checklists/requirements.md ~80 新增
specs/901-ext-server-quick-panel/tasks.md 本文件 新增

生成时间: 2026-04-13

@lehhair
Copy link
Copy Markdown
Owner

lehhair commented Apr 13, 2026

大概明白你的需求了,也许我应该考虑直接在侧边栏支持直接渲染多服务器会话?

@fellow99
Copy link
Copy Markdown
Author

大概明白你的需求了,也许我应该考虑直接在侧边栏支持直接渲染多服务器会话?

对,其实就是希望可以同时看到多个opencode服务的各个工程以及session状态。
融入到侧边栏就更方便了。

@lehhair
Copy link
Copy Markdown
Owner

lehhair commented Apr 13, 2026

大概明白你的需求了,也许我应该考虑直接在侧边栏支持直接渲染多服务器会话?

对,其实就是希望可以同时看到多个opencode服务的各个工程以及session状态。 融入到侧边栏就更方便了。

可以,后续应该会做,你这个放到OpenCode字样上做的话可能并不是很符合我想要的交互设计,你可以先这样自用着,我先试试其它的方案

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants