Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c6048dd
Redesign model picker with favorites and search
Chrono-byte Apr 18, 2026
bbf12b3
Fail test if sidebar provider button missing
Chrono-byte Apr 18, 2026
137723d
Add aria-label to Favorites button
Chrono-byte Apr 18, 2026
687470d
Remove modelOptionsByProvider prop from ModelPicker
Chrono-byte Apr 18, 2026
22da939
Use typed provider/model in settings favorites
Chrono-byte Apr 18, 2026
2a209d7
Exclude favorited models from all models list
Chrono-byte Apr 18, 2026
d1057fb
Add accessibility attributes to model picker
Chrono-byte Apr 18, 2026
55c46c1
Show favorites when provider is all or favorites
Chrono-byte Apr 18, 2026
e690645
Introduce ClientSettingsPatch and refine settings types
Chrono-byte Apr 18, 2026
13e57b1
Use provider label in ProviderModelPicker test
Chrono-byte Apr 18, 2026
05b83ad
Clear client settings localStorage in test
Chrono-byte Apr 18, 2026
3f21091
remove codex bug file.
Chrono-byte Apr 18, 2026
b6b5e51
Consolidate provider icons and options
Chrono-byte Apr 18, 2026
3f33982
Use scoped queries in ProviderModelPicker tests
Chrono-byte Apr 18, 2026
8b5abce
Fix ModelPickerSidebar to disable provider buttons when missing from …
Chrono-byte Apr 18, 2026
3952a47
Mock env runtime and reset local API in tests
Chrono-byte Apr 18, 2026
47032ad
Expand environment runtime mock in tests
Chrono-byte Apr 18, 2026
6e0297d
Add keyboard-driven model picker favorites
juliusmarminge Apr 19, 2026
5224a63
Add favorites-aware model picker combobox
juliusmarminge Apr 19, 2026
f35dcaf
Update model picker favorites and shortcut handling
juliusmarminge Apr 19, 2026
15f55e9
Mark Cursor as new in the model picker
juliusmarminge Apr 19, 2026
fc65ff8
Simplify model picker provider icon styling
juliusmarminge Apr 19, 2026
28445c5
Stabilize shortcut modifier state updates
juliusmarminge Apr 19, 2026
0a52512
Format shortcut modifier state test helper
juliusmarminge Apr 19, 2026
faf636f
Separate opencode model labels from provider names
juliusmarminge Apr 19, 2026
cc8004f
Remove obsolete .codex file
juliusmarminge Apr 19, 2026
958b79d
Move model picker open state to its own store
juliusmarminge Apr 20, 2026
d3119a9
rm react-scan
juliusmarminge Apr 20, 2026
ad3f024
Fall back to the active provider model in the picker
juliusmarminge Apr 20, 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
1 change: 1 addition & 0 deletions apps/desktop/src/clientPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const clientSettings: ClientSettings = {
confirmThreadArchive: true,
confirmThreadDelete: false,
diffWordWrap: true,
favorites: [],
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
assert.equal(defaultsByCommand.get("thread.next"), "mod+shift+]");
assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1");
assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9");
assert.equal(defaultsByCommand.get("modelPicker.toggle"), "mod+shift+m");
assert.equal(defaultsByCommand.get("modelPicker.jump.1"), "mod+1");
assert.equal(defaultsByCommand.get("modelPicker.jump.9"), "mod+9");
}),
);

Expand Down
7 changes: 7 additions & 0 deletions apps/server/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
KeybindingShortcut,
KeybindingWhenNode,
MAX_KEYBINDINGS_COUNT,
MODEL_PICKER_JUMP_KEYBINDING_COMMANDS,
MAX_WHEN_EXPRESSION_DEPTH,
ResolvedKeybindingRule,
ResolvedKeybindingsConfig,
Expand Down Expand Up @@ -65,13 +66,19 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
{ key: "mod+n", command: "chat.new", when: "!terminalFocus" },
{ key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" },
{ key: "mod+shift+n", command: "chat.newLocal", when: "!terminalFocus" },
{ key: "mod+shift+m", command: "modelPicker.toggle", when: "!terminalFocus" },
{ key: "mod+o", command: "editor.openFavorite" },
{ key: "mod+shift+[", command: "thread.previous" },
{ key: "mod+shift+]", command: "thread.next" },
...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
key: `mod+${index + 1}`,
command,
})),
...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
key: `mod+${index + 1}`,
command,
when: "modelPickerOpen",
})),
];

function normalizeKeyToken(token: string): string {
Expand Down
59 changes: 29 additions & 30 deletions apps/server/src/provider/opencodeRuntime.test.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import assert from "node:assert/strict";
import { describe, expect, it } from "vitest";

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

const childProcessMock = vi.hoisted(() => ({
execFileSync: vi.fn((command: string, args: ReadonlyArray<string>) => {
if (command === "which" && args[0] === "opencode") {
return "/opt/homebrew/bin/opencode\n";
}
return "";
}),
spawn: vi.fn(),
}));

vi.mock("node:child_process", () => childProcessMock);

describe("resolveOpenCodeBinaryPath", () => {
it("returns absolute binary paths without PATH lookup", async () => {
const { resolveOpenCodeBinaryPath } = await import("./opencodeRuntime.ts");

assert.equal(resolveOpenCodeBinaryPath("/usr/local/bin/opencode"), "/usr/local/bin/opencode");
assert.equal(childProcessMock.execFileSync.mock.calls.length, 0);
});

it("resolves command names through PATH", async () => {
const { resolveOpenCodeBinaryPath } = await import("./opencodeRuntime.ts");
describe("flattenOpenCodeModels", () => {
it("keeps the canonical model name separate from the subprovider label", () => {
const models = flattenOpenCodeModels({
providerList: {
connected: ["github-copilot"],
all: [
{
id: "github-copilot",
name: "GitHub Copilot",
models: {
"claude-opus-4.5": {
id: "claude-opus-4.5",
name: "Claude Opus 4.5",
variants: {},
},
},
},
],
},
agents: [],
} as unknown as Parameters<typeof flattenOpenCodeModels>[0]);

assert.equal(resolveOpenCodeBinaryPath("opencode"), "/opt/homebrew/bin/opencode");
assert.deepEqual(childProcessMock.execFileSync.mock.calls[0], [
"which",
["opencode"],
expect(models).toEqual([
{
encoding: "utf8",
timeout: 3_000,
slug: "github-copilot/claude-opus-4.5",
name: "Claude Opus 4.5",
subProvider: "GitHub Copilot",
isCustom: false,
capabilities: DEFAULT_OPENCODE_MODEL_CAPABILITIES,
},
]);
});
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/opencodeRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,8 @@ export function flattenOpenCodeModels(
for (const model of Object.values(provider.models)) {
models.push({
slug: toOpenCodeModelSlug(provider.id, model.id),
name: `${provider.name} · ${model.name}`,
name: model.name,
subProvider: provider.name,
isCustom: false,
capabilities: openCodeCapabilitiesForModel({
providerID: provider.id,
Expand Down
32 changes: 29 additions & 3 deletions apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,49 @@ import { useNavigate } from "@tanstack/react-router";

import ThreadSidebar from "./Sidebar";
import { Sidebar, SidebarProvider, SidebarRail } from "./ui/sidebar";
import {
clearShortcutModifierState,
syncShortcutModifierStateFromKeyboardEvent,
} from "../shortcutModifierState";

const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width";
const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;
const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16;

export function AppSidebarLayout({ children }: { children: ReactNode }) {
const navigate = useNavigate();

useEffect(() => {
const onWindowKeyDown = (event: KeyboardEvent) => {
syncShortcutModifierStateFromKeyboardEvent(event);
};
const onWindowKeyUp = (event: KeyboardEvent) => {
syncShortcutModifierStateFromKeyboardEvent(event);
};
const onWindowBlur = () => {
clearShortcutModifierState();
};

window.addEventListener("keydown", onWindowKeyDown, true);
window.addEventListener("keyup", onWindowKeyUp, true);
window.addEventListener("blur", onWindowBlur);

return () => {
window.removeEventListener("keydown", onWindowKeyDown, true);
window.removeEventListener("keyup", onWindowKeyUp, true);
window.removeEventListener("blur", onWindowBlur);
};
}, []);

useEffect(() => {
const onMenuAction = window.desktopBridge?.onMenuAction;
if (typeof onMenuAction !== "function") {
return;
}

const unsubscribe = onMenuAction((action) => {
if (action !== "open-settings") return;
void navigate({ to: "/settings" });
if (action === "open-settings") {
void navigate({ to: "/settings" });
}
});

return () => {
Expand Down
Loading
Loading