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:
+39
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user