Skip to content

feat(side-pane): 右侧侧栏工作台(侧边聊天 / 浏览器 / 终端 / 调用轨迹 / V工程计划与 Git / 会话状态面板) - #231

Draft
RoxyAsahi wants to merge 424 commits into
lioensky:mainfrom
RoxyAsahi:pr/side-pane-workbench
Draft

RoxyAsahi wants to merge 424 commits into
lioensky:mainfrom
RoxyAsahi:pr/side-pane-workbench

Conversation

@RoxyAsahi

@RoxyAsahi RoxyAsahi commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Warning

还没做完,大概还要几天。 先开成 draft 大概看下整体方向,功能和样式还在调。很多联动都还没做完也还没测。。。后续也建议先开个独立分支合并。。。

概述

这个 PR 给 VCPChat 的聊天窗口加了一套右侧侧栏工作台。原来要切窗口、开新页面才能做的事情,现在都放进聊天旁边的标签页里:侧边聊天、浏览器、终端、模型调用轨迹、V工程计划与 Git 改动、通知与审批。侧栏跟着当前会话/话题走,打开的标签、宽度和选中状态都会持久化。

同时,聊天区加了一块浮动的会话状态面板(Git 改动 / 计划进度),回复下面多了文件改动摘要,聊天头部可以换成胶囊样式,还有 messenger 气泡模式。

这是 draft,范围比较大(177 个提交,主要集中在 modules/ui-system/side-pane/、styles/ui-system/、modules/renderer/),想先拿到整体方向上的反馈,再决定要不要拆成几个 PR。

总览

截图里的聊天内容做了模糊处理,本机路径换成了 C:\workspace\…。


功能一览

1. 侧栏外壳与标签条

  • 打开/关闭:点聊天头部的展开按钮或胶囊、按快捷键都可以。侧栏以覆盖层的方式滑入,主聊天区不会被挤压重排。宽度可以拖拽调整,默认占比 0.45,按宿主宽度计算并记住。
  • 标签条:
    • 标签宽度统一,过多时收缩成只显示图标。
    • 当前标签是一张凸起的卡片,颜色跟随主题;其余标签之间有分隔线。
    • 悬停显示完整标题,图标在左、关闭按钮在右端。
  • 拖拽排序:用指针事件实现的横向排序,不依赖额外库。
  • 标签总览:弹出层里能搜索、用键盘选择并跳转到任一标签。
  • 右键菜单:关闭、关闭其他、关闭全部、重新打开最近关闭的标签。菜单挂在 body 下,不会被毛玻璃侧栏裁切;窗口失焦时自动关闭。
  • 快捷键:切换侧栏、循环切换标签等。网页标签获得焦点时也能用。
  • 毛玻璃背景:侧栏透出壁纸。工具条和标签用接近白色的胶囊,深浅色主题都有适配。
  • 动效:覆盖层的进入/退出、启动页分段切换的滑动、弹出层的进场。全部遵守 prefers-reduced-motion。

标签总览

2. 新标签页 / 启动页

  • 入口:点 "+" 或主页按钮,打开一个类似浏览器新标签页的启动页。
  • 三个分段:
    • 工具:侧边聊天、浏览器、终端、调用轨迹、计划详情。
    • 应用:推荐区固定显示四个常用应用,下面是全部应用。
    • 通知:待审批的工具调用和通知中心。
  • 当前助手:启动页上会显示当前助手的头像和名字;切到「通知」分段时,头像和名字会向上收起,把空间让给通知列表。
  • 记住分段:在「通知」分段时切到别的标签,再点主页按钮会回到「通知」;「+」仍然打开工具页。
  • VCPLog 状态卡:显示日志服务的连接状态。截图里因为本地服务没启动,显示为“未连接”。
工具 应用
启动页-工具 启动页-应用

3. 通知与一键审批

通知中心放进了启动页的「通知」分段,待审批的工具调用和普通通知在同一个列表里分组显示(待审批 / 通知 / 已处理)。

  • 待审批横幅:列表顶部显示「N 项待审批」,点击可以在待审批卡片之间跳转。
  • 一键审批:横幅上有「全部允许 / 全部拒绝」。
    • 为了防止误点,第一次点击只会变成「确认允许 N 项」,3 秒内再点一次才真正发出。
    • 会删除代码、或者带内容改动需要审计的请求不参与批量允许,必须逐条看过再处理;按钮上会提示跳过了几项。截图里 8 项待审批,批量只会允许其中 6 项。
  • 筛选:全部 / 待审批 / 通知 / 错误 / 已处理,chip 上带数量;「通知」只看普通通知,不含审批卡。
  • 删除提示:会删除代码的请求在卡片上标出「将删除这段代码」,并显示被删内容。
  • 颜色:横幅和卡片用同一块底色,只有图标带警告色;「全部允许」跟随主题强调色。
待审批列表 第一次点「全部允许」 删除代码需要逐条审批 只看通知
通知 确认允许 删除 通知筛选

截图里的审批请求是为了演示塞进去的样例,没有真的发出审批。

4. 侧边聊天(per-topic side chat)

  • 定位:围绕当前话题开的临时会话,不进入主聊天历史。会话只存在于磁盘上的隐藏子会话里,按序号命名。
  • 外观:
    • 输入框和主聊天的 composer 外观一致,使用 messenger 气泡;用户气泡是毛玻璃。
    • 输入框上方有渐隐过渡。
  • 操作:
    • 支持停止生成。
    • 右键菜单里可以复制、编辑消息、重新生成回答。
  • 恢复:恢复会话时,会保留用户之前所在的标签。
  • 草稿:没发出去的输入、引用和选中的模型按会话存在浏览器本地存储里,打字时不再每次写文件。写入失败会在底部提示并可以重试;浏览器存储满了时,输入继续留在会话文件里,换模型、刷新快照照常保存。

侧边聊天

5. 内置浏览器

本质是想联动后端的,还没弄。。

  • 基于 webview:工具条由圆角胶囊组成(后退/前进/刷新/地址),标签名取页面标题。
  • 地址恢复:手动输入的地址会保存下来,标签恢复时重新打开。
  • 安全与清理:
    • 弹出窗口做了加固。
    • 窗口重开时会清理子进程。
    • 焦点统一交给控制器管理。

浏览器

6. 终端与命令输出

  • 终端:基于 xterm,在主进程里配有原生 helper。
    • 终端跟随深浅色主题,启动握手过程不显示。
    • 连接状态以一个圆点的形式显示在工作区胶囊里。
  • 后台命令输出标签:默认跟到最新输出,可以从回复里直接打开。

![终端]
我知道原来前端里已经有这个了。。
(https://raw.githubusercontent.com/RoxyAsahi/VCPChat/659c53f3eea11b6a46c16a0a7267ecde1b8df4d9/terminal.png)

7. 模型调用轨迹(调用轨迹)

  • 记录:主进程记录器把每次模型请求/响应写成按话题划分的 JSONL。
    • 覆盖主聊天、群聊和标题生成三类请求。
    • 存的是“上下文增量”而不是整份上下文,并记录真实的 token 用量,包括缓存命中。
  • 展示:
    • 侧栏标签提供时间线、搜索、全部展开/收起。
    • 调用卡片按需懒构建,窄侧栏下指标会自动换行。
  • 定位:在回复的消息菜单里可以直接跳到产生这条回复的那次调用。
  • 跟随与报错:
    • 切换会话时标签自动跟随。
    • 服务端把错误当作正常流返回时,会显示为错误,而不是“正常结束”。
    • 主进程没有注册处理器时,显示错误提示,不会留下空白。

调用轨迹

8. V工程计划详情 + Git

  • 范围:计划标签跟随当前话题,Git 跟随该话题所在的工作区。每个话题一批施工,每个项目一条规则。
  • 计划页:
    • 待办列表。
    • 话题范围内的时间线:可以看每个节点的 diff,可以回退,支持筛选,能生成报告。
    • 头部是一行胶囊:状态、动作,以及能切换到任意项目的面包屑。
    • 窄侧栏下,页签和统计行都保持单行。
  • Git 页:
    • 改动文件卡片,带增删统计和逐行 diff。
    • 右键菜单。
    • 主进程补了分支、提交图和改动摘要的 API。
  • 联动:可以从计划页跳到项目页,打开的就是侧栏当前显示的那个项目。
计划 施工线 Git 改动 展开 diff
计划 施工线 Git diff

9. 会话状态面板

  • 形态:浮在会话上方的面板,可以收起成胶囊;展开时盖在消息上方,不会挤压聊天布局。
  • 跟随:面板跟着当前会话走,而不是全局只有一块。工作区按话题归属解析。
  • 两个分区,名字和侧栏标签一致:
    • Git 变更:增删统计、切换分支、提交或推送。
    • 计划:完成进度;点击后在侧栏打开对应的待办或时间线。
  • 窄屏:聊天区不足 1280px 时保留入口,不会整块隐藏。

状态面板

10. 聊天区相关改动

  • 回复下的文件改动摘要:识别 ProjectForge 的编辑,读取真实的执行状态,路径按工作区相对路径显示。
  • 聊天头部:
    • 可以在外观设置里选择经典样式或悬浮胶囊(头像 + 名称)。
    • 点胶囊或胶囊上的箭头可以展开/收起侧栏。
  • Messenger 展示模式:聊天气泡。
  • 系统消息:合并成一行提示;一次失败的请求只保留一条消息。
  • 工具审批:
    • 说明每条 ProjectForge 命令会做什么,并拦下无法执行的调用。
    • 群聊里等待审批期间保持回复存活,并返回完整的工具结果。
  • 未读徽标:在导航和目录刷新之间按顺序发布。
  • 长话题:批量记忆消息高度,加载时不再为每条消息强制布局一次。
  • 图标:剩余的 Material 图标和手绘图标统一换成 Lucide;麦克风图标保留原来的细线版本。

11. 主聊天UI尝试

试了试比较激进的UI改动,取消了标题行,只保留胶囊显示当前助手,兼打开关闭侧栏功能。抄grok bot的,让界面更简洁。
image
image
反正外观控制台里提供个可选项。。默认关闭的。。标题行上别的小按钮还没全部考虑好放在哪里。

架构

完整说明见 docs/SIDE_PANE_ARCHITECTURE.md。

modules/ui-system/side-pane/
├── side-pane-controller.js      # 对外入口:open/close/activate/reorder/closeOthers/reopenClosed …
├── side-pane-state.js           # 纯状态:按会话 owner 分组的标签、激活项、最近关闭
├── side-pane-persistence.js     # 布局/标签持久化(有上限)
├── side-pane-visibility.js      # 覆盖层显隐、宽度比例、拖拽调宽
├── side-pane-tab-strip.js       # 标签条渲染、tooltip、面板切换
├── side-pane-tab-dnd.js         # 指针事件拖拽排序
├── side-pane-tab-overview.js    # 标签总览弹出层
├── side-pane-shortcuts.js       # 快捷键(含 webview 焦点下)
├── tab definitions + providers  # 每种标签类型一个 provider:mount / dispose / 焦点 / 持久化钩子
│   ├── sideChat / browser / terminal / toolOutput
│   ├── modelTrajectory(Model|SideProvider)
│   └── planDetailSideProvider + git/(cards、diff-model、context-menu、git-view)
modules/ui-system/conversation-status-panel/   # 状态面板 owner 与 helpers
modules/ui-system/message-file-changes.js      # 回复下的文件改动摘要
modules/modelTrajectory.js + modules/ipc/…     # 主进程轨迹记录器与 IPC
  • 标签类型集中定义:每种标签有自己的 provider,控制器只负责生命周期。
  • 生命周期:异步读取、挂载和关闭都有独立的 lifetime。批量关闭会绑定到捕获时的 lifetime,避免过时的回调写回已经切走的会话。
  • 会话归属:标签按会话归属;某个 owner 没有标签时,侧栏自动收起。
  • 样式:styles/ui-system/side-pane-*.css 按区域拆分并有固定顺序,只用主题 token,不写硬编码颜色和 !important,由 scripts/check-ui-system.mjs 守卫。
  • 契约:preload 和 IPC 通道都登记在 chat contracts 里,事件图已重新生成。全部 92 处动态事件调用点都已登记,并按文件和代码逐处对上,登记过时检查会报错。

资源生命周期与数据一致性

根据 review 意见补了一轮,主要是两件事:侧栏的资源不再只有「全开 / 全关」两档,而是按需逐级启停;同一份数据只有一个来源。都建在上游已有的 LifecycleScope、VCPStateChannels、VCPContributions 和 VCPLifecycleInspector 之上,没有另起一套。

逐级启停

  • 按需加载:侧栏标签类型启动时只登记元数据,provider 第一次打开才 import()。终端、调用轨迹等主进程 IPC 领域也是先登记通道,第一次调用才 require 并初始化。
  • 作用域:每个标签有一个 occurrence 作用域(跟标签同生死),每次挂载再有一个 view 作用域。监听、Observer、定时器、IPC 订阅都挂在作用域上,关闭时统一释放,不再靠各 provider 手写清理。挂载途中被关闭、挂载抛错的情况也会回收干净。
  • 可见性由容器下发:控制器告诉每个视图它是否可见,provider 不再自己探测 DOM。看不见的视图暂停轮询,重新可见时立即补一次。
  • 休眠:隐藏太久、所属对话已经切走,或同时挂着的视图超过上限(按最近使用淘汰)时,只释放视图,标签、导航和滚动位置保留,再显示时重新挂载。终端只把画面节点移到一边,PTY 不动;有未发送输入的侧边聊天不休眠;正在忙的视图稍后再检查。
  • 窗口离开即清理:主进程按窗口登记的订阅和推送,在页面关闭、崩溃或完成导航时一起清掉。只开始导航还不算,所以被取消的导航不会误清。

一个数据一个来源

  • 共享数据源:createSharedSource 按持有者计数,第一个持有者出现时启动,最后一个离开后延迟一会儿再停。同一个窗口里的多个界面共用一份。命令运行记录、Git 变化、ProjectForge 变化、选中的 Git 工作区都改成了这种方式。
  • 主进程订阅表:窗口用 state:subscribe / state:unsubscribe 声明自己要什么,主进程只推给订阅了的窗口,不再按页面 URL 猜谁需要。第一个窗口订阅时才启动对应资源(例如对这个仓库的文件监听),最后一个离开时停掉。
  • Git 不再轮询:以前状态面板每 15 秒、侧栏 Git 每 8 秒、V工程窗口各自定时读。现在主进程只在有人订阅时监听仓库,变化时推一次,任何窗口里做的提交、暂存、切分支也会通知所有窗口。仓库元数据变化 300ms 内报,连续改文件时最多 20 秒报一次。应用自己的操作只通知一次,不会被文件监听再报一遍。监听不完整时(Linux 只能看 .git、监听挂不上或中途出错)会告诉订阅方,退回到「窗口获得焦点时补读」。
  • 跨窗口一致:窗口之间不再靠只在本窗口生效的 CustomEvent 互相通知。选中的 Git 工作区在主窗口和 V工程窗口之间同步。
  • 不再跨模块碰 DOM:通知数量、连接状态走状态通道;打开 V工程、往输入框插入文本、打开调用轨迹走命令,不再读别的模块的 DOM 文本、模拟点击或依赖全局变量。

看得见

  • 生命周期面板能看到每个侧栏视图的状态(活着 / 休眠及原因 / 隐藏了多久)、每个共享数据源的持有者,以及主进程的订阅表和 Git 监听状态。
  • 泄漏测试会把每种标签开关一遍,确认监听、订阅、定时器都回到基线。

顺带修掉的问题

  • 侧栏提交检查暂存路径时关掉重命名检测,跨工作区边界的重命名不会再有一半漏过检查。推送只把当前分支推到界面显示的那个上游,不受 push.default 等配置影响。
  • 计划详情强制回退时会核对预检时的文件 hash,确认框打开期间文件被外部改过就不覆盖。
  • 侧边聊天中止被上游拒绝时,主进程那条 HTTP 读取也会关掉;在首个响应头到达前停止,按取消收尾,不再显示服务错误。
  • 侧边聊天删除消息时先确认历史写入成功再移除;写入失败就保留消息并提示重试。
  • 删除当前助手时会通知跟随它的界面;调用轨迹标签不再轮询,只在推送不可用时才退回轮询。

这一轮的打磨

  • 代码查看器:
    • 文件被删或读不了时显示读取失败,不再显示一片空白;空文件照常显示。
    • 再次打开同一个文件会重新读取,外部改过也能看到新内容;工具条上加了「重新读取文件」。从聊天里打开的代码片段和差异仍然是打开时的快照。
    • 工作区里的文件会说明是二进制还是过大。大文件只渲染开头一段(约 256K 字符),并提示总行数和显示了多少行;复制和插入仍用完整内容。
  • 隐藏的标签不再跟着刷新:计划、调用轨迹、命令输出在藏起来时收到变化只记一笔,重新显示时读一次;藏起来之前已经排上的重读也推迟到显示时。期间没有变化就不读。
  • 侧边聊天的模型列表:缓存为空时等真正的刷新结果,不再固定等 1.5 秒;刷新失败退回缓存,最多等 10 秒,连续打开菜单共用同一次刷新。

测试与验证

  • npm run verify:development 的测试和检查全部通过:297 个测试文件、1802 个测试,0 失败、0 跳过、0 超时;之后的 UI 系统、chat 证据、设置归属、发布面、Chat Kernel 使用方等检查也全部通过。其中重新构建 Rust 数据服务的冒烟检查这一轮没跑(本机正在运行的应用占用了那个二进制),这一轮也没有改 Rust 代码。
  • node scripts/check-ui-system.mjs 通过。
  • chat contract 检查通过:0 处未登记的动态调用点。
  • 已合并最新的 main(build(projectforge): 补齐依赖,消除编译warning #232、fix(next-ui): 修复内嵌子应用分页切回时卡死在加载遮罩及黑屏问题 #233 合入之后),无冲突。合并时处理了 4 个文件:main.js(两边的窗口配置都保留)、main.html(铃铛按钮直接放进它最终的位置,侧栏按钮保留在它前面)、PowerShellExecutor.js(保留单独包装脚本,并加上关闭分页器的设置)、composerModelSelect.js(采用模型名完整显示)。single-chat-request-orchestrator 测试在 main 上因为新加的相对路径 import 无法从 data: URL 解析而失败,这里顺手修了。
  • 在 Electron 实例里手动走查了每一种标签,以及状态面板、启动页、通知与一键审批、标签总览、拖拽、快捷键、深浅色主题和减少动效。截图用的是「赤与白昼」主题。

还没做完 / 已知问题

这是 draft,下面这些我知道还没做完,合并前会逐项处理或者单独说明。

要在合并前处理

  • 拆分 PR。范围太大,打算拆成这几块分别提:外壳+启动页、侧边聊天、浏览器/终端、调用轨迹、计划/Git+状态面板、聊天区改动。拆之前先用这个 draft 收集整体方向的意见。
  • 全量测试全绿。以前遗留的失败已逐个修掉,npm run verify:development 一条命令跑完全部测试和检查。
  • 动态事件全部登记。之前挂着的 86 处已逐处审查并登记到 chat contracts。
  • 架构文档有过时的段落。docs/SIDE_PANE_ARCHITECTURE.md 里 Git 一节还在讲独立的 gitSideProvider.js 标签;实际上 Git 已经变成计划标签里的一页,入口是 git/git-view.js。
  • 新加的请求字段需要确认。调用轨迹会给流式请求加上 stream_options.include_usage,用来拿真实的 token 用量(调用方已经指定时不会覆盖)。个别不常见的上游可能不接受这个字段,需要确认能否兼容,或者做成开关。
  • 还没在真实窗口里完整走一遍的交互:
    • 侧边聊天右键菜单里的「编辑消息 / 重新回复」:目前只有测试覆盖。
    • 启动页分段之间的滑动动效。
    • 深色主题下的启动页工具卡片。

已知问题(会继续改)

  • 侧边聊天的历史写回。流结束时会把整段历史写回磁盘,可能覆盖生成过程中做的编辑或删除。主聊天、侧边聊天、多个窗口同时写同一个话题的情况都需要再查一遍。
  • 状态胶囊会盖住内容。收起后的胶囊浮在右上角,滚动时会压住第一条可见消息右上角的一小块。
  • 调用轨迹的覆盖范围:
    • 只记录主聊天、群聊和标题生成这三类请求。
    • 记的是客户端发出和收到的内容,VCPToolBox 在服务端额外加入或改写的部分看不到。
    • 只读取文件最后 32MB;上下文开头落在范围之外时,卡片上会说明有多少条没能还原。
  • 布局存档会越积越多。已经删除的话题对应的标签记录还留在存档里,不影响使用(会被跳过),但还没有自动清理。
  • 快捷键不能自定义,而且只有切换侧栏和切换标签两组。
  • 类型约束只有 JSDoc(side-pane-types.js),没有 TypeScript 检查。
  • 图标细节:通知区里有一个图标不是 Lucide,还有一处线宽是 1.7,和其余的 2 不一致。
  • 终端打包:终端依赖主进程的原生 helper 和 vendor/xterm,打包流程需要一起带上,还没在打包产物里验证过。
  • 署名头:少数移植自 Apache-2.0 项目的文件保留了原有的署名头。

🤖 Generated with Claude Code

@RoxyAsahi
RoxyAsahi force-pushed the pr/side-pane-workbench branch from 7b75a5a to b662f58 Compare October 6, 2026 00:00
@lioensky

lioensky commented Oct 6, 2026

Copy link
Copy Markdown
Owner

感觉这么做有很多问题
1.dom安全和回收极难管理。
2.初始化流程默认要全量加载v工程的所有handler包括终端的ptyhandle。
3.很多流程耦合太深了。

@lioensky

lioensky commented Oct 6, 2026

Copy link
Copy Markdown
Owner

核心在于,怎么渐进化的声明周期和回收周期,以及收缩将来v工程界面和侧栏界面的广播一致性。

RoxyAsahi and others added 19 commits October 6, 2026 21:55
…ssion info

Saving the session info wrote the browser draft first and gave up when
that write threw, so a model switch or a parent snapshot refresh was
lost whenever browser storage was full. The session info is now saved
regardless. While the draft is not in browser storage, the input stays
in the file instead of being stripped as already migrated, and the
"draft not saved" retry stays visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ails or hangs

Waiting on the model refresh meant a refresh error failed the whole list,
and a server that never answers left the picker loading indefinitely
because the models request has no timeout. A failed refresh now falls
back to the cache, the wait is capped at 10 seconds, and opening the
picker again while a refresh is running reuses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The note derived "KB" from the character count, which understates the
size of non-ASCII text (three bytes per CJK character). It now gives the
file's line count and how many lines are shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs and subscriptions in the view scope

Releasing the view scope alone now tears down everything the two tabs
registered, so a tab the controller drops without calling dispose leaves
nothing behind. Closing the trajectory tab while the push registration is
still pending no longer leaves a watch counted in the main process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button sits right of the create-topic button and only appears while the
panel has something to show for the current conversation. Hiding is remembered
across restarts; data refreshes never reopen a panel the user closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bar was a fixed 64px stub beside "N/M 已完成", which read as decoration.
The count line now carries the percentage on the right, and a 4px bar below
it fills the full width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The side pane diagnostics now list, per mounted tab, how many listeners,
timers, observers and subscriptions its view scope and child scopes
hold, and the shared source diagnostics name the holders that keep a
source alive. Both report counts and labels only, never payloads.

A listener registered through a scope no longer runs once that scope has
started releasing: release is asynchronous, so events that arrived while
it was still in progress used to reach handlers of a view already being
torn down. Timers behaved this way already.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n the view scope

Each of these tabs now hangs its listeners, timers, observers and source
subscriptions on a scope under the view scope the controller passes, so
releasing the view (a tab put to sleep, a mount cancelled midway) tears
all of it down even when the provider's dispose is never called. Their
own bookkeeping (cleanup arrays, disposed flags, generation counters)
goes away; whether a view is still current is read from its scope.

A code viewer released while it is still loading stops instead of
mounting into a view that is gone. The two listener adapters of the code
viewer no longer exist, so their entries leave the contract registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…passes

The chat tab type now forwards the mount context, and the side chat
surface creates its scope as a child of the view scope instead of the
pane-wide one. Releasing the view alone therefore tears the surface down
(stream listeners, composer, sync subscriptions), while dispose still
runs the same teardown once and only then releases the scope, so the
draft the controller captures on close is unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The command run records move out of PowerShellExecutor.js into
commandRunStore.js, which has no load side effects: no Electron, no IPC
registration, no config reads, no watchers. Output sanitising moves with
it into terminalOutputSanitizer.js. The executor requires both and still
exports the same functions, so existing callers keep working, and since
Node caches the module both sides share one set of records.

The terminal IPC handlers read and watch runs through the store, so
opening the command output tab or the status panel no longer loads the
executor (and with it node-pty) just to list runs.

The executor's sibling modules are now listed in the packaged files;
command-output-parser.js, which the executor already required, was
missing there. The packaging test derives the list from the executor's
local requires so a new sibling cannot be left out again.

This changes the plugin's file layout, so it is offered as a proposal
for the plugin maintainer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two more choices for the tool presentation setting, next to the original
cards, compact rows and grouped folds:

- 单行合并 (inline): one borderless row per call. A result that directly
  follows its request with the same tool name is shown on the request
  row; failures show as a problem label with the first error line on
  hover. Consecutive commands fold into a terminal group, consecutive
  reads and searches into a lookup group.
- 整轮折叠 (process): the same borderless rows with an icon that turns
  into a chevron on hover and the first error line in red. After the
  reply finishes, everything from the first to the last tool block,
  including the text between them, folds into one "已处理" row; the
  final answer stays outside.

Pairing is visual only: both blocks keep their own keys, states,
disclosure memory and delete buttons. The turn fold is not marked as a
protocol block, so read-aloud and context extraction keep its text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an embedded app tab (V工程) covers the whole chat, the Git page has no
layout, so pushes only mark it stale. Switching back fired none of the events
that clear the flag (pointer enter, window focus, plan page switch), so a
commit made in the other tab stayed on screen until the mouse moved over the
list. A ResizeObserver on the page now re-reads once when it gets a size again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h the other domains

Git is registered eagerly: the module only wraps execFile, and the status
panel reads the workspace list on its first frame, so deferring the require
saves nothing. Repository watching still waits for the first window to
subscribe. Quit now releases Git through disposeAll like every other domain.

The handle wrapper keeps a separate ipcMain.handle(channel) branch so the
contract scan still attributes every git: channel to this module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The terminal ignored the view scope the controller passes, so the
lifecycle inspector showed a live terminal view holding nothing while it
had three listeners, a ResizeObserver and a debounce timer. They now go
through the view scope and are released with it when the view sleeps or
the tab closes; the shell session still follows the tab occurrence and
keeps running in the stash.

Regenerates the chat event graph for the current sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream moved the window pin button out of the injected script in
windowPinService into preloads/behaviors/pinButton.js; the Lucide pin icon
from this branch now lives there. The vcp-ui dynamic listener sites moved by
one line and the chat event graph is regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on moved

The listener helpers in messageRenderer and appearance-studio are unchanged;
only their line numbers shifted, which left the contract gate failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code viewer tab ids carry the file path. On Windows the backslashes are
read as CSS escapes, so the selector never matched and keyboard focus
fell to <body> after cycling to, closing, or leaving the menu of such a
tab; a path containing a quote made the selector throw and the mount fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
… literal colour

check:ui-system was red on the side pane branch. tool-presentation.css had
101 selectors outside .vcp-ui-scope, an !important on the reduced-motion
rule and two fixed 11px sizes; components.css (from upstream lioensky#234) fell back
to a hard-coded indigo because --vcp-ui-primary is not a token.

Every tool presentation selector now sits under .vcp-ui-scope (the chat
messages container already carries it, and the file is alone in its layer,
so the uniform specificity bump changes no cascade). The reduced-motion rule
reuses the shimmer selector instead of !important, the 11px sizes use
--vcp-ui-text-xs, and the pinned window button uses the accent token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
claude added 7 commits October 7, 2026 00:24
Before, a provider that threw during mount (a corrupt restored payload,
a failed lazy import, a bug) left its tab selected over a blank pane,
with the error only in the console. The view now says the tab could not
open, shows the error, and offers a retry; showing the tab again still
retries as before, and closing the tab removes the error view.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
…wn with it

initWorkspaceSidePane ran inside the main startup try block ahead of the
filter manager and setupEventListeners, so any exception while building
the side pane skipped the rest of startup and left the send button and
the other main window bindings dead. It now runs in its own try/catch
and reports the failure as a toast.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
A file:// page in the side browser runs with Electron's default file
privileges, so its script could fetch other local files (SSH keys,
VCPChat settings) and post them out. Like DeepSeek Harness
(browser-guests.ts onBeforeRequest) and ZCode (isAllowedBrowserUrl),
the side browser now only opens http, https and about pages, and its
session cancels file: and other non-web subresource requests.

Any page could also start downloads on its own, and each one opened the
system browser. A download now reaches the system browser only right
after a real input on that page, one per input, the same rule popups use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
…sible focus ring

- Arrow keys re-rendered the strip and then focused the removed button,
  while activateTab moved focus into the panel, so each press threw a
  keyboard user out of the tablist. Keyboard switches now activate
  without moving focus and refocus the new button by id, as Radix Tabs
  (used by ZCode) does.
- While the new-tab page or hidden notifications was active, no tab had
  tabindex=0, so the strip had no Tab stop. The first tab now gets it.
- Delete closes the focused closable tab.
- Tabs and header buttons removed the outline with no replacement; they
  now get the same focus ring as the tab menu items.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
render() rebuilds the whole page, and it only put focus back on filter
inputs and page tabs. Toggling a section or a batch, opening filters,
picking a contributor, or any background refresh dropped focus to
<body>. Interactive buttons now carry a data-focus-key that render()
records and restores, the imperative equivalent of React keeping keyed
nodes in ZCode and DSH. Clearing the filters lands on the search box.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
Shift+F10 on a change row opened the menu at (0,0) with focus left on
the row, no arrow navigation, and Escape dropped focus. Like the Radix
ContextMenu in ZCode's GitPaneChangeCard, the menu now opens beside the
row when there is no pointer, takes focus, moves with the arrow keys,
Home and End, and Escape, Tab or choosing an item returns focus to the
row. Icon ligature text is hidden from screen readers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
The button called openPythonAttachmentInTextEditor, which only accepts
.py file URLs on Windows, so it failed silently for every file. Sending
the path to open-external-link instead would hand model-supplied paths
(.bat, .lnk) to shell.openExternal. The button now reveals workspace
files through the workspace-checked git:reveal-path and shows the path
for anything else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
claude added 30 commits October 7, 2026 07:00
If the reply finished on its own while a slow interrupt request was
pending, the failed interrupt deleted it as if it were an empty
placeholder (side chat and main chat). Now only a still-streaming
placeholder is removed. If upstream accepts the interrupt but never
sends a terminal, the side chat stops locally after 3s and saves the
partial as cancelled instead of staying busy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
21a0a48 moved restart onto the app confirm dialog, which is async and
focuses its own button, but the click handler still called term.focus()
right after restart(). Focus went back to the xterm behind the dialog,
so typing reached the shared shell while the dialog was open. The
handler now leaves focus alone when a confirm will be shown; the dialog
returns focus to the restart button when it closes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnTGLLVUgUjQG3f4cWBHJw
…to claude/project-thread-1zv9nc

# Conflicts:
#	modules/ui-system/side-pane/git/git-view.js
#	modules/ui-system/side-pane/modelTrajectorySideProvider.js
After #14 side chats use dormancy 'keep', so the type note that said
unsent input should use isBusy() instead of keep, and the isBusy examples
that named a sending side chat, no longer matched the code. The side chat
handle's setVisible, removed as dead code in 21a0a48, came back with
#14's revert and is removed again. The architecture doc's terminal
example used a deps.notify that does not exist, and the side chat owner
is about 770 lines, not 560.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnTGLLVUgUjQG3f4cWBHJw
States now go error, then loading, then empty (as ZCode GitPane does),
so a load in progress or a failed read is never shown as "nothing here":
- Git: no "还没有工作区 / 添加工作区" before the workspace list arrives
  or when it fails; the error has a retry.
- Code viewer picker: a failed workspace read says so; the file list
  error has an in-place retry.
- Trajectory: no "请先选择智能体和话题" flash while subscribing.
- Tool output: a failed command list shows the error with a retry
  instead of "还没有命令记录"; a rejected output read lands on the error
  bar instead of hanging on "加载中…"; a failed reload during a running
  command keeps the output on screen.
- Side chat: the intro stays hidden while history loads; the history
  retry works from the keyboard.

Layout and feedback:
- Git diff: an open diff keeps the previous text while an edited file
  is refetched instead of collapsing to one line; failed diffs are not
  cached and have a retry.
- Plan: retry buttons switch to "正在重试…" at once; a background
  refresh with a filter swaps results in place; filter errors retry.
- Code viewer: reloading a shown file keeps it until the new text
  arrives; its loading icon actually spins.

Docs: dormancy notes follow #14 (side chats stay mounted).
Tests for the new cases use waitFor instead of fixed sleeps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
…t send owner tests in test:workbench

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
…s to keep

Mutation runs against #15's tests (5ff688f) found three regressions they
no longer catch:
- regenerate's removeMessageById(id, true): the mock stopped recording
  the save flag, so a renderer removal that saves again (and can write to
  the renderer's current topic) passed. Both delete and regenerate assert
  [id, false] again.
- terminal toolbar listeners bypassing the view scope: `listener > 0`
  goes back to the exact count of 3.
- the pane's active / collapsed classes after a reversed animation: they
  drive the layout (side-pane-shell.css) and the visibility check, so the
  four class asserts come back.

Nothing tested that side chats stay mounted (#14 dropped the sleep tests
only); setting the chat type back to 'none' passed every test. A
draft-restore case now hides a side chat past hiddenMs and checks the
same handle, draft and references are still there. Also drops the
duplicate busy mark the #15 merge left in side-pane-dormancy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnTGLLVUgUjQG3f4cWBHJw
The source editor rejected only a literal .git segment. On Windows the
8.3 short name GIT~1, trailing dots/spaces and the ::$INDEX_ALLOCATION
stream name reach the same directory (git's is_ntfs_dotgit), and on any
platform a workspace symlink pointing at .git let source:write-file
overwrite .git/config (core.fsmonitor, hooks path). Segments are now
checked with git's NTFS rules, and the real parent directory is checked
for .git too. The Git panel uses the same segment check, and the reveal
fallback compares paths with path.relative instead of a case-sensitive
prefix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hW799u5Ji5k9khDAwu688
…low it

Opening the side terminal tab resized the shared PTY to the narrow pane even
while the terminal window (and AI commands) were using it at full width, and a
side view without focus drew the PTY's output at its own width.

- terminal:create only presets the size when no PTY is running yet, and
  returns the PTY's real cols/rows.
- PowerShellExecutor tells mirror views when the PTY size changes; the main
  process forwards it as terminal:resized (after flushing buffered output).
- The side view claims the size only while it has focus (and only when it
  differs); without focus it draws at the PTY's size, as DSH's read-only views
  do, and fits to its own width again when the user clicks in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AP71nAxrCFEpH7st6asf9
…s it

Sending in the side chat disabled its textarea until the reply finished.
Focus fell to <body>, so anything typed during the reply was dropped,
the next message needed another click into the box, and Esc could not stop
the reply. Clicking the stop button also left focus on <body> because the
button hides itself.

The textarea now stays enabled the way the main chat's does; the existing
busy guard still keeps Enter from starting a second round, so the next
question can be written while waiting. Esc in the box stops the reply, like
ZCode's Esc-to-stop, and the stop button hands focus back to the box. If
a pending question is retracted while the user has written something new,
it is put back in front of that draft instead of being dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019L2bZomLn4NVoxS4Rv5Bkv
…sion guards

# Conflicts:
#	modules/ui-system/side-pane/side-pane-types.js
Move/up/cancel were only heard on the strip, so releasing outside it (or
losing pointer capture when the dragged node was replaced by a render) left
the drag session alive and every later press was ignored. They are now
heard on the document for the length of a press. Renders that arrive during
a drag (a browser tab changing its title or icon while loading) are held
until the drop, so the dragged node is never swapped out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AP71nAxrCFEpH7st6asf9
- Plan detail: a focus target from the status panel is applied once and
  then cleared from the saved tab, so restart or wake no longer jumps back.
- Side chat restore keeps the saved createdAt, so lists keep their order.
- A local-only side chat draft that cannot be read now warns the user
  instead of silently restoring blank.
- Width persistence logs a {success:false} settings save result.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
The restart button called session.restart() and then term.focus(). When
the shell is running, restart() first opens the confirm dialog, which
focuses its own button; the terminal then took focus straight back. Esc
still closed the dialog through its document listener, but the keystroke
also reached xterm and bash read it as a Meta prefix, so the next
character typed was eaten (typing "echo still" ran "cho still").

Focus the terminal first and then restart: the dialog takes focus from
the terminal and hands it back when it closes, and Enter on it confirms
without a stray newline in the shell.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019L2bZomLn4NVoxS4Rv5Bkv
…llapses it

At window widths of 960px or less the pane floats over the conversation and
covers the send button, and the only way out was the collapse button or
Ctrl+Alt+B. A click in the conversation now collapses it like a drawer,
unless that click itself opened or switched a side tab (code block "副屏",
ask in side chat).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AP71nAxrCFEpH7st6asf9
Opening the terminal tab and typing right away lost every key until the PTY
attached. Input typed during the connection is now held (up to 4K chars) and
sent in order once it connects; it is dropped if the connection fails, so a
command meant for a terminal that never started is not replayed later.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012AP71nAxrCFEpH7st6asf9
The trajectory tab sleeps after 5 minutes hidden. Waking it reset every
collapsed row and jumped the view to the bottom. It now captures its
expansion overrides, commands and scroll position, and restores them
when the remount still shows the same conversation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DFRovoZHMzeheXKkLNWKG
…orkbench

The .git alias and symlink regression test lives in source-service.test.js,
which no CI job ran.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
…inal restart confirm keeps focus

# Conflicts:
#	modules/ui-system/side-pane/terminalSideProvider.js
#	tests/side-pane-terminal-lifecycle.test.mjs
…row window overlay

# Conflicts:
#	package.json
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
… leak, performance and QA threads

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UCGGgmHsCToooXTgSXpmq9
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.

3 participants