Files
Imanol Maiztegui b4fd1be904 Per-turn diff view with clickable banner (#9999)
* refactor(vscode): replace collapsible diff summary with clickable banner

Replace the expandable accordion-based diff summary in session turns
with a simpler clickable button that opens the dedicated changes view
via a postMessage to the extension host. This removes the inline file
list expansion in favor of the native VS Code changes panel.

- Remove Collapsible/Accordion/StickyAccordionHeader components
- Remove getDirectory/getFilename helpers and expanded state management
- Add openChanges action via useVSCode context
- Style the trigger as a minimal button with hover chevron indicator
- Update story name/description to reflect new behavior

* feat(vscode/diff): simplify DiffSource interface to declarative fetch model

Convert DiffSource from a class-based lifecycle pattern (initialFetch/start/dispose)
to a minimal declarative interface where sources only implement `fetch()` and
optionally `fetchFile`/`revert`/`dispose`. Move all polling, hash-dedup, loading
state, and message posting responsibility into SourceController.

- Replace class-based SessionDiffSource/WorktreeDiffSource with factory functions
- Introduce DiffSourceFetch return type with stopPolling flag for terminal states
- Remove DiffSourcePost/DiffSourceMessage types in favor of controller-owned posting
- SourceController now owns setInterval polling and hash-based dedup logic
- Rename requestFile → fetchFile, revertFile → revert, make dispose optional
- Update all unit tests to match the new declarative source contract

* refactor(vscode): add per-turn diff viewing with hidden picker mode

Introduce a TurnDiffSource that fetches diffs scoped to a single user
message rather than the full session snapshot. The diff viewer can now
open in a fixed, non-switchable mode when invoked from a specific turn.

- Add `turn.ts` source with factory, descriptor, and id helpers
- Extend PanelContext with `hidePicker` flag to suppress source selector
- Thread `turnId` from webview message through sidebar handler to command
- Catalog returns empty descriptors when picker is hidden
- Export `toSessionDiffFile` from session source for reuse in turn source
- Add `turn` to DiffSourceType union
- Add unit tests for turn source fetch behavior and catalog integration

* fix(vscode/diff): always log DiffSource fetch errors

Initial-fetch errors were only posted as a discarded 'error' message and
had no console trace, making them invisible in production. Log on both
initial and polling ticks so Extension Host output captures the failure.

* chore: update kilo-vscode visual regression baselines

* feat(vscode/diff): self-cancel polling when source reports completion

Convert polling callback to async and use runFetch return value to
stop the interval once the diff source signals it is done, avoiding
unnecessary continued fetches after completion.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-07 13:41:03 +02:00

87 lines
2.7 KiB
TypeScript

import { describe, it, expect } from "bun:test"
import type { SnapshotFileDiff } from "@kilocode/sdk/v2/client"
import {
createTurnDiffSource,
turnDescriptor,
turnSourceId,
TURN_PREFIX,
type TurnDiffFetch,
} from "../../src/diff/sources/turn"
type FetchCall = { sessionID: string; messageID: string; directory?: string }
function recording(result: SnapshotFileDiff[] | Error): { fetch: TurnDiffFetch; calls: FetchCall[] } {
const calls: FetchCall[] = []
const fetch: TurnDiffFetch = async (params) => {
calls.push(params)
if (result instanceof Error) throw result
return result
}
return { fetch, calls }
}
const samplePatch = [
"diff --git a/foo.ts b/foo.ts",
"--- a/foo.ts",
"+++ b/foo.ts",
"@@ -1,1 +1,1 @@",
"-old",
"+new",
].join("\n")
describe("createTurnDiffSource.fetch", () => {
it("calls the fetch with sessionID + messageID + directory", async () => {
const { fetch, calls } = recording([])
const source = createTurnDiffSource("sess", "msg", fetch, "/repo")
await source.fetch()
expect(calls).toEqual([{ sessionID: "sess", messageID: "msg", directory: "/repo" }])
})
it("returns diffs with stopPolling=true so the controller skips polling", async () => {
const { fetch } = recording([
{ file: "foo.ts", patch: samplePatch, additions: 1, deletions: 1, status: "modified" },
])
const source = createTurnDiffSource("sess", "msg", fetch)
const result = await source.fetch()
expect(result.stopPolling).toBe(true)
expect(result.notice).toBeUndefined()
expect(result.diffs).toHaveLength(1)
expect(result.diffs[0]!.file).toBe("foo.ts")
expect(result.diffs[0]!.before).toBe("old\n")
expect(result.diffs[0]!.after).toBe("new\n")
})
it("propagates underlying fetch errors", async () => {
const { fetch } = recording(new Error("backend unavailable"))
const source = createTurnDiffSource("sess", "msg", fetch)
await expect(source.fetch()).rejects.toThrow("backend unavailable")
})
it("calls fetch without directory when workspaceRoot is not given", async () => {
const { fetch, calls } = recording([])
const source = createTurnDiffSource("sess", "msg", fetch)
await source.fetch()
expect(calls).toEqual([{ sessionID: "sess", messageID: "msg", directory: undefined }])
})
})
describe("turn source descriptor + id helpers", () => {
it("encodes sessionId + messageId in the source id", () => {
expect(turnSourceId("abc", "42")).toBe(`${TURN_PREFIX}abc:42`)
})
it("produces a descriptor with type='turn' and no revert capability", () => {
const desc = turnDescriptor("abc", "42")
expect(desc.id).toBe("turn:abc:42")
expect(desc.type).toBe("turn")
expect(desc.capabilities).toEqual({ revert: false, comments: true })
})
})