fix(changes): 会话事件改走宿主 changes.ops 路由 + diff 展开二级设置 + 视觉迭代

CI 修复(plugin-mount 的 ctx.sessions.get TypeError ×1003):
- 客户端 runtime 无事件日志面——badge/SessionLens 原在客户端直读
  ctx.sessions.get(id).events / ctx.on('session/event'),全部不可达。
- 新增宿主路由 changes.ops:live 会话日志优先,冷会话回放
  sessionPersistence.inspect;afterSeq 增量(缺省游标含 seq 0,
  显式游标为 seq > afterSeq,同 sidechat.events 约定),窗口上限
  4000 与客户端累加器一致。
- ChangesTab 轮询(mount 一拉 + visible 2.5s 增量)折入 ops,写
  模块级 opCounts 缓存供 badge 同步读取;SessionLens 改纯展示。

diff 展开二级设置(changesDiffFloat,默认浮窗):
- prefs 四处接线 + git tab settings select(浮窗/面板,图标选项卡)
- 预览面板「展开为 diff tab」按设置落点:浮窗居中弹出或源 pane
  下方 split;i18n 全 21 词典同步。

视觉迭代(真机挂载截图审查后调整):
- Session 视角:kind 徽章为左轨,大小+相对时间右对齐元列;失败
  操作行红色内嵌条;文件组头降为三级标签并加组间距。
- Git 视角:untracked '?' 徽章与新增同绿色;stage 按钮改 plus 图标。
- diff 折叠行:可展开折叠带 › 箭头 + 次级对比度,静态 gap 不再
  显示 pointer/hover;预览头路径改尾部截断(RTL 技巧,保文件名)。

验证:typecheck ✓ / vitest 1123 ✓ / pnpm test:mount(钉 alpha.2 CLI)
14/14 ✓;真机挂载截图(badge=5、双视角、浮窗展开)0 console error。
This commit is contained in:
Menghuan1918
2026-08-31 17:52:59 +08:00
parent 24a3813c63
commit f024fbf092
40 changed files with 456 additions and 112 deletions
+39 -1
View File
@@ -18,7 +18,7 @@ import { basename, dirname, extname, isAbsolute, join } from 'node:path'
import type { IncomingMessage } from 'node:http'
import type { Duplex } from 'node:stream'
import { WebSocket, WebSocketServer } from 'ws'
import type { Context, SidebarHttpRequest } from './context-types.ts'
import type { Context, SidebarHttpRequest, SidebarSessionEvent } from './context-types.ts'
import {
Config,
PrefsSchema,
@@ -455,6 +455,44 @@ function buildApi(
const rev = requireString(payload, 'rev')
return { content: await git.show(cwd, rev, path, repoRoot) }
},
// The session's file-tool events for the changes tab's session lens
// (and its badge): the CLIENT runtime's sessions face has no event-log
// access, so the events cross the wire here — live session log first,
// the persisted logical log for not-yet-hydrated sessions. Only the
// two event types the lens folds are sent, narrowed to `seq > afterSeq`
// so polling is a small delta, with the same recent-window cap the
// client accumulator applies.
'changes.ops': async (payload) => {
const sessionId = requireString(payload, 'sessionId')
const rawAfter = (payload as { afterSeq?: unknown } | null)?.afterSeq
if (rawAfter !== undefined
&& (typeof rawAfter !== 'number' || !Number.isSafeInteger(rawAfter) || rawAfter < 0)) {
throw new SidebarError('bad-request', 'afterSeq must be a non-negative integer')
}
// An absent cursor means "from the very first event" — a session whose
// log opens on a tool event (subagent seeds do) carries seq 0, which a
// literal `> 0` comparison would drop, so the absent case floors at -1.
const afterSeq = rawAfter ?? -1
let events: readonly SidebarSessionEvent[] | undefined = ctx.sessions.get(sessionId)?.events
if (events === undefined) {
const persistence = ctx.get('sessionPersistence')
if (persistence !== undefined) {
try {
events = (await persistence.inspect(sessionId)).events
} catch {
// Cold read unavailable (session never persisted): an empty
// window is the honest answer, not a wire error.
}
}
}
if (events === undefined) return { events: [], lastSeq: Math.max(afterSeq, 0) }
const CHANGES_EVENTS_CAP = 4000
const filtered = events.filter(
event => (event.type === 'tool/call' || event.type === 'tool/result') && event.seq > afterSeq,
)
const window = filtered.length > CHANGES_EVENTS_CAP ? filtered.slice(filtered.length - CHANGES_EVENTS_CAP) : filtered
return { events: window, lastSeq: window.at(-1)?.seq ?? afterSeq }
},
// Release a terminal immediately. The WebSocket close frame already does
// this while the socket is open; this route covers the tab-close that
// happens while the socket is down (reconnect loop), so a closed tab can