Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createPromptInputController, createPromptProjectControls } from "@/page
import { createPromptModelSelection } from "@/pages/session/composer/prompt-model-selection"
import { useSessionKey } from "@/pages/session/session-layout"
import { useComposerCommands } from "@/pages/session/use-composer-commands"
import { createPromptAgentSelection } from "./prompt-agent-selection"

export function createNewSessionDraftController(workspace: { worktree: () => string; resetWorktree: () => void }) {
const prompt = usePrompt()
Expand All @@ -18,14 +19,16 @@ export function createNewSessionDraftController(workspace: { worktree: () => str
const route = useSessionKey()
const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>()
const model = createPromptModelSelection({ agent: () => local.agent.current() })
const agent = createPromptAgentSelection({ agent: local.agent, model: prompt.model })

useComposerCommands({ model })
useComposerCommands({ model, agent })

const controls = createPromptInputController({
sessionKey: route.sessionKey,
sessionID: () => route.params.id,
queryOptions: serverSync().queryOptions,
model,
agent,
})
const projectControls = createPromptProjectControls()
const input = usePromptInputV2Controller({
Expand Down
30 changes: 30 additions & 0 deletions packages/app/src/pages/new-session/prompt-agent-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { batch } from "solid-js"
import type { PromptModel } from "@/context/prompt-state"

export function createPromptAgentSelection(input: {
agent: {
current(): { model?: Omit<PromptModel, "variant">; variant?: string } | undefined
set(name: string | undefined): void
move(direction: 1 | -1): void
}
model: {
current(): PromptModel | undefined
set(model: PromptModel | undefined): void
}
}) {
const update = (select: () => void) =>
batch(() => {
select()
const agent = input.agent.current()
if (!agent) return
const previous = input.model.current()
const model = agent.model ?? previous
if (!model) return
input.model.set({ ...model, variant: agent.variant ?? previous?.variant })
})

return {
set: (name: string | undefined) => update(() => input.agent.set(name)),
move: (direction: 1 | -1) => update(() => input.agent.move(direction)),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function createPromptInputController(input: {
sessionID: Accessor<string | undefined>
queryOptions: Pick<QueryOptionsApi, "agents" | "providers">
model?: ModelSelection
agent?: Pick<ReturnType<typeof useLocal>["agent"], "set">
}) {
const layout = useLayout()
const local = useLocal()
Expand All @@ -41,7 +42,7 @@ export function createPromptInputController(input: {
current: local.agent.current()?.name ?? "",
loading: agentsQuery.isLoading,
visible: local.agent.visible(),
select: local.agent.set,
select: (name) => (input.agent ?? local.agent).set(name),
},
model: {
selection: input.model ?? local.model,
Expand Down
9 changes: 6 additions & 3 deletions packages/app/src/pages/session/use-composer-commands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ const withCategory = (category: string) => {
})
}

export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
export const useComposerCommands = (
input: { model?: ModelSelection; agent?: Pick<ReturnType<typeof useLocal>["agent"], "move"> } = {},
) => {
const command = useCommand()
const dialog = useDialog()
const language = useLanguage()
const local = useLocal()
const { sessionKey } = useSessionLayout()
const sessionOwnership = createSessionOwnership(sessionKey)
const model = input.model ?? local.model
const agent = input.agent ?? local.agent
const modelCommand = withCategory(language.t("command.category.model"))
const agentCommand = withCategory(language.t("command.category.agent"))

Expand Down Expand Up @@ -69,15 +72,15 @@ export const useComposerCommands = (input: { model?: ModelSelection } = {}) => {
keybind: "mod+.",
slash: "agent",
disabled: !local.agent.visible(),
onSelect: () => local.agent.move(1),
onSelect: () => agent.move(1),
}),
agentCommand({
id: "agent.cycle.reverse",
title: language.t("command.agent.cycle.reverse"),
description: language.t("command.agent.cycle.reverse.description"),
keybind: "shift+mod+.",
disabled: !local.agent.visible(),
onSelect: () => local.agent.move(-1),
onSelect: () => agent.move(-1),
}),
])
}
66 changes: 66 additions & 0 deletions packages/app/test-browser/prompt-agent-selection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { expect, test } from "bun:test"
import { createPromptState, type PromptModel } from "@/context/prompt-state"
import { createPromptAgentSelection } from "@/pages/new-session/prompt-agent-selection"

const agents = [
{ name: "alpha", model: { providerID: "fixture", modelID: "alpha" }, variant: "low" },
{ name: "beta", model: { providerID: "fixture", modelID: "beta" }, variant: "high" },
{ name: "gamma", model: { providerID: "fixture", modelID: "beta" }, variant: "low" },
{ name: "inherited" },
]

function setup(model?: PromptModel) {
const prompt = createPromptState({ model })
let index = 0
const agent = {
current: () => agents[index],
set: (name: string | undefined) => {
index = agents.findIndex((agent) => agent.name === name)
},
move: (direction: 1 | -1) => {
index = (index + direction + agents.length) % agents.length
},
}
return { prompt, agent, select: createPromptAgentSelection({ agent, model: prompt.model }) }
}

test("agent selection replaces the model and variant inherited by a new draft", () => {
const { prompt, select } = setup({ providerID: "fixture", modelID: "alpha", variant: "low" })
select.set("beta")
expect(prompt.model.current()).toEqual({ providerID: "fixture", modelID: "beta", variant: "high" })
select.set("gamma")
expect(prompt.model.current()).toEqual({ providerID: "fixture", modelID: "beta", variant: "low" })
})

test("both agent cycle directions update draft selection", () => {
const { prompt, select } = setup()
select.move(1)
expect(prompt.model.current()).toEqual({ providerID: "fixture", modelID: "beta", variant: "high" })
select.move(-1)
expect(prompt.model.current()).toEqual({ providerID: "fixture", modelID: "alpha", variant: "low" })
})

test("agents without defaults preserve manual model and explicit default variant", () => {
const model = { providerID: "other", modelID: "manual", variant: null }
const { prompt, select } = setup(model)
select.set("inherited")
expect(prompt.model.current()).toEqual(model)
})

test("initialization and session restoration do not overwrite the draft model", () => {
const model = { providerID: "other", modelID: "manual", variant: "high" }
const { prompt, agent, select } = setup(model)
expect(prompt.model.current()).toEqual(model)
agent.set("beta")
expect(prompt.model.current()).toEqual(model)
select.set("beta")
prompt.model.set(model)
expect(prompt.model.current()).toEqual(model)
})

test("a missing agent does not clear the draft model", () => {
const model = { providerID: "fixture", modelID: "alpha", variant: "low" }
const { prompt, select } = setup(model)
select.set("missing")
expect(prompt.model.current()).toEqual(model)
})
Loading