From 13b761af361f0deffadba177b65a4e3deb06acfe Mon Sep 17 00:00:00 2001 From: Jaxton07 Date: Mon, 21 Sep 2026 11:35:52 +0800 Subject: [PATCH] =?UTF-8?q?perf(ui):=20=E4=BC=98=E5=8C=96=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E4=BE=A7=E6=A0=8F=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将右侧变更栏改为固定宽度的 transform 浮层,避免长会话在开合期间逐帧重排聊天内容,并补充键盘可访问性与排障文档。 --- docs/INDEX.md | 4 +- docs/PITFALLS.md | 9 +++ packages/desktop/src/renderer/src/App.tsx | 2 +- .../src/components/diff/DiffSidebar.tsx | 30 +++------- .../components/diff/useDiffSidebarOverlay.ts | 42 ------------- .../src/renderer/src/styles/globals.css | 59 +++++++------------ 6 files changed, 41 insertions(+), 105 deletions(-) delete mode 100644 packages/desktop/src/renderer/src/components/diff/useDiffSidebarOverlay.ts diff --git a/docs/INDEX.md b/docs/INDEX.md index ea6ef02a..3d555bc2 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -199,7 +199,7 @@ src/ | 目录 | 内容 | |---|---| | `chat/` | **MessageList**(底部跟随 + 脱离回底;行模型 `useMemo`,轮末行定位规则在 shared chat-rows;**挂载窗口**见 `mount-window.ts`)/ **SelectionToolbar**(对话区选中文字浮出菜单:添加到对话/新会话继续)/ **MessageItem**(纯分发壳)+ `message-actions.tsx`(复制/Fork/撤回按钮)/ **UserMessage** / **SystemMessage**(compaction 分割线 + mutex 通知)/ **AssistantMessage** / **Markdown**(markstream-react 流式丝滑渲染,fade 关闭避免合成层闪烁;代码块只读编辑器关闭自动 decoration,样式覆写在 globals.css `.markdown-body`,见 PITFALLS)+ **MermaidBlock**(接管 markstream 的 mermaid code_block:loose 模式 + 悬浮工具栏 + 渲染失败报错卡;见 PITFALLS)/ **ToolCallCard** / **SubagentRunCard**(独立行,有 sessionFile 可点开子会话)/ **TodoPanel**(呼吸灯 + 展开 morph 同一容器)/ **MetaGroup**(memo 折叠组 + 圆点行/分类统计行)+ `use-sweep-highlight`(统一扫光)+ `use-shown-working`(working→worked 滞后缓冲)/ **PreviewTicker** + `activity-ticker`(工作中预览行调度)/ **StreamingMarquee**(溢出 tail-follow,位移用 shared marquee-motion)/ **ImagePreview**(全屏多图,portal 到 body)/ **ErrorNote**(统一报错卡)+ **RetryNote**(自动重试瞬态行)/ **CenterOrb** + `center-orb-draw`(中央状态动画,绘制闭式函数)/ **TurnDiffChip**(轮末计时行 + 文件变更 chip;计时器运行中 1s 心跳跳动/定格;文件行右键 + hover「⋯」→ 路径菜单 `ui/FilePathMenu`)/ `meta-summary-label`(i18n 胶水) | -| `diff/` | **useDiffSidebarOverlay**(宽度账本纯计算 + rAF 合帧 resize 监听:可用宽不够就转浮层)/ **DiffSidebar**(右侧变更侧栏:按轮分组 unified diff + 内嵌 BranchRow git 分支行;开关在 SessionTabBar;`cwd` 下传给卡片做相对路径基准)+ DiffFileCard(卡片头右键 + hover「⋯」→ 路径菜单) | +| `diff/` | **DiffSidebar**(右侧变更浮层抽屉:固定宽度 + transform 合成动画,不参与聊天列布局;按轮分组 unified diff + 内嵌 BranchRow git 分支行;开关在 SessionTabBar;`cwd` 下传给卡片做相对路径基准)+ DiffFileCard(卡片头右键 + hover「⋯」→ 路径菜单) | | `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` 纯函数) | @@ -232,7 +232,7 @@ src/ | 顶栏 tab / 拖拽排序 / 置顶 | `session/SessionTabBar.tsx`(dnd-kit,DragOverlay ghost 拾起时实测宽度沿用原胶囊 + 轴锁定 + drag-region 退出)+ `stores/sessions.ts` `selectBarSessions`(**顶栏严格 = 置顶表**:会话被置顶就算 tab 没开也在顶栏,点击自动开;新会话不进顶栏)+ `stores/ui-preferences.ts` `reorderPinned`(拖拽改 `pinnedSessions` 顺序) | | 左侧栏(分组 / 开合 / 置顶 / 搜索 / 移除项目 / 会话右键菜单) | 组件 `components/sidebar/*`(容器 → `Sidebar`;纯派生 → `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/shoot-sidebar.mjs` | | 顶栏内容 / 显隐 | 顶栏**常驻**(窗口拖动、左栏开合、变更侧栏入口都在上面,v9 起不再整条隐藏)→ `App.tsx` 无条件渲染 `SessionTabBar`;设置项「顶栏显示会话」(`settings/AppearancePanel.tsx` → `barSessionsVisible`)只控制**要不要出置顶会话胶囊**:关掉后顶栏不出胶囊、也不出空态提示,会话全在左栏(v9 已删旧版的三处兜底:左栏头 44px 拖拽带 + 「+」、聊天列顶 12px 隐形拖拽带、左栏底部「本轮改动」入口) | -| 右栏(push ↔ 窄窗浮层) | `components/diff/DiffSidebar.tsx` + `useDiffSidebarOverlay.ts`(宽度账本:可用宽 < 380 + min(420, 38vw) → 转浮层)+ `globals.css` 的 `.diff-sidebar.is-overlay` 段;开关在顶栏(顶栏关闭时在左栏底部) | +| 右侧变更浮层 | `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 成功才移除) | | 扩展交互(issue #45) | 契约 `shared/src/extension-dialog.ts` → `backend/src/session/extension-dialog-host.ts`(队列+裁决)→ `main/ipc/` 转发 → `stores/transcript.ts` pendingDialogs → `session/InteractionDock.tsx`;notify→`stores/toasts.ts` pushExtension;setEditorText→草稿+预填提示(Composer) | | 会话权限模式(默认/完全访问) | 后端:`pi-backend.ts`(`permissionModes` map 内存态 + `get/setSessionPermissionMode`)+ `permissions/extension.ts`(fullAccess 审计分支)+ `permissions/audit.ts`;IPC:`PermissionGetMode/SetMode`;前端:`composer/PermissionPicker.tsx`(chip;新会话页读写 `newSessionDraft.permissionMode`)+ `stores/sessions.ts`(真实会话走 `permissionModes` map,promotion 成功后把 draft 档位应用到新 id)+ `use-composer-send.ts`。**模式不写进 permissions.json、不被新会话继承(spec D1);D7:按会话持久化到 ui-state.json 的 sessionPermissionModes(只存非 default,切回默认即删键),打开会话时 await 恢复——重启/内存回收后重开同一会话保住原档位,新会话与 fork 仍从 default 起步** | diff --git a/docs/PITFALLS.md b/docs/PITFALLS.md index 076c010a..dba4138a 100644 --- a/docs/PITFALLS.md +++ b/docs/PITFALLS.md @@ -30,6 +30,7 @@ | onDragStart 里拿不到拖拽尺寸(`active.rect.current.initial` 恒 null) | 四 · dnd-kit rect ref 填充晚于 onDragStart | | 报错文案悬在空态页不消失、切新会话还在 | 四 · store 级 error 字段永不清理(已修:改 toast + 乐观回滚) | | 切到长会话卡顿约 1 秒、消息多的会话越久越卡 | 四 · 长会话切会话卡顿(挂载窗口 + ToolCallCard 布局抖动)(2026-09-12 修复) | +| 有内容的会话开右侧变更栏掉帧,新会话却丝滑 | 四 · 右侧栏 width push 动画导致聊天区逐帧重排(2026-09-21) | | 已完成会话上滚滚不动、要大力滚,贴底还吸附(0.5.8 线上 bug) | 四 · 长会话切会话卡顿 → 三次修复(markstream content-visibility 600px 估值占位)(2026-09-16 修复) | | 长会话里上滚,位置被反复重置/拽回底部(0.5.7 线上 bug) | 四 · 长会话切会话卡顿 → 二次修复(markstream 占位条缩水 + 手写滚动补偿)(2026-09-13 修复) | | 改了 `src/main/` 但 app 行为没变(dev 不重建主进程) | 三 · electron-vite dev 主进程 watcher 不可依赖(2026-09-17) | @@ -427,6 +428,14 @@ pi SDK 必须声明进 `packages/desktop/package.json` dependencies(electron-b 挂载窗口的两个已知面:① **切走再切回窗口复位回尾部 40 行**(每次切换成本恒定,不会越用越慢);② 窗口只增不减 ⇒ 滚过的行一直挂着,DOM 上限 = 该会话全量(换来的好处是不做反向回收、无滚动跳变)。③ 页内查找不受影响:Electron 默认菜单没有 Find、代码也没调 `findInPage`,Cmd+F 本来就没有;将来若做消息搜索,应查 transcript store 而非 DOM,与挂载窗口无关。 +### 右侧栏 width push 动画导致聊天区逐帧重排(2026-09-21) + +**症状**:新会话页打开/收起变更栏很顺,有消息内容的会话明显掉帧;内容越长越明显。 + +**根因**:右栏原来在同一 flex 行里做 `width: 0 → min(420px, 38vw)` 的 420ms push 过渡。每个动画帧都会改变聊天列宽度,迫使 Markdown 重新换行和整列布局;`MessageList` 的 ResizeObserver 又会在跟随底部时逐帧调用 `scrollTo`。实测一次开栏约 52 次布局、42–50 次贴底滚动;隐藏 diff 内容改善很小,强制脱离布局后 LayoutDuration 从约 37–43ms 降到 3–5ms,说明瓶颈不在 diff 行本身。 + +**修复**:右栏统一为绝对定位浮层抽屉,宽度固定,进退只动画 `transform/opacity`;聊天列完全不改宽。点击聊天区不自动关闭,保留顶栏开关、面板关闭按钮和 Esc,便于边看消息边核对变更。不要用 `max-width`、grid 列宽或另一种尺寸属性代替 `width`——它们仍然逐帧触发布局;要丝滑必须让运动留在合成层。 + ### macOS 关窗 = 隐藏窗口:三个反直觉点(2026-09-17,issue #55) 行为:macOS 下点红点/⌘W → `preventDefault() + win.hide()`(app 继续跑,Dock 点回原窗口原状态);⌘Q / 菜单退出仍要真退。 diff --git a/packages/desktop/src/renderer/src/App.tsx b/packages/desktop/src/renderer/src/App.tsx index ca2756fd..4f3c2886 100644 --- a/packages/desktop/src/renderer/src/App.tsx +++ b/packages/desktop/src/renderer/src/App.tsx @@ -95,7 +95,7 @@ export default function App() {
- {/* 中间列保底 380 = 宽度账本(画板 C),右栏挤不下时已由 DiffSidebar 自己转浮层 */} + {/* 右侧 DiffSidebar 是独立浮层,不参与中间聊天列宽度与布局动画。 */}
{showEmpty ? : } diff --git a/packages/desktop/src/renderer/src/components/diff/DiffSidebar.tsx b/packages/desktop/src/renderer/src/components/diff/DiffSidebar.tsx index f3bb51e4..9975a271 100644 --- a/packages/desktop/src/renderer/src/components/diff/DiffSidebar.tsx +++ b/packages/desktop/src/renderer/src/components/diff/DiffSidebar.tsx @@ -7,7 +7,6 @@ import { useTranscriptStore } from "../../stores/transcript"; import { useUiStore } from "../../stores/ui"; import { CloseIcon } from "../icons"; import { DiffFileCard } from "./DiffFileCard"; -import { useDiffSidebarOverlay } from "./useDiffSidebarOverlay"; /** 空 turn 列表稳定引用(selector/useMemo 缺省,禁内联新数组) */ const EMPTY_TURNS: TurnChanges[] = []; @@ -90,8 +89,8 @@ function BranchRow() { } /** - * 右侧 diff 栏(宽度 0 ↔ 420px 过渡,聊天列自然压缩)。 - * 窄窗(可用宽放不下「380 保底 + 右栏」)自动转浮层叠在聊天列上,点外/Esc 收起(见 useDiffSidebarOverlay)。 + * 右侧 diff 浮层抽屉:固定宽度、transform 进退,不参与聊天列布局。 + * 关闭只走顶栏按钮 / 面板关闭按钮 / Esc;点击聊天区不收起,便于对照消息与变更。 * 按轮次倒序分组(最近一轮在上);分段开关「全部 / 最近一轮」; * chip 跳转 = stores/ui.ts 的 diffFocus → 这里直接操作 DOM(开卡片 + 滚动定位 + 闪烁)。 * 开关状态内存态不持久化(重启统一关闭,用户已定)。 @@ -120,26 +119,15 @@ export function DiffSidebar() { const totalRemoved = visibleTurns.reduce((s, tc) => s + tc.totalRemoved, 0); const bodyRef = useRef(null); - const overlay = useDiffSidebarOverlay(); - const asideRef = useRef(null); - - // 浮层模式的「点外收起」+ Esc:push 模式不接(右栏是常驻分栏,点聊天区不该关它) + // 浮层保持常驻,点击聊天区不收起;Esc 是关闭按钮之外的键盘出口。 useEffect(() => { - if (!overlay || !open) return; - const onPointerDown = (e: PointerEvent) => { - if (e.target instanceof Node && asideRef.current?.contains(e.target)) return; - setOpen(false); - }; + if (!open) return; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; - window.addEventListener("pointerdown", onPointerDown); window.addEventListener("keydown", onKeyDown); - return () => { - window.removeEventListener("pointerdown", onPointerDown); - window.removeEventListener("keydown", onKeyDown); - }; - }, [overlay, open, setOpen]); + return () => window.removeEventListener("keydown", onKeyDown); + }, [open, setOpen]); // chip → 侧栏跳转:目标不在当前分段时先切「全部」(effect 随 visibleTurns 变化重跑),再定位 useEffect(() => { @@ -168,11 +156,7 @@ export function DiffSidebar() { }, [diffFocus, visibleTurns, clearDiffFocus]); return ( -