mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(agent-manager): close diff cache review gaps
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import * as fs from "fs/promises"
|
||||
import { createHash } from "crypto"
|
||||
import { binaryFile } from "../diff/shared/binary"
|
||||
import { imageMime, loadImage, readImageFile } from "../diff/shared/image"
|
||||
import { resolveInside } from "../diff/shared/path"
|
||||
@@ -144,7 +145,46 @@ async function statStamp(dir: string, file: string): Promise<string> {
|
||||
if (!full) return `missing:${file}`
|
||||
const stat = await fs.lstat(full).catch(() => undefined)
|
||||
if (!stat) return `missing:${file}`
|
||||
return `${stat.size}:${stat.mtimeMs}`
|
||||
return `${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.ino ?? 0}`
|
||||
}
|
||||
|
||||
async function contentStamp(dir: string, file: string, status: Status): Promise<string> {
|
||||
if (status === "deleted") return "deleted"
|
||||
const full = resolveInside(dir, file)
|
||||
if (!full) return `missing:${file}`
|
||||
const stat = await fs.lstat(full).catch(() => undefined)
|
||||
if (!stat) return `missing:${file}`
|
||||
const value = stat.isSymbolicLink()
|
||||
? Buffer.from(await fs.readlink(full))
|
||||
: stat.isFile()
|
||||
? await fs.readFile(full).catch(() => undefined)
|
||||
: undefined
|
||||
if (!value) return `unreadable:${file}`
|
||||
return createHash("sha256").update(value).digest("hex")
|
||||
}
|
||||
|
||||
function detailStamp(value: WorktreeDiffEntry, meta: Meta): string {
|
||||
if (meta.status === "deleted") return "deleted"
|
||||
const data = value.image?.after?.data
|
||||
if (data) return createHash("sha256").update(Buffer.from(data, "base64")).digest("hex")
|
||||
return createHash("sha256")
|
||||
.update(value.after ?? "")
|
||||
.digest("hex")
|
||||
}
|
||||
|
||||
async function detailReads(git: GitOps, dir: string, anc: string, meta: Meta, signal?: AbortSignal) {
|
||||
return Promise.all([
|
||||
readBefore(git, dir, anc, meta.file, meta.status, signal),
|
||||
readAfter(dir, meta.file, meta.status),
|
||||
meta.tracked ? unifiedPatch(git, dir, anc, meta.file, signal) : Promise.resolve(""),
|
||||
])
|
||||
}
|
||||
|
||||
async function sizes(git: GitOps, dir: string, anc: string, meta: Meta, signal?: AbortSignal) {
|
||||
return Promise.all([
|
||||
meta.status === "added" ? 0 : blobSize(git, dir, anc, meta.file, signal),
|
||||
meta.status === "deleted" ? 0 : fileSize(dir, meta.file),
|
||||
])
|
||||
}
|
||||
|
||||
async function lineCount(file: string): Promise<number> {
|
||||
@@ -265,21 +305,26 @@ export async function diffSummary(git: GitOps, dir: string, base: string, log?:
|
||||
export function createLocalDiff(git: GitOps, log?: Log) {
|
||||
const states = new Map<string, { anc: string; metas: Map<string, Meta> }>()
|
||||
const generations = new Map<string, number>()
|
||||
const details = new Map<string, { value: WorktreeDiffEntry; bytes: number }>()
|
||||
const details = new Map<string, { value: WorktreeDiffEntry; bytes: number; stamp: string }>()
|
||||
const pending = new Map<string, { signal?: AbortSignal; work: Promise<WorktreeDiffEntry> }>()
|
||||
let bytes = 0
|
||||
|
||||
const remember = (id: string, value: WorktreeDiffEntry) => {
|
||||
const size =
|
||||
(value.before?.length ?? 0) +
|
||||
(value.after?.length ?? 0) +
|
||||
(value.patch?.length ?? 0) +
|
||||
(value.image?.before?.data?.length ?? 0) +
|
||||
(value.image?.after?.data?.length ?? 0)
|
||||
const forget = (id: string) => {
|
||||
const value = details.get(id)
|
||||
if (!value) return
|
||||
bytes -= value.bytes
|
||||
details.delete(id)
|
||||
}
|
||||
|
||||
const remember = (id: string, value: WorktreeDiffEntry, stamp: string) => {
|
||||
const size = [value.before, value.after, value.patch, value.image?.before?.data, value.image?.after?.data].reduce(
|
||||
(sum, value) => sum + Buffer.byteLength(value ?? ""),
|
||||
0,
|
||||
)
|
||||
const current = details.get(id)
|
||||
if (current) bytes -= current.bytes
|
||||
details.delete(id)
|
||||
details.set(id, { value, bytes: size })
|
||||
details.set(id, { value, bytes: size, stamp })
|
||||
bytes += size
|
||||
while (details.size > 128 || bytes > 64 * 1024 * 1024) {
|
||||
const key = details.keys().next().value!
|
||||
@@ -311,11 +356,14 @@ export function createLocalDiff(git: GitOps, log?: Log) {
|
||||
if (!state) return diffFile(git, dir, base, file, log)
|
||||
const meta = state.metas.get(file)
|
||||
if (!meta) return null
|
||||
const id = `${dir}\0${base}\0${state.anc}\0${file}\0${meta.stamp}`
|
||||
const id = `${dir}\0${base}\0${state.anc}\0${file}\0${meta.tracked}\0${meta.status}\0${meta.additions}\0${meta.deletions}\0${meta.binary}\0${meta.stamp}`
|
||||
const cached = details.get(id)
|
||||
if (cached) {
|
||||
remember(id, cached.value)
|
||||
return cached.value
|
||||
if (cached.stamp === (await contentStamp(dir, file, meta.status))) {
|
||||
remember(id, cached.value, cached.stamp)
|
||||
return cached.value
|
||||
}
|
||||
forget(id)
|
||||
}
|
||||
const current = pending.get(id)
|
||||
if (current && !current.signal?.aborted) return current.work
|
||||
@@ -325,7 +373,8 @@ export function createLocalDiff(git: GitOps, log?: Log) {
|
||||
(value) => {
|
||||
if (pending.get(id)?.work !== work) return
|
||||
pending.delete(id)
|
||||
remember(id, value)
|
||||
if (value.image?.before?.error === "unreadable" || value.image?.after?.error === "unreadable") return
|
||||
remember(id, value, detailStamp(value, meta))
|
||||
},
|
||||
() => {
|
||||
if (pending.get(id)?.work === work) pending.delete(id)
|
||||
@@ -390,7 +439,7 @@ async function detailMeta(git: GitOps, dir: string, anc: string, file: string):
|
||||
|
||||
async function blobSize(git: GitOps, dir: string, anc: string, file: string, signal?: AbortSignal): Promise<number> {
|
||||
const result = await git.execGit(["cat-file", "-s", `${anc}:${file}`], dir, { signal })
|
||||
if (result.code !== 0) return 0
|
||||
if (result.code !== 0) throw new Error(`Could not read base blob for ${file}`)
|
||||
return parseInt(result.stdout.trim(), 10) || 0
|
||||
}
|
||||
|
||||
@@ -430,18 +479,21 @@ async function readBefore(
|
||||
): Promise<string> {
|
||||
if (status === "added") return ""
|
||||
const result = await git.execGit(["show", `${anc}:${file}`], dir, { signal })
|
||||
return result.code === 0 ? result.stdout : ""
|
||||
if (result.code !== 0) throw new Error(`Could not read base file for ${file}`)
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
async function readAfter(dir: string, file: string, status: Status): Promise<string> {
|
||||
if (status === "deleted") return ""
|
||||
const full = resolveInside(dir, file)
|
||||
if (!full) return ""
|
||||
if (!full) throw new Error(`Could not resolve working file for ${file}`)
|
||||
const stat = await fs.lstat(full).catch(() => undefined)
|
||||
if (!stat) return ""
|
||||
if (!stat) throw new Error(`Could not read working file for ${file}`)
|
||||
if (stat.isSymbolicLink()) return fs.readlink(full).catch(() => "")
|
||||
if (!stat.isFile()) return ""
|
||||
return fs.readFile(full, "utf-8").catch(() => "")
|
||||
if (!stat.isFile()) throw new Error(`Working path is not a file: ${file}`)
|
||||
return fs.readFile(full, "utf-8").catch(() => {
|
||||
throw new Error(`Could not read working file for ${file}`)
|
||||
})
|
||||
}
|
||||
|
||||
async function unifiedPatch(
|
||||
@@ -456,7 +508,8 @@ async function unifiedPatch(
|
||||
dir,
|
||||
{ signal },
|
||||
)
|
||||
return result.code === 0 ? result.stdout : ""
|
||||
if (result.code !== 0) throw new Error(`Could not create diff for ${file}`)
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
function linesOf(text: string): number {
|
||||
@@ -493,10 +546,8 @@ async function materialize(
|
||||
): Promise<WorktreeDiffEntry> {
|
||||
const mime = imageMime(meta.file)
|
||||
if (meta.binary && !mime) return summarize(meta)
|
||||
const [beforeBytes, afterBytes] = await Promise.all([
|
||||
meta.status === "added" ? 0 : blobSize(git, dir, anc, meta.file, signal),
|
||||
meta.status === "deleted" ? 0 : fileSize(dir, meta.file),
|
||||
])
|
||||
const [beforeBytes, afterBytes] = await sizes(git, dir, anc, meta, signal)
|
||||
if (signal?.aborted) throw new Error("Diff detail aborted")
|
||||
if (mime) {
|
||||
const image = await loadImage(
|
||||
meta.file,
|
||||
@@ -505,6 +556,7 @@ async function materialize(
|
||||
: { bytes: beforeBytes, read: () => readBlob(git, dir, anc, meta.file, signal) },
|
||||
meta.status === "deleted" ? undefined : { bytes: afterBytes, read: () => readFile(dir, meta.file) },
|
||||
)
|
||||
if (signal?.aborted) throw new Error("Diff detail aborted")
|
||||
return { ...summarize(meta), summarized: false, image }
|
||||
}
|
||||
// Cheap size probe before materializing content — protects the extension
|
||||
@@ -520,11 +572,8 @@ async function materialize(
|
||||
return summarize(meta)
|
||||
}
|
||||
|
||||
const [before, after, tracked] = await Promise.all([
|
||||
readBefore(git, dir, anc, meta.file, meta.status, signal),
|
||||
readAfter(dir, meta.file, meta.status),
|
||||
meta.tracked ? unifiedPatch(git, dir, anc, meta.file, signal) : Promise.resolve(""),
|
||||
])
|
||||
const [before, after, tracked] = await detailReads(git, dir, anc, meta, signal)
|
||||
if (signal?.aborted) throw new Error("Diff detail aborted")
|
||||
const patch = meta.tracked ? tracked : buildUntrackedPatch(meta.file, after)
|
||||
const additions = meta.status === "added" && meta.additions === 0 && !meta.tracked ? linesOf(after) : meta.additions
|
||||
return {
|
||||
|
||||
@@ -67,6 +67,7 @@ export class SourceController {
|
||||
private interval: ReturnType<typeof setInterval> | undefined
|
||||
private lastHash: string | undefined
|
||||
private epoch = 0
|
||||
private readonly fetches = new Map<DiffSource, Promise<boolean>>()
|
||||
|
||||
constructor(
|
||||
private readonly build: (id: string, ctx: PanelContext) => DiffSource,
|
||||
@@ -91,6 +92,7 @@ export class SourceController {
|
||||
stop(): void {
|
||||
this.epoch++
|
||||
this.stopPolling()
|
||||
this.fetches.clear()
|
||||
this.active?.dispose?.()
|
||||
this.active = undefined
|
||||
this.activeId = undefined
|
||||
@@ -117,7 +119,7 @@ export class SourceController {
|
||||
|
||||
if (opts.fetch === false) return
|
||||
|
||||
const keepPolling = await this.runFetch(source, epoch, true)
|
||||
const keepPolling = await this.fetch(source, epoch, true)
|
||||
// Prevents the polling interval from starting after teardown or swap.
|
||||
if (this.epoch !== epoch || this.activeId !== id) return
|
||||
if (opts.poll !== false && keepPolling) this.startPolling(source, epoch)
|
||||
@@ -151,7 +153,7 @@ export class SourceController {
|
||||
// Push fresh diffs immediately after a successful revert so the webview
|
||||
// doesn't have to wait for the next polling tick.
|
||||
if (result.ok && this.epoch === epoch && this.active === source) {
|
||||
await this.runFetch(source, epoch, false)
|
||||
await this.fetch(source, epoch, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +162,7 @@ export class SourceController {
|
||||
const source = this.active
|
||||
if (!source) return
|
||||
const epoch = this.epoch
|
||||
await this.runFetch(source, epoch, true)
|
||||
await this.fetch(source, epoch, true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -251,7 +253,7 @@ export class SourceController {
|
||||
if (busy) return
|
||||
busy = true
|
||||
// Self-cancel when the tick reports the source is done
|
||||
const keep = await this.runFetch(source, epoch, false).finally(() => {
|
||||
const keep = await this.fetch(source, epoch, false).finally(() => {
|
||||
busy = false
|
||||
})
|
||||
if (!keep && this.epoch === epoch && this.active === source) this.stopPolling()
|
||||
@@ -264,4 +266,20 @@ export class SourceController {
|
||||
this.interval = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private fetch(source: DiffSource, epoch: number, initial: boolean): Promise<boolean> {
|
||||
const current = this.fetches.get(source)
|
||||
if (current) return current
|
||||
const work = this.runFetch(source, epoch, initial)
|
||||
this.fetches.set(source, work)
|
||||
work.then(
|
||||
() => {
|
||||
if (this.fetches.get(source) === work) this.fetches.delete(source)
|
||||
},
|
||||
() => {
|
||||
if (this.fetches.get(source) === work) this.fetches.delete(source)
|
||||
},
|
||||
)
|
||||
return work
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("createWorktreeDiffs", () => {
|
||||
it("stores full diffs per session", () => {
|
||||
withDiffs((diffs) => {
|
||||
diffs.onWorktreeDiff({ type: "agentManager.worktreeDiff", sessionId: "s1", diffs: [diff("a.ts")] })
|
||||
expect(diffs.diffDatas()["s1"]).toHaveLength(1)
|
||||
expect(diffs.diffDatas()["single\0s1"]).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("createWorktreeDiffs", () => {
|
||||
file: "a.ts",
|
||||
diff: diff("a.ts", 9),
|
||||
})
|
||||
expect(diffs.diffDatas()["s1"]![0]!.additions).toBe(9)
|
||||
expect(diffs.diffDatas()["single\0s1"]![0]!.additions).toBe(9)
|
||||
expect(diffs.diffFileLoadingFor(() => "s1").size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -167,7 +167,7 @@ import { PRPanelHost } from "./pr/PRPanelHost"
|
||||
import { createRevertFile } from "./revert-file"
|
||||
import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView"
|
||||
import { createApplyToLocal } from "./apply-to-local"
|
||||
import { createWorktreeDiffs, wireDiffId } from "./worktree-diffs"
|
||||
import { createWorktreeDiffs, diffDataKey, wireDiffId } from "./worktree-diffs"
|
||||
import type { ReviewComment } from "../diff-viewer/review-comments"
|
||||
import { createReviewComposers } from "./review-composers"
|
||||
import type { SidebarSearchMenuRef } from "./SidebarSearchMenu"
|
||||
@@ -333,6 +333,7 @@ const AgentManagerContent: Component = () => {
|
||||
setSidePanel(SidePanel.Terminal)
|
||||
}
|
||||
const composers = createReviewComposers(currentProjectId)
|
||||
createEffect(on(activeProjectId, (_next, previous) => previous && composers.clearProject(previous), { defer: true }))
|
||||
const reviewState = createReviewState()
|
||||
const reviewOpenByContext = reviewState.open
|
||||
const setReviewOpenByContext = reviewState.setOpen
|
||||
@@ -549,16 +550,6 @@ const AgentManagerContent: Component = () => {
|
||||
if (sel === null) return
|
||||
setReviewOpenForContext(sel, open)
|
||||
}
|
||||
const reviewComments = createMemo(() => {
|
||||
const sel = selection()
|
||||
if (sel === null) return [] as ReviewComment[]
|
||||
return readReviewComments(reviewCommentsByContext(), currentProjectId() ?? "single", sel)
|
||||
})
|
||||
const setReviewCommentsForSelection = (comments: ReviewComment[]) => {
|
||||
const sel = selection()
|
||||
if (sel === null) return
|
||||
setReviewCommentsByContext((prev) => setReviewComments(prev, currentProjectId() ?? "single", sel, comments))
|
||||
}
|
||||
const apply = createApplyToLocal({
|
||||
vscode,
|
||||
dialog,
|
||||
@@ -719,6 +710,8 @@ const AgentManagerContent: Component = () => {
|
||||
})
|
||||
createEffect(() => {
|
||||
const ids = new Set(worktrees().map((wt) => wt.id))
|
||||
composers.prune(ids)
|
||||
composers.prune(ids)
|
||||
setReviewOpenByContext((prev) => {
|
||||
const next = pruneReviewState(prev, currentProjectId() ?? "single", ids)
|
||||
if (Object.keys(next).length === Object.keys(prev).length) return prev
|
||||
@@ -1661,6 +1654,17 @@ const AgentManagerContent: Component = () => {
|
||||
// The composite id (ctx#scope) the extension keys diff data by.
|
||||
const diffScopeId = review.id
|
||||
|
||||
const reviewComments = createMemo(() => {
|
||||
const key = diffScopeId()
|
||||
if (!key) return [] as ReviewComment[]
|
||||
return readReviewComments(reviewCommentsByContext(), currentProjectId() ?? "single", key)
|
||||
})
|
||||
const setReviewCommentsForSelection = (comments: ReviewComment[]) => {
|
||||
const key = diffScopeId()
|
||||
if (!key) return
|
||||
setReviewCommentsByContext((prev) => setReviewComments(prev, currentProjectId() ?? "single", key, comments))
|
||||
}
|
||||
|
||||
const diffScopeControls = (compact: boolean) => (
|
||||
<DiffScopeControls
|
||||
descriptors={review.descriptors()}
|
||||
@@ -1726,20 +1730,15 @@ const AgentManagerContent: Component = () => {
|
||||
tabFocus.restore()
|
||||
}
|
||||
|
||||
// Data for the review tab / side panel: keyed by the composite diff id
|
||||
// (ctx#scope) the extension pushes, so each scope keeps its own file set and
|
||||
// switching back to a fetched scope is instant.
|
||||
const reviewDiffs = createMemo(() => {
|
||||
const data = diffDatas()
|
||||
const key = diffScopeId()
|
||||
if (!key) return []
|
||||
return data[key] ?? []
|
||||
return data[diffDataKey(activeProjectId(), key)] ?? []
|
||||
})
|
||||
|
||||
const diffSessionKey = createMemo(() => diffScopeId() ?? "")
|
||||
|
||||
// Source-level notice for the active composite id (e.g. snapshots disabled
|
||||
// for the Session scope), shown as a banner instead of the empty state.
|
||||
const diffNotice = createMemo(() => {
|
||||
const key = diffScopeId()
|
||||
if (!key) return undefined
|
||||
@@ -2634,13 +2633,13 @@ const AgentManagerContent: Component = () => {
|
||||
data={diffDatas}
|
||||
loading={(key) => diffs.diffLoadingFor(() => key)}
|
||||
loadingFiles={(key) => diffs.diffFileLoadingFor(() => key)}
|
||||
notice={(key) => diffNotices()[key]}
|
||||
comments={(ctx) =>
|
||||
readReviewComments(reviewCommentsByContext(), currentProjectId() ?? "single", ctx)
|
||||
notice={(key) => diffNotices()[diffDataKey(activeProjectId(), key)]}
|
||||
comments={(key) =>
|
||||
readReviewComments(reviewCommentsByContext(), currentProjectId() ?? "single", key)
|
||||
}
|
||||
setComments={(ctx, comments) =>
|
||||
setComments={(key, comments) =>
|
||||
setReviewCommentsByContext((prev) =>
|
||||
setReviewComments(prev, currentProjectId() ?? "single", ctx, comments),
|
||||
setReviewComments(prev, currentProjectId() ?? "single", key, comments),
|
||||
)
|
||||
}
|
||||
composer={composers.get}
|
||||
@@ -2668,6 +2667,11 @@ const AgentManagerContent: Component = () => {
|
||||
}}
|
||||
revertingFiles={revertCtl.revertingFor}
|
||||
activeTerminalId={terms.activeId()}
|
||||
contexts={() => new Set(worktrees().map((wt) => wt.id))}
|
||||
onEvict={(key) => {
|
||||
composers.drop(key)
|
||||
diffs.drop(key)
|
||||
}}
|
||||
/>
|
||||
<Show when={sidePanel() === SidePanel.PR && activePR()}>
|
||||
<PRPanelHost
|
||||
|
||||
@@ -159,6 +159,21 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => props.active,
|
||||
(active) => {
|
||||
if (!active) return
|
||||
const value = reviewComposerDraft(composer())
|
||||
const edit = reviewComposerEdit(composer())
|
||||
setDraft(value)
|
||||
setEditing(edit)
|
||||
draftMeta = composer().draft
|
||||
editMeta = composer().edit
|
||||
},
|
||||
),
|
||||
)
|
||||
const setOpen = (files: string[] | ((prev: string[]) => string[])) => {
|
||||
const key = props.sessionKey ?? ""
|
||||
const current = open()
|
||||
@@ -249,6 +264,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
on(
|
||||
() => props.sessionKey,
|
||||
() => {
|
||||
if (props.active === false) return
|
||||
setDraft(null)
|
||||
draftMeta = null
|
||||
setEditing(null)
|
||||
@@ -393,8 +409,10 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
|
||||
const result = buildFileAnnotations(file, commentsByFile().get(file) ?? [], editing(), draft(), draftMeta, editMeta)
|
||||
draftMeta = result.draftMeta
|
||||
editMeta = result.editMeta
|
||||
composer().draft = draft() ? draftMeta : null
|
||||
composer().edit = editing() ? editMeta : null
|
||||
if (props.active !== false) {
|
||||
composer().draft = draft() ? draftMeta : null
|
||||
composer().edit = editing() ? editMeta : null
|
||||
}
|
||||
return result.annotations
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { WorktreeFileDiff } from "../src/types/messages"
|
||||
import type { ReviewComment } from "../diff-viewer/review-comments"
|
||||
import type { ReviewComposer } from "../diff-viewer/review-annotations"
|
||||
import { DiffPanel } from "./DiffPanel"
|
||||
import { diffDataKey } from "./worktree-diffs"
|
||||
|
||||
const CACHE_SIZE = 4
|
||||
|
||||
@@ -18,6 +19,8 @@ interface Props {
|
||||
context: Accessor<string | undefined>
|
||||
project: Accessor<string | undefined>
|
||||
active: Accessor<boolean>
|
||||
onEvict?: (key: string) => void
|
||||
contexts: Accessor<Set<string>>
|
||||
data: Accessor<Record<string, WorktreeFileDiff[]>>
|
||||
loading: (key: string) => boolean
|
||||
loadingFiles: (key: string) => Set<string>
|
||||
@@ -46,6 +49,15 @@ export const DiffPanelCache: Component<Props> = (props) => {
|
||||
const [entries, setEntries] = createSignal<Entry[]>([])
|
||||
let used = 0
|
||||
|
||||
createEffect(() => {
|
||||
const contexts = props.contexts()
|
||||
setEntries((prev) => {
|
||||
const next = prev.filter((entry) => entry.ctx === "local" || contexts.has(entry.ctx))
|
||||
for (const item of prev) if (!next.includes(item)) props.onEvict?.(item.cacheKey)
|
||||
return next
|
||||
})
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.active()) return
|
||||
const key = props.current()
|
||||
@@ -59,12 +71,19 @@ export const DiffPanelCache: Component<Props> = (props) => {
|
||||
const current = scoped.find((item) => item.cacheKey === cacheKey)
|
||||
if (current) {
|
||||
current.used = ++used
|
||||
for (const item of prev) if (!scoped.includes(item)) props.onEvict?.(item.cacheKey)
|
||||
return scoped
|
||||
}
|
||||
const next = [...scoped, { key, cacheKey, ctx, used: ++used }]
|
||||
if (next.length <= CACHE_SIZE) return next
|
||||
if (next.length <= CACHE_SIZE) {
|
||||
for (const item of prev) if (!next.includes(item)) props.onEvict?.(item.cacheKey)
|
||||
return next
|
||||
}
|
||||
const oldest = next.reduce((entry, item) => (item.used < entry.used ? item : entry))
|
||||
return next.filter((item) => item !== oldest)
|
||||
const result = next.filter((item) => item !== oldest)
|
||||
props.onEvict?.(oldest.cacheKey)
|
||||
for (const item of prev) if (!result.includes(item)) props.onEvict?.(item.cacheKey)
|
||||
return result
|
||||
})
|
||||
})
|
||||
|
||||
@@ -77,7 +96,7 @@ export const DiffPanelCache: Component<Props> = (props) => {
|
||||
return (
|
||||
<div class="am-diff-panel-cache" classList={{ "am-diff-panel-cache-active": active() }} inert={!active()}>
|
||||
<DiffPanel
|
||||
diffs={props.data()[entry.key] ?? []}
|
||||
diffs={props.data()[diffDataKey(props.project(), entry.key)] ?? []}
|
||||
loading={props.loading(entry.key)}
|
||||
active={active()}
|
||||
loadingFiles={props.loadingFiles(entry.key)}
|
||||
@@ -89,8 +108,8 @@ export const DiffPanelCache: Component<Props> = (props) => {
|
||||
onDiffStyleChange={props.onDiffStyleChange}
|
||||
markdownRender={props.markdownRender}
|
||||
onMarkdownRenderChange={props.onMarkdownRenderChange}
|
||||
comments={props.comments(entry.ctx)}
|
||||
onCommentsChange={(comments) => props.setComments(entry.ctx, comments)}
|
||||
comments={props.comments(entry.key)}
|
||||
onCommentsChange={(comments) => props.setComments(entry.key, comments)}
|
||||
composer={props.composer(entry.cacheKey)}
|
||||
onSendClick={props.onSendClick}
|
||||
onClose={props.onClose}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { showToast } from "@kilocode/kilo-ui/toast"
|
||||
import { groupApplyConflicts } from "./apply-conflicts"
|
||||
import { ApplyDialog } from "./ApplyDialog"
|
||||
import { composeDiffId } from "./diff-scope-state"
|
||||
import { diffDataKey } from "./worktree-diffs"
|
||||
import type { tracker } from "./telemetry"
|
||||
import type { useDialog } from "@kilocode/kilo-ui/context/dialog"
|
||||
import type { useLanguage } from "../src/context/language"
|
||||
@@ -74,7 +75,7 @@ export function createApplyToLocal(opts: ApplyToLocalOptions) {
|
||||
const applyDiffs = createMemo(() => {
|
||||
const key = applyDiffKey()
|
||||
if (!key) return [] as WorktreeFileDiff[]
|
||||
return diffDatas()[key] ?? ([] as WorktreeFileDiff[])
|
||||
return diffDatas()[diffDataKey(opts.projectId?.(), key)] ?? ([] as WorktreeFileDiff[])
|
||||
})
|
||||
|
||||
const applyStateForTarget = createMemo(() => {
|
||||
|
||||
@@ -50,8 +50,9 @@ export function pruneReviewState<T>(
|
||||
): Record<string, T> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(values).filter(([key]) => {
|
||||
const [owner, context] = key.split(":")
|
||||
return owner !== project || context === "local" || contexts.has(context)
|
||||
const [owner, value] = key.split(":")
|
||||
const context = value?.split("#", 1)[0]
|
||||
return owner !== project || context === "local" || (context !== undefined && contexts.has(context))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,21 +21,23 @@ export function createRevertFile(
|
||||
projectId?: Accessor<string | undefined>,
|
||||
) {
|
||||
const [files, setFiles] = createSignal<Record<string, Set<string>>>({})
|
||||
const key = (project: string | undefined, scope: string) => `${project ?? "single"}\0${scope}`
|
||||
|
||||
const reverting = createMemo(() => {
|
||||
const id = diffScopeId()
|
||||
if (!id) return new Set<string>()
|
||||
return files()[id] ?? new Set<string>()
|
||||
return files()[key(projectId?.(), id)] ?? new Set<string>()
|
||||
})
|
||||
|
||||
const revertingFor = (id: string) => files()[id] ?? new Set<string>()
|
||||
const revertingFor = (id: string) => files()[key(projectId?.(), id)] ?? new Set<string>()
|
||||
|
||||
function revertFor(id: string | undefined, context: string | undefined, source: string, file: string) {
|
||||
if (!id || !context) return
|
||||
const data = key(projectId?.(), id)
|
||||
setFiles((prev) => {
|
||||
const set = new Set(prev[id] ?? [])
|
||||
const set = new Set(prev[data] ?? [])
|
||||
set.add(file)
|
||||
return { ...prev, [id]: set }
|
||||
return { ...prev, [data]: set }
|
||||
})
|
||||
vscode.postMessage({
|
||||
type: "agentManager.revertWorktreeFile",
|
||||
@@ -51,12 +53,13 @@ export function createRevertFile(
|
||||
}
|
||||
|
||||
function onResult(ev: AgentManagerRevertWorktreeFileResultMessage) {
|
||||
const data = key(ev.projectId, ev.sessionId)
|
||||
setFiles((prev) => {
|
||||
const set = new Set(prev[ev.sessionId] ?? [])
|
||||
const set = new Set(prev[data] ?? [])
|
||||
set.delete(ev.file)
|
||||
const next = { ...prev }
|
||||
if (set.size === 0) delete next[ev.sessionId]
|
||||
else next[ev.sessionId] = set
|
||||
if (set.size === 0) delete next[data]
|
||||
else next[data] = set
|
||||
return next
|
||||
})
|
||||
if (ev.status === "success") {
|
||||
|
||||
@@ -20,5 +20,23 @@ export function createReviewComposers(project: Accessor<string | undefined>) {
|
||||
}
|
||||
}
|
||||
|
||||
return { get, clear }
|
||||
const drop = (key: string) => values.delete(key)
|
||||
|
||||
const clearProject = (id: string) => {
|
||||
const prefix = `${id}\0`
|
||||
for (const key of values.keys()) {
|
||||
if (key.startsWith(prefix)) values.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
const prune = (contexts: Set<string>) => {
|
||||
const prefix = `${project() ?? "single"}\0`
|
||||
for (const key of values.keys()) {
|
||||
if (!key.startsWith(prefix)) continue
|
||||
const ctx = key.slice(prefix.length).split("#", 1)[0]
|
||||
if (ctx !== "local" && !contexts.has(ctx)) values.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
return { get, clear, drop, clearProject, prune }
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ interface DiffRequestOptions {
|
||||
|
||||
export function createDiffRequests(opts: DiffRequestOptions) {
|
||||
const requested = new Map<string, string>()
|
||||
let active = false
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
@@ -38,6 +39,15 @@ export function createDiffRequests(opts: DiffRequestOptions) {
|
||||
on(
|
||||
() => [opts.open(), opts.diffs(), opts.loading(), opts.send()] as const,
|
||||
([open, diffs]) => {
|
||||
if (!opts.send()) {
|
||||
requested.clear()
|
||||
active = false
|
||||
return
|
||||
}
|
||||
if (!active) {
|
||||
requested.clear()
|
||||
active = true
|
||||
}
|
||||
const files = new Set(open)
|
||||
for (const file of requested.keys()) {
|
||||
if (!files.has(file)) requested.delete(file)
|
||||
|
||||
Reference in New Issue
Block a user