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/vscode-first-send-agent-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Preserve the selected mode when sending the first message in a new VS Code task so the chosen model is paired with the correct agent instructions.
39 changes: 37 additions & 2 deletions packages/kilo-vscode/tests/unit/prompt-send-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,47 @@ describe("sendMessage / sendCommand draft id contract", () => {
// from ":pending:<id>" to ":session:<newSessionId>". The user loses the
// typed message and the new session starts empty.
const body = extractFunctionBody(source, "sendMessage")
expect(body).toMatch(/!sid && !draftID \? crypto\.randomUUID\(\) : draftID/)
expect(body).toContain("const fresh = !sid && !draftID")
expect(body).toMatch(/const effectiveDraftID = fresh \? crypto\.randomUUID\(\) : draftID/)
})

it("sendCommand mints a draftID when there is no current session and none was supplied", () => {
const body = extractFunctionBody(source, "sendCommand")
expect(body).toMatch(/!sid && !draftID \? crypto\.randomUUID\(\) : draftID/)
expect(body).toContain("const fresh = !sid && !draftID")
expect(body).toMatch(/const effectiveDraftID = fresh \? crypto\.randomUUID\(\) : draftID/)
})

it("sendMessage seeds the pending agent before resolving the draft-scoped agent", () => {
// Fresh draft IDs are created after ModeSwitcher stored the selected mode in
// pendingAgentSelection(). The draft scope must inherit that pending agent
// before promptAgent(scope) runs, otherwise the first send pairs the selected
// model with the default agent's system prompt.
const body = extractFunctionBody(source, "sendMessage")
expect(body).toMatch(
/if \(fresh && effectiveDraftID\) agentDrafts\.seed\(effectiveDraftID\)[\s\S]*const agent = promptAgent\(scope\)/,
)
})

it("sendCommand seeds the pending agent before resolving the draft-scoped agent", () => {
const body = extractFunctionBody(source, "sendCommand")
expect(body).toMatch(
/if \(fresh && effectiveDraftID\) agentDrafts\.seed\(effectiveDraftID\)[\s\S]*const agent = promptAgent\(scope\)/,
)
})

it("does not clear a newer pending agent when a seeded draft is promoted", () => {
const body = extractFunctionBody(source, "handleSessionCreated")
const draftBlock = body.match(/if \(draftID\) \{([\s\S]*?)\} else if/)
expect(draftBlock).not.toBeNull()
expect(draftBlock![1]).not.toContain("setPendingAgentSelection(null)")
})

it("prunes seeded draft agents only after the draft is abandoned", () => {
const failed = extractFunctionBody(source, "handleSendMessageFailed")
expect(source).toMatch(/const agentDrafts = createDraftAgentSeed/)
expect(source).toContain("active: (draft) => !!submissionMap[draft]")
expect(failed).toContain("draftSessionID() !== message.draftID")
expect(failed).toContain("agentDrafts.prune(message.draftID)")
})
})

Expand Down
86 changes: 85 additions & 1 deletion packages/kilo-vscode/tests/unit/session-agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, it, expect } from "bun:test"
import { resolveSessionAgent } from "../../webview-ui/src/context/session-agent"
import {
createDraftAgentSeed,
draftAgentSelection,
resolveSessionAgent,
} from "../../webview-ui/src/context/session-agent"
import type { Message } from "../../webview-ui/src/types/messages"

function makeMessage(overrides: Partial<Message> = {}): Message {
Expand Down Expand Up @@ -76,3 +80,83 @@ describe("resolveSessionAgent", () => {
expect(result).toBeUndefined()
})
})

describe("draftAgentSelection", () => {
it("carries a pending agent into a new draft scope", () => {
const result = draftAgentSelection({}, "draft-1", "plan")

expect(result).toBe("plan")
})

it("does not overwrite an existing draft agent", () => {
const result = draftAgentSelection({ "draft-1": "code" }, "draft-1", "plan")

expect(result).toBeUndefined()
})

it("ignores missing pending agents", () => {
const result = draftAgentSelection({}, "draft-1", null)

expect(result).toBeUndefined()
})
})

describe("createDraftAgentSeed", () => {
it("seeds and prunes abandoned draft agents", () => {
const selections: Record<string, string> = {}
const seed = createDraftAgentSeed({
selections: () => selections,
pending: () => "plan",
active: () => false,
set: (draft, agent) => {
selections[draft] = agent
},
drop: (draft) => {
delete selections[draft]
},
})

seed.seed("draft-1")
expect(selections["draft-1"]).toBe("plan")

seed.prune("draft-1")
expect(selections["draft-1"]).toBeUndefined()
})

it("keeps active drafts available for retry", () => {
const selections: Record<string, string> = {}
const seed = createDraftAgentSeed({
selections: () => selections,
pending: () => "code",
active: () => true,
set: (draft, agent) => {
selections[draft] = agent
},
drop: (draft) => {
delete selections[draft]
},
})

seed.seed("draft-1")
seed.prune("draft-1")

expect(selections["draft-1"]).toBe("code")
})

it("promotes drafts without dropping the migrated agent", () => {
const dropped: string[] = []
const seed = createDraftAgentSeed({
selections: () => ({}),
pending: () => "ask",
active: () => false,
set: () => {},
drop: (draft) => dropped.push(draft),
})

seed.seed("draft-1")
seed.promote("draft-1")
seed.prune("draft-1")

expect(dropped).toEqual([])
})
})
30 changes: 30 additions & 0 deletions packages/kilo-vscode/webview-ui/src/context/session-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,33 @@ export function resolveSessionAgent(messages: Message[], names: Set<string>): st
return name
}
}

export function draftAgentSelection(selections: Record<string, string>, draft: string, pending: string | null) {
if (selections[draft]) return undefined
return pending ?? undefined
}

export function createDraftAgentSeed(opts: {
selections: () => Record<string, string>
pending: () => string | null
active: (draft: string) => boolean
set: (draft: string, agent: string) => void
drop: (draft: string) => void
}) {
const seeded = new Set<string>()
return {
seed(draft: string) {
const agent = draftAgentSelection(opts.selections(), draft, opts.pending())
if (!agent) return
opts.set(draft, agent)
seeded.add(draft)
},
promote(draft: string) {
seeded.delete(draft)
},
prune(draft?: string) {
if (!draft || opts.active(draft) || !seeded.delete(draft)) return
opts.drop(draft)
},
}
}
27 changes: 24 additions & 3 deletions packages/kilo-vscode/webview-ui/src/context/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
import { createAbortState } from "./abort-state"
import { clearIfOn, createCloudPrune } from "./session-cloud-prune"
import { isSameSessionTree } from "./model-usage"
import { createDraftAgentSeed } from "./session-agent"

const RECENT_LIMIT = 5
const MESSAGE_PAGE_LIMIT = 80
Expand Down Expand Up @@ -555,7 +556,17 @@
if (sessionID) return store.agentSelections[sessionID] ?? defaultAgent()
return selectedAgentName()
}

const agentDrafts = createDraftAgentSeed({
selections: () => store.agentSelections,
pending: pendingAgentSelection,
active: (draft) => !!submissionMap[draft],
set: (draft, agent) => setStore("agentSelections", draft, agent),
drop: (draft) =>
setStore(
"agentSelections",
produce((agents) => void delete agents[draft]),
),
})
const agentNames = createMemo(() => new Set(agents().map((agent) => agent.name)))

const { pendingCloudPrune, prune: pruneCloudOrphans } = createCloudPrune((m) => setStore("parts", produce(m)), stash)
Expand Down Expand Up @@ -1319,6 +1330,7 @@
for (const key of sessionVariantKeys(variants, draftID)) delete variants[key]
}),
)
agentDrafts.promote(draftID)
} else if (pendingAgent && !store.agentSelections[session.id]) {
setStore("agentSelections", session.id, pendingAgent)
setPendingAgentSelection(null)
Expand Down Expand Up @@ -1835,6 +1847,7 @@
})

if (!message.sessionID && message.draftID) {
if (draftSessionID() !== message.draftID) agentDrafts.prune(message.draftID)
setDraftSessionID(message.draftID)
}
}
Expand Down Expand Up @@ -2256,8 +2269,10 @@
dismissQuestion(q.id)
}

const effectiveDraftID = !sid && !draftID ? crypto.randomUUID() : draftID
const fresh = !sid && !draftID
const effectiveDraftID = fresh ? crypto.randomUUID() : draftID
const scope = effectiveDraftID ?? sid
if (fresh && effectiveDraftID) agentDrafts.seed(effectiveDraftID)
if (scope) {
clearClose(scope)
addOptimistic(scope, messageID, text, files, review)
Expand Down Expand Up @@ -2328,8 +2343,10 @@
dismissQuestion(q.id)
}

const effectiveDraftID = !sid && !draftID ? crypto.randomUUID() : draftID
const fresh = !sid && !draftID
const effectiveDraftID = fresh ? crypto.randomUUID() : draftID
const scope = effectiveDraftID ?? sid
if (fresh && effectiveDraftID) agentDrafts.seed(effectiveDraftID)
if (scope) {
clearClose(scope)
addOptimistic(scope, messageID, `/${command} ${args}`.trim(), files)
Expand Down Expand Up @@ -2500,11 +2517,13 @@
}

// Reset agent selection to default for the new session (model overrides persist)
agentDrafts.prune(draftSessionID())
setPendingAgentSelection(defaultAgent())
vscode.postMessage({ type: "createSession" })
}

function clearCurrentSession() {
agentDrafts.prune(draftSessionID())
setUserClearedSession(true)
setCurrentSessionID(undefined)
setDraftSessionID(undefined)
Expand Down Expand Up @@ -2553,6 +2572,7 @@
// they update even while disconnected. Bailing out here when not connected
// froze the chat on the previous session while the side diff (resolved from
// the worktree selection) still moved (the reported "only the diff changes").
agentDrafts.prune(draftSessionID())
setCurrentSessionID(id)
setDraftSessionID(id)
setUserClearedSession(false)
Expand Down Expand Up @@ -2599,6 +2619,7 @@
return
}
const key = `cloud:${cloudSessionId}`
agentDrafts.prune(draftSessionID())
setCloudPreviewId(cloudSessionId)
setCurrentSessionID(key)
setDraftSessionID(key)
Expand Down Expand Up @@ -2977,4 +2998,4 @@
throw new Error("useSession must be used within a SessionProvider")
}
return context
}

Check failure on line 3001 in packages/kilo-vscode/webview-ui/src/context/session.tsx

View workflow job for this annotation

GitHub Actions / unit tests

File has too many lines (3001). Maximum allowed is 3000
Loading