给 opencode 桌面端(Electron)加三组官方没有的交互,脚本一次全部打上。
边想边展开 → 想完先摊着 → 整轮结束(最后是正文)才直接折,还在调工具就等滚出屏幕(下方内容不动)→ 点开还能看
首页每个会话行悬停时显示垃圾桶按钮 → 点击弹官方删除确认框 → 确认后删掉该会话
一次删很多:按住 Shift 点垃圾桶 = 跳过确认框直接删;或进多选删除模式(右下角悬浮条点「多选删除」,或会话右键菜单选「多选删除」),点行勾选(可「全选」),按「删除 N」→「确认删除 N」一次删完。
每条助手回复末尾补上完成时间(· 14:32)→ 首页每个会话行右侧显示最后活跃时间(今天 14:32,跨天 10/16)
| # | 行为 |
|---|---|
| 1 | 思考流式输出时展开,正文实时可见 |
| 2 | 思考结束后不立刻折叠:后面最后一块是正文(整轮结束)就直接收;还在调工具(工作中)或只有下一段思考时,才等这块整块滚出视口上方再收。收起时补偿滚动,下方内容原地不动 |
| 3 | 折起后点标题可重新展开 |
| 4 | 展开状态下,点底部 「收起 ▴」、双击空白处、或再点标题,都能收起 |
| 5 | 手动开合过的块不会被自动折叠,状态在滚动/重渲染后也不丢 |
| # | 行为 |
|---|---|
| 1 | 首页会话行悬停时右侧出现垃圾桶按钮(默认隐藏,鼠标移开消失) |
| 2 | 点击弹官方删除确认框,确认后会话被删除 |
| 3 | Shift + 点击垃圾桶 = 跳过确认框,直接删 |
| 4 | 右下角悬浮条「多选删除」(或会话右键菜单「多选删除」)进多选模式:点行勾选、可「全选」,「删除 N」按一次变「确认删除 N」,再按就批量删完 |
| 5 | 复用官方写死关闭的删除开关 + 官方 onDeleteSession,不自己实现删除逻辑;只影响首页会话行,会话页里的归档按钮不动 |
| # | 行为 |
|---|---|
| 1 | 每条助手回复末尾(官方「Build · 模型 · 耗时」那行)追加 · 14:32 完成时间 |
| 2 | 消息还在流式输出时不显示,回复完成才出现 |
| 3 | 首页每个会话行右侧显示最后活跃时间:今天 HH:MM、跨天 MM/DD、跨年 YYYY/MM/DD;悬停时排在项目名和垃圾桶之间,不重叠 |
| 4 | 时间全部从后端 /api/session/<sid>/message 的 time.completed 和 session.time.updated 取,不改数据结构、不额外存盘 |
官方 2.0.x 只有「默认展开」和「默认折叠」两种静态行为,没有上面第一套;首页也只有一个「归档」入口,没有删除按钮;消息末尾只有「模型 · 耗时」,没有绝对时间。
- Windows
- Node.js 18+
- opencode 桌面端(Electron 版,安装目录下有
resources\app.asar)
支持版本:2.0.32(2.0.29 的思考折叠锚点相同,但 2.0.29 里没有首页删除按钮的那段代码,会报找不到目标)。opencode 升级后 minified 代码会变,锚点失效时脚本会明确报错,需要按下面「升级后锚点找不到」更新。
三组补丁是同一次运行里全部打上的:任一目标的锚点对不上,脚本会报错并不写文件(不会出现只打一半的情况)。
git clone https://github.com/qq398768650/opencode-desktop-reasoning-patch.git
cd opencode-desktop-reasoning-patch
# 1. 先看会改什么(dry-run,不动文件、不动进程)
node apply-patch.mjs
# 2. 真正写入(自动:关桌面端 → 备份官方版 → 写补丁 → 再开回来)
node apply-patch.mjs --write
# 3. 验证:历史思考块默认折叠,点一下能开、再点能收
node verify.mjs
# 3b. 不想开调试口时,可以先做静态校验(直接查 asar 里三组补丁的 12 个标记)
node verify.mjs --static
# 3c. 只查删除/多选补丁有没有注入(要先切到首页;只查存在性,不会真删)
node verify.mjs --delete
# 3d. 只查时间补丁(首页看行时间、会话页看回复完成时间)
node verify.mjs --time
# 4. 想撤销补丁、回到官方原版(自动:关桌面端 → 盖回备份 → 再开回来)
node apply-patch.mjs --restore只想先把产物打出来看/手动部署(不碰安装目录、不关桌面端):
node apply-patch.mjs --out patched.asar--write 会自动完成关/开桌面端,不会断会话——agent 跑在后端进程 opencode-cli.exe serve --service 里,和桌面窗口是两个进程。
只想单独重启桌面端:node restart.mjs。
| 文件 | 作用 |
|---|---|
apply-patch.mjs |
主脚本:找安装目录 → 定位两个 renderer chunk → 打三组补丁 → 重建 asar(含 dry-run / --out / --write) |
restart.mjs |
关掉再带 --remote-debugging-port=<自动挑的端口> 重开桌面端 |
debug-port.mjs |
挑调试口:读 Windows 的 TCP 保留端口区间 + netstat 占用,从 9222 往后找第一个可用的(--port / OC_DEBUG_PORT 可覆盖) |
verify.mjs |
走 CDP 检查折叠行为(--delete 查删除/多选补丁,--time 查时间补丁,--static 只查 asar 标记),PASS 时退出码 0 |
SKILL.md |
给 opencode agent 当技能用的说明(含补丁细节、坑、锚点定位法) |
使用说明.md |
面向人的使用说明 |
桌面端界面打包在 <安装目录>\resources\app.asar,真正渲染界面的组件在 /out/renderer/assets/*.js 里。脚本自动定位两个目标 chunk 并改写:
| 标记 | 改什么 |
|---|---|
| P1 | i()(思考块是否展开):流式中恒 true;否则优先本地 store,其次全局 Map(跨重挂载记忆),都没有时回退到 !_later$()(最后一块是工具调用或下一段思考、且本块还没整块滚出视口上方 → 保持展开) |
| P4 | 摘掉 get open() 里 ?? (thinking.details === 'expanded' ? true : void 0) 的兜底——就是它让每个思考块默认恒展开 |
| P8 | onOpenChange 把用户的开合状态记进全局 Map,并写本地 store 做即时反馈 |
| P2 | 底部加「收起 ▴」按钮、根节点加双击空白收起;折叠时机用 _later$()(扫到最后一个 part:最后是正文块就直接收;最后是工具调用或下一段思考时,还要求本块整块滚出视口上方),工具调用行不算「出结果」;触发源两个:MutationObserver 盯 document.body(200ms 节流,收一次就 disconnect)+ 全局 scroll 捕获监听(capture:true,120ms 节流)扫描 globalThis.__ocFoldQ$;折叠时做滚动锚定(_anchor$):按折叠前后本块在视口里的位移补偿 scrollTop,块滚到视口上方时改用底边做锚点,避免翻历史被带偏;补偿在 queueMicrotask 里再跑一次(渲染前完成,不会闪一帧) |
关键点:思考结束时组件会卸载重挂载,所以「不主动收」是撑不住的——必须让新实例挂载时自己判断「我后面还有没有内容」,没有就保持展开。
官方代码里删除按钮其实已经写好,只是开关被写死成关闭。脚本做 8 处替换:
| 标记 | 改什么 |
|---|---|
| D1 | 把开关 ,bie=!1,xie=500; 改成 ,bie=!0,xie=500, + 内联 16×16 垃圾桶图标组件(ZZ_TRASH_ICON);多选删除 UI 的 IIFE(D_MULTI_JS)和时间显示的 IIFE(T_TIME_JS)接在声明结尾 ; 之后当独立语句(不能塞进那个逗号列表,见「已知坑」) |
| D2 | 按钮文案 common.archive → common.delete("归档"→"删除") |
| D3 | 按钮 data-action home-session-archive → home-session-delete |
| D4 | 图标 archive → 垃圾桶;顺手注册行→会话映射(__ocRegRow$)、把官方 onDeleteSession 存进 __ocDeleteApi$ |
| D5 | aria-label common.archive → common.delete |
| D6 | 点击动作 e.onArchiveSession(e.record.session) → 包一层 try/finally 设 __ocSkipConfirm = !!t.shiftKey 再调 e.onDeleteSession(...)(Shift 免确认) |
| D7 | showDelete 加旁路 globalThis.__ocSkipConfirm ? v(e,t) : r.show(确认框)(v 是官方内部删除函数,onDeleteSession 就是 showDelete) |
| D8 | 会话右键菜单「删除」项后追加「多选删除」项,onSelect 调 __ocMultiEnter$ 进多选模式 |
纯 DOM 注入,不改官方组件逻辑:
| 标记 | 做什么 |
|---|---|
| T1 回复完成时间 | 扫 [data-slot="text-part-meta"](官方「Build · 模型 · 耗时」行),从最近的 [data-timeline-part-id="msg_xxx:text:0"] 取消息 id,在 time.completed 存在时往同级追加 <span data-oc-done-time>· HH:MM</span> |
| T2 首页活跃时间 | 扫首页 [data-session-id] 行,从 __ocRowMap$ 取 session.time.updated,在 [data-slot="home-session-labels"] 末尾追加 <span data-oc-home-time>(flex:0 0 auto,右对齐) |
| T3 数据来源 | 后端 API base 从 performance.getEntriesByType('resource') 里最近的 http://127.0.0.1:<port>/api/ 取(端口每次重启都变,不能写死);会话 id 从 URL 和 [data-tab-key] 里收集,GET /api/session/<sid>/message 命中消息 id 后缓存进 __ocMsgTime$ |
| T4 重绘 | MutationObserver 盯 document.body(120ms 节流)+ 首页 15s 定时兜底(time.updated 变化不必然触发 DOM 变更) |
两个目标 chunk 必须串行重建:每替换一个文件都会平移它之后所有文件的 offset,第二个要在第一个的结果上继续。
- asar header 的 JSON 长度必须和原版一模一样(不足用空格补齐)。改短会让
dataStart前移 → Electron 启动失败,进程直接退出,连日志目录都不建。 dataStart也必须和原版完全一致,别按对齐公式重算。2.0.30 原版是 4 字节对齐(jsonLen2172835 →dataStart2172852,尾部 1 个 NUL);按 8 字节对齐重算会得到 2172856(+4),Electron 报「加载应用程序时发生错误」。- 字符串替换必须自检:曾因正则分组数对不上,写出
undefined.open让渲染进程崩。 - 往 minified 的
var/let声明列表里插东西,只能插name=expr:D1 那个锚点…,yie=B(…),bie=!1,xie=500;就是一条var声明,直接用逗号接 IIFE(,(function(){…})())会SyntaxError: Unexpected token '('——而且压缩后整个 bundle 就几行,node --check只会把错误扣在整行上,行号完全没用。要么给 IIFE 赋值目标,要么放到声明结尾;之后当独立语句。 - 验证产物时注意:asar 里
node_modules/@lydell/node-pty-win32-x64/*这类文件是app.asar.unpacked的,它们的offset指向外部文件,用 asar 内的数据去比对 integrity 必然 mismatch——原版也一样,别误判成补丁打坏了。 - 启动失败时看
%APPDATA%\ai.opencode.desktop\logs\<最新时间戳>\renderer.log,里面fatal renderer error的堆栈行:列直接指向出错 chunk 的哪一处替换。 - 改 asar 必须完全退出桌面端,热重载(Ctrl+R)会白屏(主进程缓存了旧 asar 头,偏移已变)。
- 官方原版备份命名
app.asar.bak-<版本号>;脚本发现已打过补丁又没有备份时会拒绝执行。撤销补丁用node apply-patch.mjs --restore(同样自动关/开桌面端)。 - 测试脚本改动时不要直接
--write:--write会taskkill掉真实桌面端。用假安装目录跑 dry-run /--out:LOCALAPPDATA=/tmp/fakelocal node apply-patch.mjs --out /tmp/patched.asar # %LOCALAPPDATA%/Programs/@opencode-aidesktop/{OpenCode.exe, resources/app.asar} - 调试口也不能写死 9222:Windows 上 Hyper-V / WSL / Docker 会占掉一段 TCP 保留端口(本机实测有
[1114,1613]、[9611,9710]、[50000,50059]),9222 落进去时 Electron 绑不上也不报错,verify 只会说「连不上调试口」。现在由debug-port.mjs挑端口,verify 反向扫 9222–9400。 - 后端 API 端口不固定(每次重启都变,实测 45278),时间补丁从
performance里取 base;/json/list里也不止一个页面(还有「本地服务管理器」),CDP 要挑oc://renderer/index.html。 - 会话页里查不到
[data-session-id](那是首页行专用),当前会话 id 只能从/api/session/<sid>/message的请求记录和[data-tab-key]反推,不能信「最近一次请求的 sid」。 - 时间 IIFE 不能塞进
bie=!0,xie=500,那条逗号声明(同 D1 的坑),只能接在;后面。 - 回复时间的 meta 行只在消息完成后才渲染,「流式中不显示」是天然行为;
verify.mjs --time因此要等 2.5s 让/message回来,否则误报。
opencode 升级后 minified 代码会变,脚本会报「锚点没找到」。此时:
- 从 asar 抽出所有
/out/renderer/assets/*.js,搜defaultOpen、data-timeline-part-id、ui.message.thought定位思考组件所在 chunk;搜onArchiveSession、home-session-archive定位首页会话行组件所在 chunk。 - 看新版
i=()=>...、get open(){...}、onOpenChange:t=>{...}以及 8 处删除按钮锚点的实际写法,更新apply-patch.mjs里的P1_FIND / P4_FIND / P8_FIND / P2_FIND和D_RULES。 - 重跑 dry-run,确认全部 OK 再
--write。
| opencode | 思考折叠 chunk | 删除按钮 chunk | asar 大小 |
|---|---|---|---|
| 2.0.29 | /out/renderer/assets/route-4O9GzN5h.js(350831 B) |
无此代码 | 176333529 → ~176334560 |
| 2.0.30 | /out/renderer/assets/route-CPNDqx0F.js(355075 → 356889 B) |
/out/renderer/assets/main-CfL8Gr4d.js(1321365 → 1321786 B) |
176341661 → 176343896 |
| 2.0.32 | /out/renderer/assets/route-BYzALZ62.js(357215 → 360467 B) |
/out/renderer/assets/main-CZyBNj5O.js(官方 1321667 → 1330501 B,含多选删除 + 时间) |
176346919 → 176359005 |
非官方补丁,直接改的是 opencode 桌面端的打包文件(app.asar)。每次 opencode 升级都会覆盖掉,需要重新打一次。脚本会先把官方原版备份成 app.asar.bak-<版本号>,出问题可以用它还原。