Skip to content

Commit 7176fd2

Browse files
Chrono-bytejuliusmarminge
authored andcommitted
Redesign model picker with favorites and search (pingdotgg#2153)
Co-authored-by: Julius Marminge <julius0216@outlook.com>
1 parent a213e51 commit 7176fd2

39 files changed

Lines changed: 2821 additions & 516 deletions

‎apps/desktop/src/clientPersistence.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const clientSettings: ClientSettings = {
5252
confirmThreadArchive: true,
5353
confirmThreadDelete: false,
5454
diffWordWrap: true,
55+
favorites: [],
5556
sidebarProjectGroupingMode: "repository_path",
5657
sidebarProjectGroupingOverrides: {
5758
"environment-1:/tmp/project-a": "separate",

‎apps/server/src/keybindings.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
207207
assert.equal(defaultsByCommand.get("thread.next"), "mod+shift+]");
208208
assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1");
209209
assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9");
210+
assert.equal(defaultsByCommand.get("modelPicker.toggle"), "mod+shift+m");
211+
assert.equal(defaultsByCommand.get("modelPicker.jump.1"), "mod+1");
212+
assert.equal(defaultsByCommand.get("modelPicker.jump.9"), "mod+9");
210213
}),
211214
);
212215

‎apps/server/src/keybindings.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
KeybindingShortcut,
1414
KeybindingWhenNode,
1515
MAX_KEYBINDINGS_COUNT,
16+
MODEL_PICKER_JUMP_KEYBINDING_COMMANDS,
1617
MAX_WHEN_EXPRESSION_DEPTH,
1718
ResolvedKeybindingRule,
1819
ResolvedKeybindingsConfig,
@@ -80,13 +81,19 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
8081
{ key: "mod+n", command: "chat.new", when: "!terminalFocus" },
8182
{ key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" },
8283
{ key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" },
84+
{ key: "mod+shift+m", command: "modelPicker.toggle", when: "!terminalFocus" },
8385
{ key: "mod+o", command: "editor.openFavorite" },
8486
{ key: "mod+shift+[", command: "thread.previous" },
8587
{ key: "mod+shift+]", command: "thread.next" },
8688
...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
8789
key: `mod+${index + 1}`,
8890
command,
8991
})),
92+
...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
93+
key: `mod+${index + 1}`,
94+
command,
95+
when: "modelPickerOpen",
96+
})),
9097
];
9198

9299
function normalizeKeyToken(token: string): string {

‎apps/server/src/provider/opencodeRuntime.test.ts‎

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,36 @@
1-
import assert from "node:assert/strict";
1+
import { describe, expect, it } from "vitest";
22

3-
import { describe, it, vi } from "vitest";
3+
import { DEFAULT_OPENCODE_MODEL_CAPABILITIES, flattenOpenCodeModels } from "./opencodeRuntime.ts";
44

5-
const childProcessMock = vi.hoisted(() => ({
6-
execFileSync: vi.fn((command: string, args: ReadonlyArray<string>) => {
7-
if (command === "which" && args[0] === "opencode") {
8-
return "/opt/homebrew/bin/opencode\n";
9-
}
10-
return "";
11-
}),
12-
spawn: vi.fn(),
13-
}));
14-
15-
vi.mock("node:child_process", () => childProcessMock);
16-
17-
describe("resolveOpenCodeBinaryPath", () => {
18-
it("returns absolute binary paths without PATH lookup", async () => {
19-
const { resolveOpenCodeBinaryPath } = await import("./opencodeRuntime.ts");
20-
21-
assert.equal(resolveOpenCodeBinaryPath("/usr/local/bin/opencode"), "/usr/local/bin/opencode");
22-
assert.equal(childProcessMock.execFileSync.mock.calls.length, 0);
23-
});
24-
25-
it("resolves command names through PATH", async () => {
26-
const { resolveOpenCodeBinaryPath } = await import("./opencodeRuntime.ts");
5+
describe("flattenOpenCodeModels", () => {
6+
it("keeps the canonical model name separate from the subprovider label", () => {
7+
const models = flattenOpenCodeModels({
8+
providerList: {
9+
connected: ["github-copilot"],
10+
all: [
11+
{
12+
id: "github-copilot",
13+
name: "GitHub Copilot",
14+
models: {
15+
"claude-opus-4.5": {
16+
id: "claude-opus-4.5",
17+
name: "Claude Opus 4.5",
18+
variants: {},
19+
},
20+
},
21+
},
22+
],
23+
},
24+
agents: [],
25+
} as unknown as Parameters<typeof flattenOpenCodeModels>[0]);
2726

28-
assert.equal(resolveOpenCodeBinaryPath("opencode"), "/opt/homebrew/bin/opencode");
29-
assert.deepEqual(childProcessMock.execFileSync.mock.calls[0], [
30-
"which",
31-
["opencode"],
27+
expect(models).toEqual([
3228
{
33-
encoding: "utf8",
34-
timeout: 3_000,
29+
slug: "github-copilot/claude-opus-4.5",
30+
name: "Claude Opus 4.5",
31+
subProvider: "GitHub Copilot",
32+
isCustom: false,
33+
capabilities: DEFAULT_OPENCODE_MODEL_CAPABILITIES,
3534
},
3635
]);
3736
});

‎apps/server/src/provider/opencodeRuntime.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,8 @@ export function flattenOpenCodeModels(
586586
for (const model of Object.values(provider.models)) {
587587
models.push({
588588
slug: toOpenCodeModelSlug(provider.id, model.id),
589-
name: `${provider.name} · ${model.name}`,
589+
name: model.name,
590+
subProvider: provider.name,
590591
isCustom: false,
591592
capabilities: openCodeCapabilitiesForModel({
592593
providerID: provider.id,

‎apps/web/src/components/AppSidebarLayout.tsx‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,49 @@ import { useNavigate } from "@tanstack/react-router";
33

44
import ThreadSidebar from "./Sidebar";
55
import { Sidebar, SidebarProvider, SidebarRail } from "./ui/sidebar";
6+
import {
7+
clearShortcutModifierState,
8+
syncShortcutModifierStateFromKeyboardEvent,
9+
} from "../shortcutModifierState";
610

711
const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width";
812
const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;
913
const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16;
10-
1114
export function AppSidebarLayout({ children }: { children: ReactNode }) {
1215
const navigate = useNavigate();
1316

17+
useEffect(() => {
18+
const onWindowKeyDown = (event: KeyboardEvent) => {
19+
syncShortcutModifierStateFromKeyboardEvent(event);
20+
};
21+
const onWindowKeyUp = (event: KeyboardEvent) => {
22+
syncShortcutModifierStateFromKeyboardEvent(event);
23+
};
24+
const onWindowBlur = () => {
25+
clearShortcutModifierState();
26+
};
27+
28+
window.addEventListener("keydown", onWindowKeyDown, true);
29+
window.addEventListener("keyup", onWindowKeyUp, true);
30+
window.addEventListener("blur", onWindowBlur);
31+
32+
return () => {
33+
window.removeEventListener("keydown", onWindowKeyDown, true);
34+
window.removeEventListener("keyup", onWindowKeyUp, true);
35+
window.removeEventListener("blur", onWindowBlur);
36+
};
37+
}, []);
38+
1439
useEffect(() => {
1540
const onMenuAction = window.desktopBridge?.onMenuAction;
1641
if (typeof onMenuAction !== "function") {
1742
return;
1843
}
1944

2045
const unsubscribe = onMenuAction((action) => {
21-
if (action !== "open-settings") return;
22-
void navigate({ to: "/settings" });
46+
if (action === "open-settings") {
47+
void navigate({ to: "/settings" });
48+
}
2349
});
2450

2551
return () => {

0 commit comments

Comments
 (0)