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:
+27
-12
@@ -1,15 +1,16 @@
|
||||
/**
|
||||
* The unified changes tab shell: the lens switcher swaps the Git lens for
|
||||
* the session lens, and a Git-lens file row opens the shared preview pane
|
||||
* the session lens, a Git-lens file row opens the shared preview pane
|
||||
* (loaded through the mocked git API, rendered by the shared DiffFiles
|
||||
* stack) instead of minting a diff tab directly.
|
||||
* stack), and the session ops ride the mocked `changes.ops` poll — the
|
||||
* event poll, the op fold and the badge cache all live in the tab.
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { ChangesTab } from '../src/client/changes/ChangesTab.tsx'
|
||||
import { ChangesTab, opCountOf } from '../src/client/changes/ChangesTab.tsx'
|
||||
import { createSidebarStore } from '../src/client/state.ts'
|
||||
import { api, type GitStatusResult, type GitWorktree } from '../src/client/api.ts'
|
||||
import type { Context } from '../src/context-types.ts'
|
||||
@@ -19,16 +20,21 @@ import type { SidebarTab } from '../src/client/state.ts'
|
||||
|
||||
const MAIN = 'C:/repo/main'
|
||||
|
||||
/** One synthetic tool/call + tool/result pair (write of one file). */
|
||||
function writeEvents(seq: number, path: string): Array<{ type: string; seq: number; time: number; data: Record<string, unknown> }> {
|
||||
return [
|
||||
{ type: 'tool/call', seq, time: seq, data: { name: 'write', callId: `w${seq}`, arguments: JSON.stringify({ file_path: path, content: 'body' }) } },
|
||||
{ type: 'tool/result', seq: seq + 1, time: seq + 1, data: { message: { source: { kind: 'tool', callId: `w${seq}` }, content: [{ type: 'tool-result', content: [{ type: 'text', text: 'ok' }] }] } } },
|
||||
]
|
||||
}
|
||||
|
||||
function fakeContext(): Context {
|
||||
return {
|
||||
sessions: { get: () => undefined },
|
||||
get: () => undefined,
|
||||
on: () => () => { /* no-op */ },
|
||||
} as unknown as Context
|
||||
}
|
||||
|
||||
function mount(root: Root): void {
|
||||
const tab: SidebarTab = { id: 'git', type: 'git', title: 'Changes' }
|
||||
function mount(root: Root, tab: SidebarTab = { id: 'git', type: 'git', title: 'Changes' }): void {
|
||||
act(() => {
|
||||
root.render(createElement(ChangesTab, {
|
||||
ctx: fakeContext(),
|
||||
@@ -108,21 +114,30 @@ describe('ChangesTab', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('switches to the session lens, which shows its empty state without ops', async () => {
|
||||
it('folds the polled session events into session-lens rows and the badge cache', async () => {
|
||||
mockGit([])
|
||||
const ops = vi.spyOn(api, 'changesOps')
|
||||
.mockResolvedValue({ events: [...writeEvents(1, 'src/a.ts'), ...writeEvents(3, 'src/b.ts')], lastSeq: 4 })
|
||||
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root: Root = createRoot(container)
|
||||
try {
|
||||
mount(root)
|
||||
await flushEffects()
|
||||
|
||||
// The tab pulled once on mount and published the op count.
|
||||
expect(ops).toHaveBeenCalled()
|
||||
expect(opCountOf('session')).toBe(2)
|
||||
|
||||
const group = container.querySelector('[role="group"]')
|
||||
expect(group).not.toBeNull()
|
||||
const buttons = [...group!.querySelectorAll<HTMLButtonElement>('button')]
|
||||
await act(async () => { buttons[1]!.click() })
|
||||
// No session events in the fake context: the session lens empty state
|
||||
// (locale-agnostic — the test env may resolve zh or en).
|
||||
expect(container.textContent).toMatch(/本会话还没有文件操作|No file operations/)
|
||||
// Two files grouped, newest first; the write chip carries its count.
|
||||
expect(container.textContent).toContain('src/b.ts')
|
||||
expect(container.textContent).toContain('src/a.ts')
|
||||
const chips = [...container.querySelectorAll('button[aria-pressed]')]
|
||||
expect(chips.some(chip => /写入\s*2|Write\s*2/.test(chip.textContent ?? ''))).toBe(true)
|
||||
// The git lens (its branch picker) is unmounted by the switch.
|
||||
expect(container.querySelector('select')).toBeNull()
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user