Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions docs/webui.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,107 @@ writes into `cs.chat`; switched-away writes via
`workspace` and `workspaceFallback` so post-mortems can answer
"why did the file tree jump".

## Sidebar (webui-parity ticket 47)

The sidebar was aligned with the reference client across three areas:
collapse behaviour, nav rows, and the session list. This section is the
contract; `docs/webui.zh-CN.md`'s sidebar section is the user-facing twin.

### Collapse contract

| Constant | Value | Where |
| --- | --- | --- |
| `SIDEBAR_RAIL` | `64` (px) | `components/shell.tsx` — raised from 52 to match the reference rail |
| `SIDEBAR_MIN` / `SIDEBAR_MAX` / `SIDEBAR_DEFAULT` | `240` / `400` / `240` (px) | drag-resize clamp, unchanged |
| `SIDEBAR_AUTO_COLLAPSE_PX` | `980` (px) | `innerWidth < 980` collapses; never auto-expands |
| Width transition | `180ms cubic-bezier(.2,.7,.2,1)` | on the outer wrapper, matching the reference's `.webui-rail` |
| Collapsed background | `bg-transparent` | expanded keeps `bg-bg_default_scrim` |
| Collapsed toolbar compensation | `pl-[142px]` | wraps the toolbar slot in `AppShell`, the reference's own constant |

The collapse toggle is a `size-8` button on a `pointer-events-none` overlay
**outside** the sidebar (`data-testid="sidebar-collapse-toggle"`, expanded at
`left: 8px`, collapsed at `SIDEBAR_RAIL + 8`), so it stays reachable in both
states instead of living inside the clipping card. Its `aria-label` flips
with state — `sidebar.expand` / `sidebar.collapse`, "Expand navigation bar" /
"Collapse navigation bar" (展开导航栏 / 收起导航栏) — and `aria-expanded`
carries the machine-readable half.

The collapsed form is an **icon rail**, not the reference's emptied strip:
rows and the avatar menu stay reachable. This is a deliberate keep — the
reference's own collapsed shell hides everything and its rail-mode
`UserMenu` branch never executes there.

Persistence: the collapsed flag round-trips through the existing
`webui:ui-state` payload (`sidebarCollapsed`, see `lib/persist.ts`
`readShellCollapsedFromPersistedState` / `writePersistedShellCollapsed`).
**No new storage key was added**, so the persistence-keys section is
unchanged.

### Nav-row activation

Activation is a pure function, `lib/sidebar-nav.ts#isSidebarNavActive`, so
the regression suite drives the decision rather than rendered classes:

- `sidebar.search` lights when the tree column's active tab is the `search`
surface;
- `sidebar.plugins` lights for the `plugins` surface (the legacy
`panel === "plugins"` mirror agrees with `openSurfaceTab`);
- `topbar.newSession` lights only with **no session selected and no sibling
surface active** — the reference's home-mode rule for its 新建任务 row.

The page computes the tree-surface signal (`activeNavSurface`, passed
through `AppShell`) because the tab strip state lives in `page.tsx`; the
session-id half comes from the store inside the shell. The active token is
`bg_interaction_tertiary_hover` held permanently — the reference's own
nav-active treatment, distinct from the session-row rule below.

定时 / 网站 / 远程 are **not rendered**: the repo's standing decision is
that a control without a contract behind it is omitted, not shipped inert.
They remain a product decision (see HANDOVER's 18094 fusion list).

### Session-list contract

- Selected rows (`sidebar-session-row`, `sidebar-subagent-row`) paint with
`bg-bg_interaction_tertiary_selected`; hover uses
`bg_interaction_tertiary_hover`. Before ticket 47 both states wrote the
hover token, which made the open session indistinguishable from any
hovered row. Honest boundary: upstream defines the two tokens as the
**same value in the light theme** (both resolve to `--opacity_black_1_4`,
see `tokens.css`), so the visual distinction holds only in the **dark
theme** (hover `opacity_white_0_4` vs selected `opacity_white_0_8`); the
light-theme equality is the upstream token set's current state, not a
regression introduced here.
- Session and subagent rows are `<a href={sessionHref(id)}>` deep links over
the **`?session=` query grammar** (`lib/url-restore.ts#sessionHref`), not
the reference's `#session=` fragment: the restore pipeline (cold load,
popstate, replaceState sync) parses the query string, and
`writeSessionToUrl` preserves fragments, so a fragment href would linger
beside the query parameter. Plain left clicks are intercepted into the
same `switchSession` call as before; modified clicks (middle / cmd /
ctrl / shift) fall through to the browser, and the URL they open is one
the cold-load path already restores.
- Disclosures (project → directories → subagents) mount through
`session-tree.tsx#Expandable`, the reference's `.webui-expandable-motion`
(grid-template-rows `0fr→1fr`, 180ms, plus a 140ms opacity crossfade);
`globals.css` carries the rule under the same class name with a
`prefers-reduced-motion` branch that drops the transition but keeps the
open/closed state. Children stay mounted while collapsed (wrapped in
`inert` + `aria-hidden`).
- The reveal mechanism stays **per-directory, 6 at a time**
(`SESSION_VISIBLE_LIMIT`); no global Load-more was introduced.
- `GET /api/session-tree` and `GET /api/sessions` are **read-only in this
ticket** — no request parameter or response field changed.

### What this ticket does not do

The reference has a right-click context menu (pin / archive / fork / delete)
and a localStorage pin+archive overlay; both need server contracts that do
not exist yet (no pin, archive or copy endpoints) and are deferred as a
batch. The Agent Team badge, the `workspaceDir` secondary line and the
「最近任务」 section exist in reference files but are not rendered by the
reference's own shell, so they are not implemented here either. A later
agent must not mistake any of these for "implemented but broken".

## Context window (what the picker shows, and what a pick does today)

The model picker's detail area (below the provider list) mounts a context-window radio group only for the active model, and only when its `/api/models` entry carries at least two `contextWindowOptions`. A model without the field — or with a single option, which would be a no-op choice — renders no control and no placeholder. Today that is exactly `MiniMax-M3` and `MiniMax-M3.1-Flash-Preview` (`[512000, 1000000]`); every other catalogue entry stays field-free. Each option label is a compact token count (`512K`, `1M`), and an option the engine hints as `higher_usage` (`contextWindowOptionHints`) carries a "higher usage" tag.
Expand All @@ -308,7 +409,10 @@ below cites the component file and one `data-testid` per surface.
| Surface | Component | Anchor `data-testid` |
| --- | --- | --- |
| Sidebar (rail) | `components/shell.tsx` | `sidebar-scroll-viewport` |
| Sidebar collapse toggle (overlay outside the rail, ticket 47) | `components/shell.tsx` | `sidebar-collapse-toggle` |
| Sidebar session tree | `components/session-tree.tsx` | `sidebar-session-row` |
| Session-tree section header (plain text, ticket 47) | `components/session-tree.tsx#SectionHeader` | `sidebar-section-header` |
| Session-tree error state (`role="alert"`, ticket 47) | `components/session-tree.tsx` | `sidebar-tree-error` |
| Sidebar user menu (settings / check-in / usage / sign-out) | `components/shell.tsx#SidebarFooter` | `sidebar-user-menu` |
| Sidebar inbox (alerts flyout) | `components/inbox.tsx` | `inbox-flyout` |
| Toolbar (top bar with model selector) | `components/toolbar.tsx` | `toolbar-session-status` |
Expand Down
31 changes: 31 additions & 0 deletions docs/webui.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,34 @@ exec 回合的代价——以下都是当前真实存在的行为,选择权限
- 响应负载多了 `session.workspace` 和 `session.workspaceFallback`;尾部 `pushStateFor(cid)` 推 SSE 时原样带上新的 `cs.workspace.dir`,`FilesPanel` 通过 `useSessionContext()` 订阅自然重新渲染,前端**不**需要为了这次修复改动任何接线。
- `routes/sessions.js#_eventsAppend("session.switch", …)` 写入 `workspace` 和 `workspaceFallback`,事后追查"为什么文件树跳了"时可以从审计链里直接定位。

## 左侧边栏(webui-parity ticket 47)

左侧边栏对照参照实现做了三块对齐:折叠行为、导航项、会话列表。用户看到的变化与不变化如下。

### 折叠行为

- **折叠宽度 64px**(原 52px),折叠/展开是 180ms 的宽度过渡。折叠后侧栏背景转为透明,只剩一条图标轨——**不是**参照里折叠后整条清空的空竖条;图标轨可用性更高(新建/搜索/插件与头像菜单都还在),是保留的现有能力。
- **折叠开关移到了侧栏之外**:一个浮在界面上层的 32px 按钮,展开时贴侧栏左上角,折叠后浮在图标轨右侧。它的读屏标签随状态在「展开导航栏 / 收起导航栏」之间切换(中英两语都切)。
- **折叠时主内容有左补偿**:会话顶栏的标题行在折叠时保持大约原来的水平位置(`pl-[142px]`),不会因为侧栏收窄而向左跳一截。
- 三项既有能力**保留未动**:拖拽调宽(240–400px)、视口窄于 980px 时自动折叠、折叠偏好持久化(沿用既有的 `webui:ui-state` 键,未新增键)。

### 导航项

侧栏导航是「新建会话 / 搜索 / 插件」三项。参照里另有「定时 / 网站 / 远程」三项,它们在参照中是不可点击的占位(inert);本仓库的既定决策是不渲染没有落点的占位控件,这三项**不出现**在界面上,属于产品拍板项(见 HANDOVER 融合清单)。当前所在界面对应的导航项有**激活态高亮**:树列正在显示「搜索」或「插件」表面时对应行高亮;没有会话选中(首页)且没有别的表面占屏时,「新建会话」高亮。

### 会话列表

- **选中态与悬停态可区分**:选中行用 `bg_interaction_tertiary_selected`,悬停用 `bg_interaction_tertiary_hover`——此前两处都写悬停令牌,「我在哪个会话」的信号是丢失的。如实说明边界:这两个令牌在**浅色主题下上游定义为同一个值**(都映射 `--opacity_black_1_4`,见 `tokens.css`),视觉区分只在**深色主题**成立(悬停 `opacity_white_0_4`、选中 `opacity_white_0_8`);浅色下的同色是上游设计令牌的现状,不是本工单引入的回归。
- 「项目」小节头是 28px 高的纯文本行(不可交互);此前它渲染了一个没有点击行为的按钮,读屏用户会聚焦到一个点不动的控件,已移除。
- 空态是带圆角与背景的卡片面;错误态带 `role="alert"` 并把失败原因(网络错误 / HTTP 状态等)直接透出,而不是只有一句通用文案。
- 项目/目录/子代理的展开收起有 180ms 高度 + 140ms 透明度过渡;系统开启「减弱动态效果」时过渡关闭,内容照常显示。
- 会话行与子代理行是 `?session=<id>` 的深链(`<a>`):普通左键行为不变;中键 / Ctrl+点击在新标签打开的 URL,冷加载恢复机制(「重开不乱」)本来就会认。
- **揭示机制保持每目录 6 条**(「更多 (N)」逐目录展开),没有引入参照的全局 Load more——每目录揭示更省界面空间且已上线。

### 参照有、本轮未实现(如实说明)

参照实现的右键上下文菜单(置顶 / 归档 / 复制为新会话 / 删除)、置顶与归档的本地 overlay 排序、Agent Team 徽标与工作区目录副行、「最近任务」小节——**本轮均未实现**。前两项依赖服务端契约(置顶、归档、复制均无端点),需要与后续工单统一排期;后两项在参照自己的壳里也没有渲染点。后续 agent 不应把它们误判为「已实现但坏了」。

## 上下文窗口(模型选择器里能调什么、切了之后发生什么)

模型选择器面板底部有一个详情区:当前模型声明了至少两个上下文窗口档位时,显示一排「上下文窗口」单选按钮;没声明或只有一个档位的模型不渲染任何控件,也不留空白。当前只有 `MiniMax-M3` 和 `MiniMax-M3.1-Flash-Preview` 有档位(512K / 1M),其余模型一概没有。档位标签是紧凑的 token 数(512K、1M);引擎标注为「更高用量」的档位(1M)会带「用量较高」提示,如实告知成本差异。
Expand All @@ -253,7 +281,10 @@ exec 回合的代价——以下都是当前真实存在的行为,选择权限
| 表面 | 组件 | 锚点 `data-testid` |
| --- | --- | --- |
| 侧栏(rail) | `components/shell.tsx` | `sidebar-scroll-viewport` |
| 侧栏折叠开关(侧栏外浮层,工单 47) | `components/shell.tsx` | `sidebar-collapse-toggle` |
| 侧栏会话树 | `components/session-tree.tsx` | `sidebar-session-row` |
| 会话树项目小节头(纯文本,工单 47) | `components/session-tree.tsx#SectionHeader` | `sidebar-section-header` |
| 会话树错误态(`role="alert"`,工单 47) | `components/session-tree.tsx` | `sidebar-tree-error` |
| 侧栏用户菜单(设置 / 每日签到 / 用量 / 退出登录) | `components/shell.tsx#SidebarFooter` | `sidebar-user-menu` |
| 侧栏 inbox(告警浮层) | `components/inbox.tsx` | `inbox-flyout` |
| 顶栏(带模型选择器) | `components/toolbar.tsx` | `toolbar-session-status` |
Expand Down
46 changes: 45 additions & 1 deletion packages/webui/webapp/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,42 @@
animation: webui-settings-content-in 180ms ease both;
}

/* Sidebar list surfaces (webui-parity 47).
*
* Both rules copy the reference shell's own classes (`shell.css`
* `.webui-empty-state` / `.webui-expandable-motion`) so a future
* side-by-side comparison asserts against the same names:
*
* - `.webui-empty-state` gives the empty list a card face (radius, grouped
* background, padding) instead of bare floating text.
* - `.webui-expandable-motion` animates a disclosure with
* `grid-template-rows 0fr -> 1fr`, which interpolates without measuring
* row heights, plus a 140ms opacity crossfade. The `is-open` class is the
* only switch; children must keep `min-height: 0; overflow: hidden` or
* the 0fr track cannot collapse them. */
.webui-empty-state {
border-radius: var(--radius_8);
background-color: var(--bg_grouped_secondary);
padding: var(--spacing_8) var(--spacing_12);
line-height: var(--line_height_20);
}
.webui-expandable-motion {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition:
grid-template-rows 180ms cubic-bezier(0.2, 0.7, 0.2, 1),
opacity 140ms ease-out;
}
.webui-expandable-motion > * {
min-height: 0;
overflow: hidden;
}
.webui-expandable-motion.is-open {
grid-template-rows: 1fr;
opacity: 1;
}

/* Respect prefers-reduced-motion.
*
* Every transition / animation added by ticket 07 (the model-selector
Expand All @@ -200,7 +236,12 @@
* catch-all's duration hack is ever retuned. The dots' rule carries
* !important because official-utilities.css (imported after this file
* in app/layout.tsx) re-declares the dots' animation-name at equal
* specificity — see the inline note below. */
* specificity — see the inline note below.
*
* webui-parity 47: the sidebar disclosure gets `transition: none` rather
* than relying on the 0.001ms catch-all — the content must still show
* (the 0fr/1fr track and opacity are state, not motion), it just must
* not slide. */
@media (prefers-reduced-motion: reduce) {
.mavis-skeleton-bar {
animation: none;
Expand All @@ -219,6 +260,9 @@
.thinking-live-dot {
animation: none;
}
.webui-expandable-motion {
transition: none;
}
*,
*::before,
*::after {
Expand Down
14 changes: 14 additions & 0 deletions packages/webui/webapp/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,19 @@ function App() {
return null;
}, [tabState.previewActiveId, tabState.treeActiveId, tabState.tabs]);

// webui-parity 47 (N2) — which sidebar nav row reads as active. The tab
// strip is owned by this page, so the shell cannot derive the signal
// itself: the tree column's active tab being search / plugins lights the
// matching sidebar row, and `panel === "plugins"` keeps the legacy mirror
// (set by openPanel) in agreement with openSurfaceTab.
const activeNavSurface: "search" | "plugins" | null = useMemo(() => {
const treeTab = tabState.tabs.find((tab) => tab.id === tabState.treeActiveId);
if (treeTab && (treeTab.kind === "search" || treeTab.kind === "plugins")) {
return treeTab.kind;
}
return panel === "plugins" ? "plugins" : null;
}, [tabState.tabs, tabState.treeActiveId, panel]);

// ============================================================
// Reveal-in-tree handler. Defined BEFORE the early return for
// `!state` because hooks must be called in the same order on
Expand Down Expand Up @@ -671,6 +684,7 @@ function App() {
panel={null}
onOpenPanel={openPanel}
onOpenSurfaceTab={openSurfaceTab}
activeNavSurface={activeNavSurface}
onOpenSettings={openSettings}
onOpenUsage={openUsage}
alertCount={alertCount}
Expand Down
Loading
Loading