From 798def4c327d78580bfbe36e6a3325822b77292a Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:08:39 +0000 Subject: [PATCH 1/3] Initial plan From a80ef0d8692769d989f8a391edf48f98dccc685f Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:12:54 +0000 Subject: [PATCH 2/3] feat: add search and favorites to model selection dropdown Co-authored-by: OpenSource03 <29690431+OpenSource03@users.noreply.github.com> --- src/components/InputBar.tsx | 214 ++++++++++++++++++++++++++++-------- 1 file changed, 166 insertions(+), 48 deletions(-) diff --git a/src/components/InputBar.tsx b/src/components/InputBar.tsx index 31fb821..74aa948 100644 --- a/src/components/InputBar.tsx +++ b/src/components/InputBar.tsx @@ -20,8 +20,10 @@ import { MicOff, Paperclip, Pencil, + Search, Shield, Square, + Star, X, } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -30,12 +32,14 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, @@ -125,6 +129,79 @@ function ModelDropdown({ modelsLoading: boolean; modelsLoadingText: string; }) { + const [searchQuery, setSearchQuery] = useState(""); + const [favoriteModels, setFavoriteModels] = useState>(() => { + try { + const stored = localStorage.getItem("harnss-favorite-models"); + return stored ? new Set(JSON.parse(stored)) : new Set(); + } catch { + return new Set(); + } + }); + const [dropdownOpen, setDropdownOpen] = useState(false); + const searchInputRef = useRef(null); + + // Focus search input when dropdown opens + useEffect(() => { + if (dropdownOpen) { + // Small delay to ensure the dropdown is fully rendered + setTimeout(() => { + searchInputRef.current?.focus(); + }, 0); + } else { + setSearchQuery(""); + } + }, [dropdownOpen]); + + // Persist favorites to localStorage + useEffect(() => { + localStorage.setItem("harnss-favorite-models", JSON.stringify([...favoriteModels])); + }, [favoriteModels]); + + const toggleFavorite = useCallback((modelId: string, e: React.MouseEvent) => { + e.stopPropagation(); + setFavoriteModels((prev) => { + const next = new Set(prev); + if (next.has(modelId)) { + next.delete(modelId); + } else { + next.add(modelId); + } + return next; + }); + }, []); + + // Filter and sort models + const filteredModels = useMemo(() => { + let filtered = modelList; + + // Apply search filter + if (searchQuery.trim()) { + filtered = modelList.filter((m) => { + const labelMatch = fuzzyMatch(searchQuery, m.label); + const descMatch = m.description ? fuzzyMatch(searchQuery, m.description) : { match: false, score: 0 }; + return labelMatch.match || descMatch.match; + }).sort((a, b) => { + const aLabelMatch = fuzzyMatch(searchQuery, a.label); + const aDescMatch = a.description ? fuzzyMatch(searchQuery, a.description) : { match: false, score: 0 }; + const bLabelMatch = fuzzyMatch(searchQuery, b.label); + const bDescMatch = b.description ? fuzzyMatch(searchQuery, b.description) : { match: false, score: 0 }; + const aScore = Math.max(aLabelMatch.score, aDescMatch.score); + const bScore = Math.max(bLabelMatch.score, bDescMatch.score); + return bScore - aScore; + }); + } + + // Sort favorites to the top + return filtered.sort((a, b) => { + const aFav = favoriteModels.has(a.id); + const bFav = favoriteModels.has(b.id); + if (aFav && !bFav) return -1; + if (!aFav && bFav) return 1; + return 0; + }); + }, [modelList, searchQuery, favoriteModels]); + if (modelsLoading) { return (
@@ -137,7 +214,7 @@ function ModelDropdown({ ? activeEffort : undefined; return ( - + - - {modelList.map((m) => { - const effortOptions = effortOptionsByModel?.[m.id] ?? []; - if (effortOptions.length > 0 && onModelEffortChange) { - const isSelected = m.id === selectedModelId; + +
+ + setSearchQuery(e.target.value)} + className="h-8 pl-8 text-xs" + onKeyDown={(e) => { + // Prevent dropdown from closing on Enter + if (e.key === "Enter") { + e.preventDefault(); + } + }} + /> +
+ {favoriteModels.size > 0 && !searchQuery && Favorites} + {filteredModels.length === 0 ? ( +
+ No models found +
+ ) : ( + filteredModels.map((m) => { + const effortOptions = effortOptionsByModel?.[m.id] ?? []; + const isFavorite = favoriteModels.has(m.id); + if (effortOptions.length > 0 && onModelEffortChange) { + const isSelected = m.id === selectedModelId; + return ( + + +
+ +
+
{m.label}
+ {m.description && ( +
{m.description}
+ )} +
+
+
+ + {effortOptions.map((effort) => { + const isActive = isSelected && effort === activeEffort; + return ( + onModelEffortChange(m.id, effort)} + className={isActive ? "bg-accent" : ""} + > +
+
+ {effort} + {isActive && Current} +
+
{CLAUDE_EFFORT_DESCRIPTIONS[effort]}
+
+
+ ); + })} +
+
+ ); + } + return ( - - -
+ onModelChange(m.id)} + className={m.id === selectedModelId ? "bg-accent" : ""} + > +
+ +
{m.label}
{m.description && (
{m.description}
)}
- - - {effortOptions.map((effort) => { - const isActive = isSelected && effort === activeEffort; - return ( - onModelEffortChange(m.id, effort)} - className={isActive ? "bg-accent" : ""} - > -
-
- {effort} - {isActive && Current} -
-
{CLAUDE_EFFORT_DESCRIPTIONS[effort]}
-
-
- ); - })} -
- +
+
); - } - - return ( - onModelChange(m.id)} - className={m.id === selectedModelId ? "bg-accent" : ""} - > -
-
{m.label}
- {m.description && ( -
{m.description}
- )} -
-
- ); - })} + }) + )} ); From 42531ee1efa5a1e0dc220761fce927cb43119781 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:14:22 +0000 Subject: [PATCH 3/3] test: add tests for model favorites localStorage Co-authored-by: OpenSource03 <29690431+OpenSource03@users.noreply.github.com> --- src/components/InputBar.test.ts | 71 ++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/components/InputBar.test.ts b/src/components/InputBar.test.ts index a9574f9..835a9db 100644 --- a/src/components/InputBar.test.ts +++ b/src/components/InputBar.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; import type { SlashCommand } from "@/types"; import { LOCAL_CLEAR_COMMAND, @@ -7,6 +7,22 @@ import { isClearCommandText, } from "./InputBar"; +// Mock localStorage for Node environment +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value.toString(); + }, + clear: () => { + store = {}; + }, + }; +})(); + +global.localStorage = localStorageMock as any; + describe("InputBar slash command helpers", () => { it("always includes the local clear command first", () => { const commands: SlashCommand[] = [ @@ -47,3 +63,56 @@ describe("InputBar slash command helpers", () => { ).toBe("$fix bug"); }); }); + +describe("Model favorites localStorage", () => { + beforeEach(() => { + // Clear localStorage before each test + localStorage.clear(); + }); + + afterEach(() => { + localStorage.clear(); + }); + + it("persists favorite models to localStorage", () => { + const favorites = ["claude-opus-4-5", "claude-sonnet-4-5"]; + localStorage.setItem("harnss-favorite-models", JSON.stringify(favorites)); + + const stored = localStorage.getItem("harnss-favorite-models"); + expect(stored).toBeTruthy(); + expect(JSON.parse(stored!)).toEqual(favorites); + }); + + it("loads favorite models from localStorage", () => { + const favorites = ["claude-haiku-4"]; + localStorage.setItem("harnss-favorite-models", JSON.stringify(favorites)); + + const stored = localStorage.getItem("harnss-favorite-models"); + const parsed = stored ? new Set(JSON.parse(stored)) : new Set(); + + expect(parsed.has("claude-haiku-4")).toBe(true); + expect(parsed.size).toBe(1); + }); + + it("handles empty favorites gracefully", () => { + const stored = localStorage.getItem("harnss-favorite-models"); + expect(stored).toBeNull(); + + const parsed = stored ? new Set(JSON.parse(stored)) : new Set(); + expect(parsed.size).toBe(0); + }); + + it("handles corrupted localStorage data", () => { + localStorage.setItem("harnss-favorite-models", "invalid-json{"); + + let parsed: Set; + try { + const stored = localStorage.getItem("harnss-favorite-models"); + parsed = stored ? new Set(JSON.parse(stored)) : new Set(); + } catch { + parsed = new Set(); + } + + expect(parsed.size).toBe(0); + }); +});