Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
52 changes: 50 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,52 @@ 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)
if (opened.has(key)) {
pending.delete(key)
return
}
pending.delete(key)
Comment thread
marius-kilocode marked this conversation as resolved.
Resolved
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