Skip to content

fix(web): 修复会话工作台三栏布局、命名与日志历史 - #48

Merged
ZeroPointSix merged 5 commits into
mainfrom
fix/zer-715-workbench-polish
Aug 15, 2026
Merged

fix(web): 修复会话工作台三栏布局、命名与日志历史#48
ZeroPointSix merged 5 commits into
mainfrom
fix/zer-715-workbench-polish

Conversation

@ZeroPointSix

@ZeroPointSix ZeroPointSix commented Aug 15, 2026

Copy link
Copy Markdown
Owner

What problem does this solve?

Closes: 无对应 GitHub issue —— 本改动追踪于 Linear ZER-715 优化前端效果。本仓库没有对应 issue,因此 Closes # 刻意不填数字,避免误关闭无关 issue。

Discord Discussion URL: 待补充 —— 尚未从需求提出方取得。为此本 PR 以 draft 形式打开,链接补齐后立即转正式评审;见 ### Follow-ups

PR #47 上线的只读会话工作台(/admin#/sessions)在实际使用中暴露 5 个缺陷,全部来自使用方反馈:

  1. 三栏明显不对称(“你一看一点都不对称了”)。左右两栏宽度声明不同,中栏在任何视口下都是偏心的。
  2. 存在一道多余的“划痕”。中栏左右两边的分隔线不成对:左边有缝、右边没有。
  3. 侧边栏命名不可读(“这侧边框这名字这写的真难看呢”)。列表直接显示 slack:alpha 这类原始 session id,并按 agent 类型分组。
  4. 运行状态标签冗余(“一个单独的运行中、啊空闲呢还是太麻烦了”),且底部状态条重复渲染了一次。
  5. 日志永远只显示前两条(“他有的时候是运行多次了,但是你永远只会展示前面两次,根本无法正常返回实际的情况”)。

第 1–4 条是观感/信息架构问题;第 5 条是功能性 bug,日志面板从未接过真实数据。

Review Contract

Goal

只读会话工作台的可读性收口:三栏几何对称、分隔线成对、左右栏可折叠;侧边栏以“项目 + 任务标题”而非原始 session id 组织;运行状态收敛为一个紧凑指示器;移除空壳 告警 tab;日志面板改接服务端 transcript 全量历史。

Non-goals

  • 不引入任何写操作。工作台保持只读。
  • 后端仅增加会话标题派生与列表字段,不引入写操作。
  • 不把标题写入 SessionSnapshot 持久状态;会话列表从 transcript 内存数据附带轻量 title 字段。
  • 不做 推理强度 相关的展示/配置改造(需求方明确要求本轮不做:“当然这个先不是我们重要的,我们先优先改绘画这个界面吧”)。
  • 不做暗色主题、不做虚拟滚动性能优化、不做代码分包(app.js 体积告警是既有状态)。

Accepted Residual Risks

  • 无浏览器视觉回归基线。 本仓库没有 Playwright / 截图基线,三栏对称与折叠导轨仅经 CSS 静态审查与构建验证,未经像素级断言。对称性由单一 --workbench-side-width token 保证,属结构性修复而非数值微调,因此风险可接受。
  • 标题在后端内存派生。 任务标题取 transcript 第一条 user 消息并随会话列表返回;服务重启后、首条 transcript 尚未重新进入内存前会回退到 profile 名。
  • SessionEventLog 渲染纯文本摘要(单条截断 160 字符,换行压平为 )。结构化 tool_call / tool_result 的富展示仍由 时间线 与活动流承担,日志面板刻意保持“只读文本流”定位。
  • useSessionStream 仍在 invalidate 已不再使用的 ['sessionTimeline'] query key。 无副作用(无人订阅该 key),本 PR 不顺手清理以保持单一变更范围,见 Follow-ups。

Acceptance Criteria

  • 左右两栏在所有视口下宽度相同(同一 --workbench-side-width token)。
  • 中栏左右各恰好一条分隔线,不多不少。
  • 桌面端左右栏均可折叠为 44px 导轨,并可再次展开。
  • 移动端抽屉使用样式表中真实存在的 .workbench-mobile-drawer 类。
  • 侧边栏按项目目录分组,行内显示任务标题,原 platform · thread_id 降级为副标题。
  • 运行中显示 spinner;非运行中显示“上次运行 <相对时间>”,hover 显示绝对时间。
  • 主面板底部不再出现重复的 stream status 块。
  • 告警 tab 消失,profile_config_errors 内联在 元数据 tab 中。
  • 日志面板条数等于服务端 transcript 全量条数,不再是常量 2。
  • pnpm linttsc --noEmit)零错误。
  • pnpm test 全绿(11 文件 / 66 用例)。
  • pnpm build 成功,并按仓库惯例提交 promote 后的产物。

Follow-ups

  • 标题持久化:若后续需要跨服务重启保留标题,可将其落入持久会话元数据;本轮保持轻量内存派生。
  • 推理强度 展示与配置:本轮按需求方要求推迟,不并入本 PR。
  • 清理 useSessionStream 中已失效的 ['sessionTimeline'] invalidate,并一并评估 SessionEventTimeline 是否仍需保留。
  • 补齐本 PR 的 Discord Discussion URL 并将 draft 转为正式评审。

At a Glance

BEFORE
 ├─ 260–300px ─┤                          ├─ 280–340px ─┤   ← 两栏宽度永不相等
┌──────────────┬─────────────────────────┬───────────────┐
│ SIDEBAR      │ MAIN                    │ INSPECTOR     │
│ 按 Agent 分组│  [运行中] [空闲]        │ 元数据        │
│  ├ codex     │                         │ 告警  ← 空壳  │
│  │  slack:α  │  activity feed          │ 时间线        │
│  ├ claude    │                         │ 日志 (恒为 2) │
│  │  discord:β│  stream status          │               │
│              │  stream status ← 重复   │               │
└──────────────┴─────────────────────────┴───────────────┘
                ↑ 有分隔线                ↑ 这条边没有分隔线

AFTER
 ├─── 300px ───┤                          ├─── 300px ───┤   ← 共用同一个 token
┌──────────────┬─────────────────────────┬───────────────┐
│ SIDEBAR   ⟨⟩ │ MAIN                    │ ⟨⟩ INSPECTOR  │
│ 按项目分组·2 │  ◐ 运行中               │ 元数据 + 告警 │
│  ├ alpha     │  (或:上次运行 3 分钟前)│ 时间线       │
│  │  优化前端 │                         │ 日志 (全量)   │
│  ├ beta      │  activity feed          │               │
│  │  修复回归 │                         │               │
│              │  stream status          │               │
└──────────────┴─────────────────────────┴───────────────┘
                ↑ 分隔线                  ↑ 分隔线
        ⟨⟩ = 可折叠为 44px 导轨

Prior Art & Industry Research

  • Factory(需求方点名的参考对象:“你就直接去参考别人 factory 他们那种怎么写的”)。Factory 自己的 changelog 记录了一条同类修复:“Sidebar staleness — Fixed missing last message and stale status indicators on the session sidebar (Factory App)”https://docs.factory.ai/changelog/release-notes)。侧边栏“最后一条消息 + 状态指示器”这一组合,与本 PR 的“任务标题 + 紧凑运行指示器”方向一致。
  • Hermes Agenthttps://github.com/NousResearch/hermes-agent/tree/main/web)。官方 web dashboard 的定位就是 “monitoring active sessions”。社区实现 nesquena/hermes-webui 采用 “three-panel Claude-style layout with sessions sidebar, chat, and workspace file browser”,与本仓库三栏结构同构;其特性列表明确包含 “Session projects, tags, and tool call cards” —— 即以项目维度组织会话,直接支持本 PR 的 P0-2。
  • OpenClawhttps://docs.openclaw.ai/web/control-uihttps://docs.openclaw.ai/web/dashboard)。这是一条反向证据:社区面板 xmanrui/OpenClaw-bot-review 明确采用“按 Agent 浏览所有会话”,并单独设置“告警中心”—— 与本 PR 的两项决定相反。仍然选择偏离,理由是:(a) 我们的 agent 维度基数极低(实际只有 codex / claude 两三种),分组后几乎不产生区分度,而 workdir 项目维度才是使用方真正用来定位会话的;(b) OpenClaw 的告警中心有真实多源内容(模型不可用、机器人无响应等可配置规则),而我们的 告警 tab 唯一数据源是 profile_config_errors,内联到 元数据 旁边信息密度更高。
  • 同类需求的独立复现manaflow-ai/cmux #149 “Sidebar loading indicator when AI coding agent is working”anomalyco/opencode #14400 均独立提出“侧边栏用 spinner 指示 agent 正在工作”。说明 P0-3 的 spinner 化不是个人偏好,而是这类工具的收敛设计。

Proposed Solution

P0-1 布局(styles.cssSessionWorkbenchPage.tsx

  • 新增 --workbench-side-width: 300px--workbench-rail-width: 44px--workbench-seam 三个 token。grid-template-columns 左右两侧同时引用 --workbench-side-width,消除 minmax(260px,300px) vs minmax(280px,340px) 的不对称根因。
  • 分隔线改为“每边恰好一条”:.workbench-sidebar 只出 border-inline-end.workbench-inspector 只出 border-inline-start。原实现把两栏一起赋 border-inline-end 后又用 .workbench-inspector { border-inline-end: 0 } 抹掉,导致中栏右侧完全无缝、左侧有缝 —— 这就是被看成“划痕”的来源。
  • 新增 is-sidebar-collapsed / is-inspector-collapsed 状态类,把对应列压到导轨宽度,配合 .workbench-rail 与展开按钮。
  • 顺带修正:移动端抽屉原本使用样式表里不存在.workbench-drawer,改为真实定义的 .workbench-mobile-drawer

P0-2 命名(lib/session.tsSessionSidebar.tsx

  • 新增 sessionProjectGroup():取 workdir 末段目录名,空值回退 未归类项目。分组从 agent 维度切到项目维度。

  • 后端 SessionTranscriptStore 从首条 user 消息派生并缓存最多 48 字符的标题,会话列表接口随 snapshot 返回 title

  • sessionListTitle(session, derivedTitle?) 的优先级为:详情页派生标题 → 列表接口 session.titleprofile_nameAgent · 短 id;SSE snapshot 更新时保留已有标题。

  • 侧边栏 caption 改为 按项目分组 · N 条nav aria-label 同步为 按项目分组的会话
    P0-3 状态(SessionRunIndicator.tsxStatusTag.tsxSessionMainPanel.tsx

  • 新增 SessionRunIndicatordisplay.running 为真时渲染 spinner,否则渲染 上次运行 <相对时间>,外套 Tooltip 显示绝对时间。

  • StatusTagrunning 图标由静态 CheckCircleFilled 改为 LoadingOutlined spin(该组件在其他页面仍在使用,故保留而非删除)。

  • 删除主面板底部重复的 workbench-stream-status 块。

  • 侧边栏行内的 StatusTag 换成 6px 状态点,title / aria-label 仍取自 sessionStatusDisplay,保住可访问性。

实现注意:sessionStatusDisplay.idle.active === true,所以“是否正在执行”必须读 .running 而非 .active

P1-4 检查器(SessionInspector.tsx

  • 删除 告警 tab 及其 AlertItem / isAlertItem() / AlertOutlinedprofile_config_errors<Alert> 内联到 元数据 tab 中它所描述的元数据旁边。

P1-5 日志(SessionEventLog.tsxSessionWorkbenchPage.tsx)—— 真正的 bug

  • 根因:页面维护了一个纯客户端的 ['sessionTimeline', id] query,它从不发起任何请求,只被一个 useEffectinitialTimeline 播种 1–2 条合成条目。因此不论会话实际运行多少次,日志面板永远只有那两条。
  • 修复:删除该假 query、播种 effect 与 initialTimeline 导出;SessionEventLog 的 prop 由 timeline: SessionTimelineItem[] 改为 entries: TranscriptEntry[],按 sequence 排序渲染服务端 transcript 全量历史。
  • 同时把 useSessionTranscript(sessionId) 的调用上提到页面层,经 prop 下发给主面板与检查器。此前主面板自己也调用了一次,等于对同一会话开了两条 SSE 连接。

Why this approach?

  • 对称性用 token 而不是数值对齐来保证。 把两栏指向同一个 CSS 变量后,“不对称”在结构上不可能复现;若只把 340px 改成 300px,下一次有人再动其中一侧就会重新裂开。
  • 分隔线按“归属边”建模。 让每个面板只声明自己朝向中栏的那一条边,避免了“先统一赋值再局部抹掉”这种会产生幽灵缝隙的写法。
  • 标题复用 agentcore 已有的语义,符合需求方要求(“你看一下 agEnco 就是我们底层的这个 agent 对于这个绘画的命名是什么?然后复用一下不就好了”):agentDisplayName / sourcePlatformLabel 都是 lib/format.ts 里既有的函数,本 PR 没有新造命名体系。
  • 日志直接消费 transcript,而不是再修补那套合成 timeline。合成数据正是这个 bug 的成因;继续在其上加载入口只会把假数据变成“更多假数据”。
  • SSE 连接上提到页面层,顺带把一个此前无人注意的重复连接消掉,且没有引入新的状态管理层。

Alternatives Considered

  1. 只把右栏宽度改成 300px,不引入 token。 最小改动,但无法防止回归,且不解决分隔线问题。已否。
  2. SessionTimelineItem 补一个真实的后端接口。 需要后端配合新增 endpoint,而 transcript 接口已经提供了完全够用的全量历史,属重复建设。已否。
  3. 保留 告警 tab,只在无内容时隐藏。 需求方的判断是“如果说没有这些作用的话,你就不要展示出来”;条件隐藏会让 tab 位置在有/无告警时跳动,信息密度也不如内联。已否。
  4. 用 antd Splitter 做可拖拽分栏替代折叠。 交互更强,但引入拖拽状态持久化与尺寸问题,超出本轮“观感收口”的范围。已否,可作为后续独立提案。
  5. 在侧边栏保留完整状态标签而非状态点。 与“运行中/空闲太麻烦”的反馈直接冲突。已否。

Validation

kuoya-sgp-001 上于 web/ 目录执行,全部通过:

门禁 命令 结果
依赖 pnpm install --frozen-lockfile OK,pnpm 10.14.0,lockfile 未变
类型 pnpm linttsc --noEmit 零错误
单测 pnpm testvitest run 11 文件 / 66 用例 全绿,4.02s
构建 pnpm buildvite build && promote-build 成功,7348 modules,49.28s
  • lib/session.test.ts 由 10 个用例扩到 14 个,新增覆盖 sessionProjectGroup(含 workdir: ''未归类项目)、titleFromTranscript(含空数组 → undefined、首尾空格 trim)、以及 sessionListTitle 的三级回退。原有 9 个用例逐字保留未改。
  • 受影响的既有测试 hooks/useSessionTranscript.test.tslib/transcript.test.ts 均未回归。
  • 按仓库惯例提交了 promote-build.mjs 产出的 web/app.jsweb/styles.css(与 afea34b47082e4 一致);web/index.html 内容未变化。
  • Rust 校验:openab-core 标题派生单测与 openab-gateway 会话列表集成测试均通过;前端 lint、66 项单测与生产构建也全部通过。
  • 未执行浏览器视觉回归:本仓库无截图基线,已如实登记在 ### Accepted Residual Risks

ZER-715. Read-only session workbench polish.

P0-1 layout
- Both side columns now share one --workbench-side-width token. They were
  minmax(260px, 300px) and minmax(280px, 340px), so they never lined up.
- Exactly one seam per centre-column edge. .workbench-inspector cleared its
  own border-inline-end, so the centre/right edge had no seam at all while
  the left seam stayed visible.
- Both side panels collapse to a narrow rail on desktop.
- Mobile drawers use .workbench-mobile-drawer, the class the stylesheet
  actually defines; they previously used an undefined .workbench-drawer.

P0-2 naming
- The sidebar groups by project directory instead of agent type, and rows
  show a task title instead of the raw "slack:alpha" session id.
- Titles prefer a transcript-derived task name, then the profile name, then
  "Agent - short id". Source and thread moved to the subtitle.

P0-3 status
- A compact run indicator replaces the running/idle tag pair: a spinner while
  the session is running, otherwise the time since the last run.
- Removed the duplicated stream status block from the main panel footer.

P1-4 inspector
- Dropped the standalone alerts tab. Profile config warnings now render
  inline next to the metadata they describe.

P1-5 log history
- The page kept a client-only ['sessionTimeline'] query that never fetched
  anything and was seeded with one or two synthetic items, so a session that
  had run many times only ever showed the first couple of entries. The log
  now renders the server-backed transcript history.
- The transcript stream is owned by the page so the activity feed and the log
  share a single SSE connection.

Validation: pnpm lint (tsc --noEmit), pnpm test (66 tests, 11 files), pnpm build.
@linear-code

linear-code Bot commented Aug 15, 2026

Copy link
Copy Markdown

ZER-715

@ZeroPointSix ZeroPointSix left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本轮审查发现 1 个合并前阻塞项:当前实现没有让侧边栏的所有会话显示任务标题,因此不满足 Review Contract 中“行内显示任务标题”的验收标准。由于当前 GitHub 连接账号是 PR 作者,平台不允许提交 REQUEST_CHANGES,故以 COMMENT review 留痕。请修复内联问题并补回归测试后再复审。

Comment thread web/src/components/session-workbench/SessionSidebar.tsx Outdated
ZeroPointSix and others added 3 commits August 15, 2026 08:32
P1 residual from PR #48 review: even after SessionSnapshot.title is
populated by the list API, SessionSidebar still only passed a derived
title for the active row. Inactive same-profile sessions therefore kept
falling back to profile_name and stayed indistinguishable.

Pass session.title for non-active rows, keep the live transcript title
for the selected session, and add a regression test that two same-profile
sessions keep distinct list titles.

Co-Authored-By: Claude <noreply@anthropic.com>
admin_api_integration expected SessionSnapshot.agent to equal the
profile agent_type slug ("claude"), but the snapshot field is filled
from ACP initialize agentInfo.name. The fake agent announces
"fake-claude-acp", so assert that value and keep profile_id as the
profile-binding check.

Co-Authored-By: Claude <noreply@anthropic.com>

@ZeroPointSix ZeroPointSix left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

重新审查最新提交:后端 title 派生、列表 API、前端回退/SSE 保留和 transcript 日志链路均闭合;原 P1 已解决,Admin Web、workspace CI、两组 clippy、ACP 测试和 unified 构建均通过。仅留一项非阻断的 P2 无障碍意见。

Comment thread web/src/pages/SessionWorkbenchPage.tsx
Desktop main-panel buttons collapse expanded side panels, but their
aria-labels always said “打开…”. Pass state-aware labels from the page
and rename the props to onToggle* so the control semantics match.

Co-Authored-By: Claude <noreply@anthropic.com>
@ZeroPointSix
ZeroPointSix marked this pull request as ready for review August 15, 2026 09:23
@ZeroPointSix
ZeroPointSix merged commit 2b1b4f9 into main Aug 15, 2026
11 checks passed
@ZeroPointSix
ZeroPointSix deleted the fix/zer-715-workbench-polish branch August 15, 2026 09:23
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.

1 participant