Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
005b74a
feat(cli): add interactive model catalog for /model command
Jan 9, 2026
17a0fcd
Add changeset for interactive model catalog
Jan 9, 2026
c3a9c83
feat(model-catalog): implement model selection and update provider logic
Jan 9, 2026
53999bf
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 10, 2026
866b6e0
Fix tests failing
Githubguy132010 Jan 11, 2026
b00a86a
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 11, 2026
5cc7ab0
feat(model-catalog): fix keyboard navigation and update search instru…
Githubguy132010 Jan 11, 2026
d7f4ff0
properly fix all issues
Githubguy132010 Jan 11, 2026
640de46
Fix "Search:" duplication
Githubguy132010 Jan 11, 2026
df5dc81
feat(model-catalog): update labels for sorting and filtering, enhance…
Githubguy132010 Jan 11, 2026
b580cfd
fix(model-catalog): remove terminal refresh calls to prevent renderin…
Jan 11, 2026
8163d73
fix(model-catalog): remove unused terminal refresh import
Jan 11, 2026
5bb2933
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 12, 2026
7f9b71a
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 12, 2026
1711947
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 13, 2026
15d0462
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 13, 2026
ae61d60
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 13, 2026
a5d0fad
Adress review comments
Jan 13, 2026
a394c65
Make Kilo code default starting provider
Jan 13, 2026
b2f3929
Add unit tests
Jan 13, 2026
685fbe9
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 13, 2026
a55c840
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 13, 2026
6f5cca7
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 13, 2026
68bbd51
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 14, 2026
45f511f
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 16, 2026
d4ec417
Merge branch 'main' into feature/interactive-model-catalog
Githubguy132010 Jan 17, 2026
312279b
refactor(cli): replace pagination with continuous scrolling in model …
Githubguy132010 Jan 18, 2026
efc9bfb
refactor(cli): remove pagination atoms from imports
Githubguy132010 Jan 18, 2026
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/interactive-model-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": minor
---

Add interactive model catalog for /model command
1 change: 1 addition & 0 deletions cli/src/commands/__tests__/helpers/mockContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function createMockContext(overrides: Partial<CommandContext> = {}): Comm
updateModelListFilters: vi.fn(),
changeModelListPage: vi.fn(),
resetModelListState: vi.fn(),
openModelCatalog: vi.fn().mockResolvedValue(undefined),
}

return {
Expand Down
99 changes: 48 additions & 51 deletions cli/src/commands/__tests__/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,10 @@ describe("/model command", () => {
})

describe("Show current model (no args)", () => {
it("should display current model information", async () => {
it("should open the model catalog", async () => {
await modelCommand.handler(mockContext)

expect(addMessageMock).toHaveBeenCalledTimes(1)
const message = addMessageMock.mock.calls[0][0]
expect(message.type).toBe("system")
expect(message.content).toContain("Current Configuration")
expect(message.content).toContain("gpt-4")
expect(message.content).toContain("Openrouter")
expect(mockContext.openModelCatalog).toHaveBeenCalledTimes(1)
})

it("should show error when no provider configured", async () => {
Expand All @@ -151,23 +146,6 @@ describe("/model command", () => {
expect(message.type).toBe("error")
expect(message.content).toContain("No provider configured")
})

it("should display model capabilities", async () => {
await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
expect(message.content).toContain("Context Window")
expect(message.content).toContain("8K tokens")
})

it("should show available commands", async () => {
await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
expect(message.content).toContain("/model info")
expect(message.content).toContain("/model select")
expect(message.content).toContain("/model list")
})
})

describe("Model info subcommand", () => {
Expand Down Expand Up @@ -242,8 +220,8 @@ describe("/model command", () => {
it("should display success message", async () => {
await modelCommand.handler(mockContext)

expect(addMessageMock).toHaveBeenCalledTimes(1)
const message = addMessageMock.mock.calls[0][0]
expect(addMessageMock).toHaveBeenCalledTimes(2)
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("system")
expect(message.content).toContain("Switched to")
expect(message.content).toContain("gpt-3.5-turbo")
Expand All @@ -254,7 +232,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("not found")
expect(updateProviderModelMock).not.toHaveBeenCalled()
Expand All @@ -265,7 +244,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("Usage: /model select")
})
Expand All @@ -275,7 +255,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the success message, third is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("Failed to switch model")
expect(message.content).toContain("Update failed")
Expand All @@ -286,7 +267,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("No provider configured")
})
Expand All @@ -300,8 +282,8 @@ describe("/model command", () => {
it("should list all available models", async () => {
await modelCommand.handler(mockContext)

expect(addMessageMock).toHaveBeenCalledTimes(1)
const message = addMessageMock.mock.calls[0][0]
expect(addMessageMock).toHaveBeenCalledTimes(2)
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("system")
expect(message.content).toContain("Available Models")
expect(message.content).toContain("gpt-4")
Expand All @@ -311,15 +293,15 @@ describe("/model command", () => {
it("should mark current model", async () => {
await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
const message = addMessageMock.mock.calls[1][0]
expect(message.content).toContain("gpt-4")
expect(message.content).toContain("(current)")
})

it("should mark preferred models with star", async () => {
await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
const message = addMessageMock.mock.calls[1][0]
expect(message.content).toContain("⭐")
})

Expand All @@ -336,7 +318,7 @@ describe("/model command", () => {
// Verify the filter was persisted
expect(updateFiltersMock).toHaveBeenCalledWith({ search: "gpt-4" })

const message = addMessageMock.mock.calls[0][0]
const message = addMessageMock.mock.calls[1][0]
expect(message.content).toContain('Search: "gpt-4"')
expect(message.content).toContain("gpt-4")
expect(message.content).not.toContain("gpt-3.5-turbo")
Expand All @@ -355,15 +337,19 @@ describe("/model command", () => {
// Verify the filter was persisted
expect(updateFiltersMock).toHaveBeenCalledWith({ search: "nonexistent" })

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the actual response
expect(addMessageMock).toHaveBeenCalledTimes(2)
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("system")
expect(message.content).toContain("No models found")
})

it("should display model count with pagination", async () => {
await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the actual response
expect(addMessageMock).toHaveBeenCalledTimes(2)
const message = addMessageMock.mock.calls[1][0]
expect(message.content).toContain("Showing 1-2 of 2")
})

Expand All @@ -372,7 +358,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("No provider configured")
})
Expand Down Expand Up @@ -413,9 +400,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

expect(addMessageMock).toHaveBeenCalledTimes(1)
const message = addMessageMock.mock.calls[0][0]
expect(message.content).toContain("Anthropic")
// No args opens the catalog
expect(mockContext.openModelCatalog).toHaveBeenCalledTimes(1)
})

it("should handle empty router models", async () => {
Expand All @@ -437,7 +423,9 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the actual response
expect(addMessageMock).toHaveBeenCalledTimes(2)
const message = addMessageMock.mock.calls[1][0]
expect(message.content).toContain("No models available")
})

Expand All @@ -451,7 +439,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

expect(addMessageMock).toHaveBeenCalledTimes(1)
// No args opens the catalog
expect(mockContext.openModelCatalog).toHaveBeenCalled()
}
})
})
Expand All @@ -468,7 +457,8 @@ describe("/model command", () => {
it("should paginate results with 10 items per page", async () => {
await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the actual response
const message = addMessageMock.mock.calls[1][0]
expect(message.content).toContain("Showing 1-10 of 25")
expect(message.content).toContain("Page 1/3")
})
Expand Down Expand Up @@ -505,7 +495,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("system")
expect(message.content).toContain("Already on the first page")
})
Expand All @@ -516,7 +507,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("system")
expect(message.content).toContain("Already on the last page")
})
Expand All @@ -526,7 +518,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("Invalid page number")
})
Expand All @@ -536,7 +529,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("Must be between 1 and")
})
Expand Down Expand Up @@ -576,7 +570,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("Invalid sort option")
})
Expand All @@ -586,7 +581,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("Usage: /model list sort")
})
Expand Down Expand Up @@ -650,7 +646,8 @@ describe("/model command", () => {

await modelCommand.handler(mockContext)

const message = addMessageMock.mock.calls[0][0]
// First message is deprecation warning, second is the error
const message = addMessageMock.mock.calls[1][0]
expect(message.type).toBe("error")
expect(message.content).toContain("Invalid filter option")
})
Expand Down
2 changes: 2 additions & 0 deletions cli/src/commands/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export interface CommandContext {
updateModelListFilters: (filters: Partial<ModelListFilters>) => void
changeModelListPage: (pageIndex: number) => void
resetModelListState: () => void
// Model catalog context
openModelCatalog: () => Promise<void>
}

export type CommandHandler = (context: CommandContext) => Promise<void> | void
Expand Down
29 changes: 27 additions & 2 deletions cli/src/commands/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,9 +962,22 @@ export const modelCommand: Command = {
handler: async (context) => {
const { args } = context

// No arguments - show current model
// No arguments - open the model catalog
if (args.length === 0) {
await showCurrentModel(context)
// If no provider, show error
if (!context.currentProvider) {
context.addMessage({
id: Date.now().toString(),
type: "error",
content: "No provider configured. Please configure a provider first.",
ts: Date.now(),
})
return
}
// Ensure router models are loaded first
const ready = await ensureRouterModels(context)
if (!ready) return
await context.openModelCatalog()
return
}

Expand All @@ -990,6 +1003,12 @@ export const modelCommand: Command = {
break

case "select":
context.addMessage({
id: Date.now().toString(),
type: "system",
content: "⚠️ /model select is deprecated. Use /model to open the interactive catalog.",
ts: Date.now(),
})
if (args.length < 2 || !args[1]) {
context.addMessage({
id: Date.now().toString(),
Expand All @@ -1003,6 +1022,12 @@ export const modelCommand: Command = {
break

case "list": {
context.addMessage({
id: Date.now().toString(),
type: "system",
content: "⚠️ /model list is deprecated. Use /model to open the interactive catalog.",
ts: Date.now(),
})
// Check for list subcommands
const listSubcommand = args[1]?.toLowerCase()

Expand Down
Loading