Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/defer-inactive-plan-opens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Defer automatic plan previews until their agent session is active.
40 changes: 39 additions & 1 deletion packages/kilo-vscode/tests/unit/open-plan.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "bun:test"
import type { ExtensionMessage, Part } from "../../webview-ui/src/types/messages"
import { planOpens } from "../../webview-ui/src/utils/open-plan"
import { createPlanOpener, planOpens } from "../../webview-ui/src/utils/open-plan"

const done = (id = "part-1") =>
({
Expand Down Expand Up @@ -64,4 +64,42 @@ describe("planOpens", () => {
expect(planOpens(update(running), "session-1")).toEqual([])
expect(planOpens(update(unmarked), "session-1")).toEqual([])
})

it("defers inactive plans and replays them when the session becomes active", async () => {
let active = "session-2"
const opened: string[] = []
const opener = createPlanOpener(
() => active,
(plan) => opened.push(`${plan.sessionID}:${plan.id}`),
)
const plan = update(done("part-deferred"), "session-1")

opener.accept(plan)
expect(opened).toEqual([])

active = "session-1"
opener.flush(active)
await Promise.resolve()
expect(opened).toEqual(["session-1:part-deferred"])
})

it("requeues a plan if the active session changes before dispatch", async () => {
let active = "session-1"
const opened: string[] = []
const opener = createPlanOpener(
() => active,
(plan) => opened.push(`${plan.sessionID}:${plan.id}`),
)
const plan = update(done("part-race"), "session-1")

opener.accept(plan)
active = "session-2"
await Promise.resolve()
expect(opened).toEqual([])

active = "session-1"
opener.flush(active)
await Promise.resolve()
expect(opened).toEqual(["session-1:part-race"])
})
})
16 changes: 6 additions & 10 deletions packages/kilo-vscode/webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useWorktreeMode } from "./context/worktree-mode"
import { useDiffStyle } from "./context/diff-style"
import { dispatchAgentManagerEditPreview } from "./utils/agent-manager-events"
import { strongest } from "./utils/session-activity"
import { planOpens } from "./utils/open-plan"
import { createPlanOpener } from "./utils/open-plan"
import type { PermissionFileDiff } from "./types/messages"

// Override the upstream "task" tool renderer with the fully-expanded version
Expand All @@ -36,7 +36,6 @@ import "./styles/chat.css"

type ViewType = "newTask" | "history" | "profile" | "settings" | "subAgentViewer"
const VALID_VIEWS = new Set<string>(["newTask", "history", "profile", "settings", "subAgentViewer"])
const opened = new Set<string>()

/**
* Bridge our session store to the DataProvider's expected Data shape.
Expand Down Expand Up @@ -151,14 +150,11 @@ export const DataBridge: Component<{ children: any }> = (props) => {
vscode.postMessage({ type: "openFile", filePath, line, column, sessionID })
}

const unsubscribePlans = vscode.onMessage((message) => {
for (const plan of planOpens(message, session.currentSessionID())) {
const id = `${plan.sessionID}:${plan.id}`
if (opened.has(id)) continue
opened.add(id)
queueMicrotask(() => open(plan.path, undefined, undefined, plan.sessionID))
}
})
const opener = createPlanOpener(session.currentSessionID, (plan) =>
open(plan.path, undefined, undefined, plan.sessionID),
)
const unsubscribePlans = vscode.onMessage(opener.accept)
createEffect(() => opener.flush(session.currentSessionID()))
onCleanup(unsubscribePlans)

const openDiff = (diff: PermissionFileDiff) => {
Expand Down
49 changes: 47 additions & 2 deletions packages/kilo-vscode/webview-ui/src/utils/open-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ export type PlanOpen = {
sessionID: string
}

export function planOpens(message: ExtensionMessage, activeSessionID: string | undefined): PlanOpen[] {
const opened = new Set<string>()
const pending = new Map<string, PlanOpen>()

function read(message: ExtensionMessage): PlanOpen[] {
const updates: Update[] =
message.type === "partUpdated" ? [message] : message.type === "partsUpdated" ? message.updates : []

Expand All @@ -19,7 +22,49 @@ export function planOpens(message: ExtensionMessage, activeSessionID: string | u
if (part.type !== "tool" || part.tool !== "open_plan" || part.state.status !== "completed") return []
if (part.state.metadata?.open !== true) return []
const path = part.state.metadata.plan
if (typeof path !== "string" || !path || update.sessionID !== activeSessionID) return []
if (typeof path !== "string" || !path || !update.sessionID) return []
return [{ id: part.id, path, sessionID: update.sessionID }]
})
}

export function planOpens(message: ExtensionMessage, activeSessionID: string | undefined): PlanOpen[] {
return read(message).filter((plan) => plan.sessionID === activeSessionID)
}

/** Defer plans from inactive sessions until their session becomes active. */
export function createPlanOpener(active: () => string | undefined, open: (plan: PlanOpen) => void) {
const id = (plan: PlanOpen) => `${plan.sessionID}:${plan.id}`
const schedule = (plan: PlanOpen) => {
const key = id(plan)
pending.delete(key)
Comment thread
marius-kilocode marked this conversation as resolved.
if (opened.has(key)) return
queueMicrotask(() => {
if (active() !== plan.sessionID) {
pending.set(key, plan)
return
}
if (opened.has(key)) return
opened.add(key)
open(plan)
})
}
const accept = (message: ExtensionMessage) => {
const current = active()
for (const plan of read(message)) {
const key = id(plan)
if (opened.has(key)) continue
if (plan.sessionID !== current) {
pending.set(key, plan)
continue
}
schedule(plan)
}
}
const flush = (sessionID: string | undefined) => {
if (!sessionID) return
for (const plan of pending.values()) {
if (plan.sessionID === sessionID) schedule(plan)
}
}
return { accept, flush }
}
Loading