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
4 changes: 2 additions & 2 deletions docs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ src/
| `composer/` | **Composer**(装配层 ~330 行,键盘事件分发)+ 三 hook:`use-composer-send`(ensureSession 懒创建/followUp 排队/停止先 clearQueue/发送失败草稿回填)、`use-slash-menu`(命令拉取+胶囊回填+导航)、`use-at-completion`(@ token 探测/续钻/胶囊弹回)+ 展示件 QueueBar/ImageTray/AttachmentChip/**QuoteChip**(选中引用胶囊)/SendErrorBar + **ModelPicker** / **ThinkingPicker**(按模型 thinkingLevels 过滤+clamp)/ **PermissionPicker**(会话权限模式 chip:默认/完全访问)/ **SlashMenu** / **AtMenu** / **ContextRing** + 纯函数 `slash-filter`/`at-files`/`send-error`/`quote`/`model-filter`(各有测试) |
| `session/` | **SessionTabBar** + SessionTab/TabPill(**v8:顶栏胶囊 = 置顶会话(不看 tab 开没开),严格只含置顶**,顺序就是 `pinnedSessions`;未置顶会话只在左栏;空态一行 12px `ink-faint` 提示;状态收拢到头像图标;dnd-kit 拖拽排序(改的也是 `pinnedSessions`),DragOverlay ghost + 轴锁定;拖拽期间退出 drag-region;**最左 = 左栏开合按钮 `PanelLeftIcon`**;胶囊右键 → `ui/ContextMenu`「重命名 / 置顶」;叉叉 = 取消置顶 + 从顶栏清除)/ **RenamePopover**(重命名浮层:锚在胶囊/指针锚点,160ms 进场 / 120ms 退场 + Enter/点外提交/Esc 取消)/ **SessionAvatar** + `session-status`(头像底色语义 + 状态/标题纯逻辑,顶栏与左栏共用;`SessionAvatar` 已无引用,留作备用)/ `session-menu`(菜单可用性与动作:`sidebarMenuKind` 定形态(普通 / 无菜单)、只读会话不给菜单、置顶只动 `pinnedSessions`、重命名落盘(**同步 tabs 与历史列表两份拷贝**)+失败 toast、复制诊断、删除会话;顶栏胶囊与左栏会话行共用)/ **ProjectBranchPicker**(从 `projects/` 移入的项目/分支选择器 chip;**只在无 active(新会话页)显示**,真实会话的项目创建时已绑定不可改)/ **DockSlot**(底部交换槽仲裁:权限 > 扩展对话框 > Composer)/ **ApprovalDock**(权限审批:async respond 成功才移除面板,失败保留重试)/ **InteractionDock**(扩展对话框四卡 select/input/editor/confirm:键盘 ↑↓/Enter/数字/Esc + 倒计时显示,裁决在 backend)/ **TrustDialog**(项目信任两选项)/ **UpdateButton**(顶栏更新按钮) |
| `settings/` | **SettingsDialog**(PANELS 注册表;分类 = 静态 + 插件 settings.panel 贡献动态拼接)/ **GeneralPanel**(语言/上下文管理二态/channel-watch 开关)/ **AppearancePanel**(顶部 Tab 分栏:「基础」=主题三段/背景图/**显示顶栏开关**/中央动画,「UI 插件」= 原独立分类并入的 UiPluginsSection:总开关/插件卡/槽位指派,设计稿 .local/design/ux/appearance-ui-plugins)/ **SkillsPanel** / **McpPanel**(SDK 0.84 无 MCP,占位)/ **ExtensionsPanel** + `extensions/`(目录浏览/安装/卸载,subagent 包安装两段式确认)/ **UiPluginsSection**(挂在 AppearancePanel 的 UI 插件 Tab 下) / **LanObserverPanel** / **AboutPanel** / `providers/`(ProvidersPanel + ProviderRow 操作全图标化 / LoginDialog(交互登录对话框:OAuth + api_key 交互流)/ CustomProviderForm + ModelRowsEditor(逐模型行编辑器)/ BuiltinProviderEditForm(内置端点覆写)/ SubagentPanel(子代理执行器开关 + 逐代理模型/思考深度偏好)+ `model-rows` 纯函数) |
| `sidebar/` | **Sidebar**(常驻左栏容器:取 store 数据(历史 + 内存会话 + `activeCwd = store.cwd`)→ 调 `lib/sidebar-groups` 的 **`deriveSidebarNavigation`** 一条龙派生(含只读子会话过滤与项目表同源)→ 分发 props,240↔0 push 过渡 + 收起时 `inert`;主体是唯一外层滚动容器,带 `data-sidebar-scroll-root` 供 CDP 验收定位)/ **SidebarHeader**(滚动区外常驻的新会话入口 + 搜索框;新会话复用全局 draft)/ **SidebarGroup**(日常与各项目共用的分组骨架:分组行 + 可折叠的 `SidebarSessionList` + 空态,并把项目 cwd 接到定向 draft 入口;**标题行渲染在列表容器之外**,所以标题上的滚轮归外层导航)/ **SidebarSessionList**(会话列表单一实现:`SIDEBAR_SESSION_LIST_MAX_ROWS=8` × `SIDEBAR_SESSION_ROW_HEIGHT=31px` = 248px 上限 + `overflow-y-auto/overflow-x-hidden` + 隐藏滚动条;**仅在实际溢出(> 8 行)时挂 `overscroll-y-contain`** —— 给不溢出的滚动容器挂 contain 会吞掉 wheel、外层不滚,故 contain 与 `data-scrollable` 共用同一个布尔判据;**边缘淡出**:滚动位置写成 `data-fade-top/bottom`,`globals.css` 的 `.sidebar-session-list` 按这两个属性给 `mask-image` 渐变(alpha 遮罩、不引颜色,深浅主题共用;高度旋钮 = 变量 `--session-list-fade`),不溢出的列表两面都干净;淡出属性由 effect 独占、**不写进 JSX**(否则 React 重渲染会把滚动中的值冲回初值);纯 props 驱动,不读 store/不排序)/ **ProjectRow**(图标表达展开态:项目 = folder ↔ folder-open、日常 = 恒咖啡;行末 hover 出 Codex 式 «⋯» + 新会话按钮(日常仅新会话),菜单锚在**指针点**,另含置顶标记、移除项目二次确认)/ **SessionRow**(无头像文字 + 行尾状态点 + 右键菜单锚点;行上带 `data-session-id` / `data-session-active` 只读属性供 CDP 验收定位与判定,不影响视觉)/ **SessionMenu**(会话行右键三层状态机:菜单 → 改名气泡 → 删除确认;只读子会话不弹菜单)/ **ProjectSection**(「项目」小标:**v7 起固定标题不可折叠** + 常显「+」)/ **SidebarFooter**(设置;顶栏关闭时补「本轮改动」)/ **ProjectMenu**(项目 «⋯» 菜单项 builder)/ `useExpandedGroups`(展开态读写:起点由 `expandedGroupsTouched` 决定;写入时 store 同一次 set + 单个补丁置 touched) |
| `sidebar/` | **Sidebar**(常驻左栏容器:取 store 数据(历史 + 内存会话 + `activeCwd = store.cwd`)→ 调 `lib/sidebar-groups` 的 **`deriveSidebarNavigation`** 一条龙派生(含只读子会话过滤与项目表同源)→ 分发 props,240↔0 push 过渡 + 收起时 `inert`;主体是唯一外层滚动容器,带 `data-sidebar-scroll-root` 供 CDP 验收定位)/ **SidebarHeader**(滚动区外常驻的新会话入口 + 搜索框;新会话复用全局 draft)/ **SidebarGroup**(日常与各项目共用的分组骨架:分组行 + 可折叠的 `SidebarSessionList` + 空态,并把项目 cwd 接到定向 draft 入口;展开内容常驻 DOM,通过 `sidebar-group-content` 的 grid `0fr↔1fr` + opacity 做 180ms 开合动画,折叠态 `inert`/`aria-hidden`,动画结束后重置内层 scrollTop;**标题行渲染在列表容器之外**,所以标题上的滚轮归外层导航)/ **SidebarSessionList**(会话列表单一实现:`SIDEBAR_SESSION_LIST_MAX_ROWS=8` × `SIDEBAR_SESSION_ROW_HEIGHT=31px` = 248px 上限 + `overflow-y-auto/overflow-x-hidden` + 隐藏滚动条;**仅在实际溢出(> 8 行)时挂 `overscroll-y-contain`** —— 给不溢出的滚动容器挂 contain 会吞掉 wheel、外层不滚,故 contain 与 `data-scrollable` 共用同一个布尔判据;**边缘淡出**:滚动位置写成 `data-fade-top/bottom`,`globals.css` 的 `.sidebar-session-list` 按这两个属性给 `mask-image` 渐变(alpha 遮罩、不引颜色,深浅主题共用;高度旋钮 = 变量 `--session-list-fade`),不溢出的列表两面都干净;淡出属性由 effect 独占、**不写进 JSX**(否则 React 重渲染会把滚动中的值冲回初值);纯 props 驱动,不读 store/不排序)/ **ProjectRow**(图标表达展开态:项目 = folder ↔ folder-open、日常 = 恒咖啡;行末 hover 出 Codex 式 «⋯» + 新会话按钮(日常仅新会话),菜单锚在**指针点**,另含置顶标记、移除项目二次确认)/ **SessionRow**(无头像文字 + 行尾状态点 + 右键菜单锚点;行上带 `data-session-id` / `data-session-active` 只读属性供 CDP 验收定位与判定,不影响视觉)/ **SessionMenu**(会话行右键三层状态机:菜单 → 改名气泡 → 删除确认;只读子会话不弹菜单)/ **ProjectSection**(「项目」小标:**v7 起固定标题不可折叠** + 常显「+」)/ **SidebarFooter**(设置;顶栏关闭时补「本轮改动」)/ **ProjectMenu**(项目 «⋯» 菜单项 builder)/ `useExpandedGroups`(展开态读写:起点由 `expandedGroupsTouched` 决定;写入时 store 同一次 set + 单个补丁置 touched) |
| `ui/` | Button(ghost/primary × sm/md × danger)/ Dropdown / Switch(统一受控开关,支持 indeterminate)/ **Tooltip**(自定义悬浮提示,新增提示一律用它不用原生 title;右缘元素传 `align="end"` 防幻影横向滚动条)/ **ContextMenu**(通用右键菜单:portal 到 body + `place-menu` 纯函数定位,点外/Esc/选中/滚动关闭;有边框卡片外观(`rounded-xl border border-border bg-surface shadow-pop`),项高 30px、支持 `danger` 红项与 `separatorBefore` 分隔线,不再有 veil 变体;会话胶囊 / 文件行 / 左栏会话行共用,文件行三项菜单见 **FilePathMenu**)/ `place-menu`(`placeMenu` 定位 + `anchorOfElement` 锚点转换,纯函数有测试;**左栏项目 «⋯» 与右键菜单一律传指针锚点** `{ left: clientX, top: clientY, width: 0, height: 0 }` —— 传元素矩形会把高度多算一遍,菜单掉到行下方)/ **ConfirmDialog**(破坏性操作二次确认:极淡蒙层 `bg-ink/6` + 392px 细边框卡片 + Portal 到 body 且 `z-[60]` 压过右键菜单;点卡片外/Esc/✕ = 取消,文案与按钮由调用方给;`autoFocusConfirm` 把默认焦点给确认按钮(不传则是键盘 Enter 不确认);非破坏性场景别默默加上)/ **QuitConfirmDialog**(Windows 退出前确认,文案走 i18n `quitConfirm.*`,并回 `app:quitDialogShown` 上屏回执) |
| `icons/` | 内联 SVG 集中管理;`icons.test.ts` 校验所有 path `d` 语法(防残缺数据被 Chromium 丢弃) |

Expand Down Expand Up @@ -234,7 +234,7 @@ src/
| 上下文用量圆环 | `composer/ContextRing.tsx` + `hooks/use-context-usage.ts`(事件驱动刷新,与插件 host API 共用)→ IPC getContextUsage → SDK `session.getContextUsage()`(percent null 或无消息不渲染;<60% 灰 / 60-85% 琥珀 / >85% 红) |
| 每轮计时行 + 文件变更 chip + diff 侧栏 | 计时 shared `transcript/turn-timings.ts`(deriveTurnTimings;分量 = reducer 盖戳的 `UIToolCall.endedAt` + `runEndedAt` 定格 + 历史回放透传 toolResult timestamp);变更 shared `transcript/turn-files.ts`(deriveTurnChanges)+ `chat-rows.ts`(行定位:每轮必有计时行,lan-web 不传 timings 保持旧行为);渲染 `chat/TurnDiffChip.tsx`(timer 恒在首位)+ `diff/DiffSidebar.tsx`(含 BranchRow);开关 = SessionTabBar 的 DiffIcon 按钮 + `stores/ui.ts` diffSidebarOpen |
| 顶栏 tab / 拖拽排序 / 置顶 | `session/SessionTabBar.tsx`(dnd-kit,DragOverlay ghost 拾起时实测宽度沿用原胶囊 + 轴锁定 + drag-region 退出)+ `stores/sessions.ts` `selectBarSessions`(**顶栏严格 = 置顶表**:会话被置顶就算 tab 没开也在顶栏,点击自动开;新会话不进顶栏)+ `stores/ui-preferences.ts` `reorderPinned`(拖拽改 `pinnedSessions` 顺序) |
| 左侧栏(分组 / 开合 / 置顶 / 搜索 / 移除项目 / 会话右键菜单 / 组内限高滚动) | 组件 `components/sidebar/*`(容器 → `Sidebar`;分组会话列表 → **`SidebarSessionList`**:8 行 × 31px = 248px 上限 + 原生嵌套滚动,`overscroll-y-contain` 只在溢出时挂,规则见 `docs/PITFALLS.md`「contain 吞 wheel」;纯派生 → `lib/sidebar-groups.ts`,有单测)+ 持久化字段 `barSessionsVisible` / `sidebarCollapsed` / `expandedGroups` + `expandedGroupsTouched` / `pinnedProjects`(`ui-preferences.ts` → main `ui-state.ts` normalize + 旧文件缺字段时的 touched 迁移)+ 宽度过渡与状态点呼吸 `styles/globals.css` 的 `.sidebar*` 段;置顶会话的行首图钉在 `sidebar/SessionRow.tsx`(行首 16×16 图标槽,标题 x 恒 38);验收/截图 `scripts/check-sidebar-group-scroll.mjs`(真实 wheel 滚动归属)、`scripts/shoot-sidebar.mjs`(逐帧) |
| 左侧栏(分组 / 开合 / 置顶 / 搜索 / 移除项目 / 会话右键菜单 / 组内限高滚动) | 组件 `components/sidebar/*`(容器 → `Sidebar`;分组会话列表 → **`SidebarSessionList`**:8 行 × 31px = 248px 上限 + 原生嵌套滚动,`overscroll-y-contain` 只在溢出时挂,规则见 `docs/PITFALLS.md`「contain 吞 wheel」;纯派生 → `lib/sidebar-groups.ts`,有单测)+ 持久化字段 `barSessionsVisible` / `sidebarCollapsed` / `expandedGroups` + `expandedGroupsTouched` / `pinnedProjects`(`ui-preferences.ts` → main `ui-state.ts` normalize + 旧文件缺字段时的 touched 迁移)+ 宽度过渡、分组开合动画与状态点呼吸在 `styles/globals.css` 的 `.sidebar*` 段(均覆盖 reduced-motion);置顶会话的行首图钉在 `sidebar/SessionRow.tsx`(行首 16×16 图标槽,标题 x 恒 38);验收/截图 `scripts/check-sidebar-group-scroll.mjs`(真实 wheel 滚动归属)、`scripts/shoot-sidebar.mjs`(逐帧) |
| 顶栏内容 / 显隐 | 顶栏**常驻**(窗口拖动、左栏开合、变更侧栏入口都在上面,v9 起不再整条隐藏)→ `App.tsx` 无条件渲染 `SessionTabBar`;设置项「顶栏显示会话」(`settings/AppearancePanel.tsx` → `barSessionsVisible`)只控制**要不要出置顶会话胶囊**:关掉后顶栏不出胶囊、也不出空态提示,会话全在左栏(v9 已删旧版的三处兜底:左栏头 44px 拖拽带 + 「+」、聊天列顶 12px 隐形拖拽带、左栏底部「本轮改动」入口) |
| 右侧变更浮层 | `components/diff/DiffSidebar.tsx` + `globals.css` 的 `.diff-sidebar`(固定 `min(420px, 38vw)`,transform 进退,不压缩聊天列);开关在顶栏(顶栏关闭时在左栏底部),关闭入口 = 开关 / 面板关闭按钮 / Esc |
| 权限审批面板 | `backend/src/permissions/gate.ts`(队列)+ `session/ApprovalDock.tsx`(快捷键 Enter/A/D/Esc;await 成功才移除) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export function SessionRow({
// 可测试性只读属性(spec §6:CDP 按 sessionId 定位行,而不是靠标题文本匹配;纯属性,不影响视觉/交互)
data-session-id={session.sessionId}
data-session-active={active ? "true" : "false"}
title={title}
className={`flex h-[31px] w-full items-center gap-2 rounded-[7px] px-1.5 text-left text-[13.5px] ${
active ? "bg-bubble font-medium text-ink" : "text-ink-2 hover:bg-hover hover:text-ink"
}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,29 @@ export function SidebarGroup({
onTogglePin={onTogglePin ? () => onTogglePin(group.cwd) : undefined}
onRemove={onRemove}
/>
{group.expanded && (
<SidebarSessionList
sessions={group.sessions}
activeSessionId={activeSessionId}
emptyLabel={t("sidebar.noSessions")}
onSelect={(session) => void openSession(session)}
onContextMenu={(session, anchor) => sessionMenu.open(session, anchor)}
/>
)}
<div
data-sidebar-group-content={group.expanded ? "expanded" : "collapsed"}
className={`sidebar-group-content ${group.expanded ? "is-expanded" : ""}`}
aria-hidden={!group.expanded}
inert={!group.expanded}
onTransitionEnd={(event) => {
if (event.target !== event.currentTarget || event.propertyName !== "grid-template-rows") return;
if (!group.expanded) {
const list = event.currentTarget.querySelector<HTMLElement>("[data-sidebar-session-list]");
if (list) list.scrollTop = 0;
}
}}
>
<div className="sidebar-group-content-inner">
<SidebarSessionList
sessions={group.sessions}
activeSessionId={activeSessionId}
emptyLabel={t("sidebar.noSessions")}
onSelect={(session) => void openSession(session)}
onContextMenu={(session, anchor) => sessionMenu.open(session, anchor)}
/>
</div>
</div>
{sessionMenu.element}
</div>
);
Expand Down
21 changes: 20 additions & 1 deletion packages/desktop/src/renderer/src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,24 @@ code {
opacity: 0;
transform: translateX(-28px);
}
/* 分组展开/收起:grid 0fr→1fr 让未知高度(空态/1–8 行/248px 限高列表)共用一套动画,
内层 overflow:hidden 只负责裁切;实际列表仍保留自己的纵向滚动。 */
.sidebar-group-content {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition:
grid-template-rows 180ms cubic-bezier(0.32, 0.72, 0, 1),
opacity 120ms ease-out;
}
.sidebar-group-content.is-expanded {
grid-template-rows: 1fr;
opacity: 1;
}
.sidebar-group-content-inner {
min-height: 0;
overflow: hidden;
}
/* 会话行状态点:工作中 = 墨色呼吸(与顶栏胶囊头像同一套节拍与时长) */
.sidebar-dot-working {
animation: avatar-breathe 3.2s ease-in-out infinite;
Expand Down Expand Up @@ -987,7 +1005,8 @@ code {
}
@media (prefers-reduced-motion: reduce) {
.sidebar,
.sidebar-inner {
.sidebar-inner,
.sidebar-group-content {
transition: none;
}
.sidebar-dot-working {
Expand Down
Loading
Loading