mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge pull request #13430 from Kilo-Org/investigate-duplicate-sse-rendering
fix(vscode): prevent duplicate reasoning with subagent inspectors
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Prevent duplicate reasoning and response text while subagent sessions are open.
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { mergeOptimisticPart, mergeParts, sameParts } from "../../webview-ui/src/context/session-parts"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { isolate, mergeOptimisticPart, mergeParts, sameParts } from "../../webview-ui/src/context/session-parts"
|
||||
import type { Part } from "../../webview-ui/src/types/messages"
|
||||
|
||||
function text(id: string, value: string, time: { start?: number; end?: number } = {}): Part {
|
||||
@@ -20,6 +21,39 @@ function value(parts: Part[], id: string) {
|
||||
return part.text
|
||||
}
|
||||
|
||||
describe("isolate", () => {
|
||||
it("keeps shared reasoning snapshots independent across session stores", () => {
|
||||
const shared = {
|
||||
id: "r1",
|
||||
messageID: "m1",
|
||||
type: "reasoning",
|
||||
text: "Thinking",
|
||||
time: { start: 1 },
|
||||
} satisfies Part
|
||||
const [first, setFirst] = createStore({ parts: [shared].map(isolate) })
|
||||
const [second, setSecond] = createStore({ parts: [shared].map(isolate) })
|
||||
|
||||
setFirst(
|
||||
"parts",
|
||||
produce((parts) => {
|
||||
const part = parts[0]
|
||||
if (part?.type === "reasoning") part.text += " once"
|
||||
}),
|
||||
)
|
||||
setSecond(
|
||||
"parts",
|
||||
produce((parts) => {
|
||||
const part = parts[0]
|
||||
if (part?.type === "reasoning") part.text += " once"
|
||||
}),
|
||||
)
|
||||
|
||||
expect(first.parts[0]?.type === "reasoning" && first.parts[0].text).toBe("Thinking once")
|
||||
expect(second.parts[0]?.type === "reasoning" && second.parts[0].text).toBe("Thinking once")
|
||||
expect(shared.text).toBe("Thinking")
|
||||
})
|
||||
})
|
||||
|
||||
describe("mergeParts", () => {
|
||||
it("keeps a final streamed tail part created after the reconcile snapshot started", () => {
|
||||
const parts = mergeParts(
|
||||
@@ -120,6 +154,31 @@ describe("mergeOptimisticPart", () => {
|
||||
expect(result.parts.map((part) => part.id)).toEqual(["client-text", "server-file"])
|
||||
expect(result.replaced).toBe("client-file")
|
||||
})
|
||||
|
||||
it("keeps streamed deltas independent across session stores", () => {
|
||||
const shared = text("server", "start")
|
||||
const [first, setFirst] = createStore({ parts: mergeOptimisticPart([], new Set(), shared).parts })
|
||||
const [second, setSecond] = createStore({ parts: mergeOptimisticPart([], new Set(), shared).parts })
|
||||
|
||||
setFirst(
|
||||
"parts",
|
||||
produce((parts) => {
|
||||
const part = parts[0]
|
||||
if (part?.type === "text") part.text += " chunk"
|
||||
}),
|
||||
)
|
||||
setSecond(
|
||||
"parts",
|
||||
produce((parts) => {
|
||||
const part = parts[0]
|
||||
if (part?.type === "text") part.text += " chunk"
|
||||
}),
|
||||
)
|
||||
|
||||
expect(value(first.parts, "server")).toBe("start chunk")
|
||||
expect(value(second.parts, "server")).toBe("start chunk")
|
||||
expect(value([shared], "server")).toBe("start")
|
||||
})
|
||||
})
|
||||
|
||||
describe("sameParts", () => {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { Part } from "../types/messages"
|
||||
|
||||
export function isolate(part: Part): Part {
|
||||
return { ...part }
|
||||
}
|
||||
|
||||
function stream(part: Part): part is Extract<Part, { type: "text" | "reasoning" }> {
|
||||
return part.type === "text" || part.type === "reasoning"
|
||||
}
|
||||
@@ -27,10 +31,11 @@ export function sameParts(local: Part[] = [], snapshot: Part[] = []): boolean {
|
||||
|
||||
export function mergeOptimisticPart(current: Part[], ids: ReadonlySet<string>, part: Part) {
|
||||
const index = current.findIndex((item) => ids.has(item.id) && item.type === part.type)
|
||||
if (index < 0) return { parts: [...current, part] }
|
||||
const copy = isolate(part)
|
||||
if (index < 0) return { parts: [...current, copy] }
|
||||
const old = current[index]!
|
||||
const next = current.slice()
|
||||
next[index] = part
|
||||
next[index] = copy
|
||||
return { parts: next, replaced: old.id }
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ import { getAgentModel } from "./session-model-store"
|
||||
import { resolveMessagePrefs } from "./session-preferences"
|
||||
import { errorIDs, preserveSessionErrors, withoutResolvedSessionErrors } from "./session-errors"
|
||||
import { PartStash } from "./part-stash"
|
||||
import { mergeOptimisticPart, mergeParts } from "./session-parts"
|
||||
import { isolate, mergeOptimisticPart, mergeParts } from "./session-parts"
|
||||
import { mergeMessages, sameReconcileShape } from "./session-merge"
|
||||
import { state as todoState } from "./todo-revert"
|
||||
import { sessionVariantKeys, transferVariants, variantKey } from "./session-variant-store"
|
||||
@@ -1457,7 +1457,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
const cutoff = Math.max(0, messages.length - 15)
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i]!
|
||||
const parts = msg.parts ?? []
|
||||
const parts = msg.parts?.map(isolate) ?? []
|
||||
if (mode === "reconcile" && store.parts[msg.id] && !optimisticParts.has(msg.id)) {
|
||||
const merged = mergeParts(store.parts[msg.id], parts, input.since ?? Number.POSITIVE_INFINITY)
|
||||
setStore("parts", msg.id, reconcile(merged, { key: "id" }))
|
||||
@@ -1546,7 +1546,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
if (message.parts && message.parts.length > 0) {
|
||||
optimisticParts.delete(message.id)
|
||||
stash.remove(message.id)
|
||||
setStore("parts", message.id, message.parts)
|
||||
setStore("parts", message.id, message.parts.map(isolate))
|
||||
}
|
||||
rebuildToolParts(message.sessionID, store.messages[message.sessionID] ?? [])
|
||||
}
|
||||
@@ -1624,7 +1624,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
}
|
||||
} else {
|
||||
// Add new part
|
||||
list.push(part)
|
||||
list.push(isolate(part))
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -2067,7 +2067,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
setStore("messages", key, messages)
|
||||
for (const msg of messages) {
|
||||
if (msg.parts && msg.parts.length > 0) {
|
||||
setStore("parts", msg.id, msg.parts)
|
||||
setStore("parts", msg.id, msg.parts.map(isolate))
|
||||
}
|
||||
}
|
||||
rebuildToolParts(key, messages)
|
||||
|
||||
Reference in New Issue
Block a user