From a2465f476aed3a625b53c20f7f424d8ca39ffc1b Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 26 Feb 2026 15:02:16 +0000 Subject: [PATCH] story-206: default to claude-code-pty on first use Squash merge of feature/story-206 Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/Chat.test.tsx | 55 ++++++++++++++++++++++++ frontend/src/components/Chat.tsx | 4 +- frontend/src/components/CodeRef.test.tsx | 17 ++++++-- frontend/src/components/CodeRef.tsx | 20 ++++++--- 4 files changed, 85 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Chat.test.tsx b/frontend/src/components/Chat.test.tsx index e1f87db..4cb53be 100644 --- a/frontend/src/components/Chat.test.tsx +++ b/frontend/src/components/Chat.test.tsx @@ -68,6 +68,61 @@ function setupMocks() { mockedApi.setAnthropicApiKey.mockResolvedValue(true); } +describe("Default provider selection (Story 206)", () => { + beforeEach(() => { + capturedWsHandlers = null; + }); + + it("AC1: defaults to claude-code-pty when no saved model preference exists", async () => { + mockedApi.getOllamaModels.mockResolvedValue([]); + mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false); + mockedApi.getAnthropicModels.mockResolvedValue([]); + mockedApi.getModelPreference.mockResolvedValue(null); + mockedApi.setModelPreference.mockResolvedValue(true); + mockedApi.cancelChat.mockResolvedValue(true); + + render(); + + await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); + + // With no models available, the header renders a text input with the model value + const input = screen.getByPlaceholderText("Model"); + expect(input).toHaveValue("claude-code-pty"); + }); + + it("AC2: claude-code-pty remains default even when ollama models are available", async () => { + mockedApi.getOllamaModels.mockResolvedValue(["llama3.1", "deepseek-coder"]); + mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false); + mockedApi.getAnthropicModels.mockResolvedValue([]); + mockedApi.getModelPreference.mockResolvedValue(null); + mockedApi.setModelPreference.mockResolvedValue(true); + mockedApi.cancelChat.mockResolvedValue(true); + + render(); + + // Wait for Ollama models to load and the select dropdown to appear + const select = await screen.findByRole("combobox"); + expect(select).toHaveValue("claude-code-pty"); + }); + + it("AC3: respects saved model preference for existing projects", async () => { + mockedApi.getOllamaModels.mockResolvedValue(["llama3.1"]); + mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false); + mockedApi.getAnthropicModels.mockResolvedValue([]); + mockedApi.getModelPreference.mockResolvedValue("llama3.1"); + mockedApi.setModelPreference.mockResolvedValue(true); + mockedApi.cancelChat.mockResolvedValue(true); + + render(); + + // Wait for models to load and preference to be applied + const select = await screen.findByRole("combobox"); + await waitFor(() => { + expect(select).toHaveValue("llama3.1"); + }); + }); +}); + describe("Chat message rendering — unified tool call UI", () => { beforeEach(() => { capturedWsHandlers = null; diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx index f07d845..eb1c38a 100644 --- a/frontend/src/components/Chat.tsx +++ b/frontend/src/components/Chat.tsx @@ -151,7 +151,7 @@ interface ChatProps { export function Chat({ projectPath, onCloseProject }: ChatProps) { const { messages, setMessages, clearMessages } = useChatHistory(projectPath); const [loading, setLoading] = useState(false); - const [model, setModel] = useState("llama3.1"); + const [model, setModel] = useState("claude-code-pty"); const [enableTools, setEnableTools] = useState(true); const [availableModels, setAvailableModels] = useState([]); const [claudeModels, setClaudeModels] = useState([]); @@ -244,8 +244,6 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) { const savedModel = await api.getModelPreference(); if (savedModel) { setModel(savedModel); - } else if (sortedModels.length > 0) { - setModel(sortedModels[0]); } } catch (e) { console.error(e); diff --git a/frontend/src/components/CodeRef.test.tsx b/frontend/src/components/CodeRef.test.tsx index d83197f..1eef7bb 100644 --- a/frontend/src/components/CodeRef.test.tsx +++ b/frontend/src/components/CodeRef.test.tsx @@ -13,20 +13,31 @@ describe("parseCodeRefs (Story 193)", () => { it("returns a single text part for plain text with no code refs", () => { const parts = parseCodeRefs("Hello world, no code here"); expect(parts).toHaveLength(1); - expect(parts[0]).toEqual({ type: "text", value: "Hello world, no code here" }); + expect(parts[0]).toEqual({ + type: "text", + value: "Hello world, no code here", + }); }); it("detects a simple code reference", () => { const parts = parseCodeRefs("src/main.rs:42"); expect(parts).toHaveLength(1); - expect(parts[0]).toMatchObject({ type: "ref", path: "src/main.rs", line: 42 }); + expect(parts[0]).toMatchObject({ + type: "ref", + path: "src/main.rs", + line: 42, + }); }); it("detects a code reference embedded in surrounding text", () => { const parts = parseCodeRefs("See src/lib.rs:100 for details"); expect(parts).toHaveLength(3); expect(parts[0]).toEqual({ type: "text", value: "See " }); - expect(parts[1]).toMatchObject({ type: "ref", path: "src/lib.rs", line: 100 }); + expect(parts[1]).toMatchObject({ + type: "ref", + path: "src/lib.rs", + line: 100, + }); expect(parts[2]).toEqual({ type: "text", value: " for details" }); }); diff --git a/frontend/src/components/CodeRef.tsx b/frontend/src/components/CodeRef.tsx index d512c08..cef5467 100644 --- a/frontend/src/components/CodeRef.tsx +++ b/frontend/src/components/CodeRef.tsx @@ -22,7 +22,8 @@ export function parseCodeRefs(text: string): CodeRefPart[] { const re = new RegExp(CODE_REF_PATTERN.source, "g"); let match: RegExpExecArray | null; - while ((match = re.exec(text)) !== null) { + match = re.exec(text); + while (match !== null) { if (match.index > lastIndex) { parts.push({ type: "text", value: text.slice(lastIndex, match.index) }); } @@ -33,6 +34,7 @@ export function parseCodeRefs(text: string): CodeRefPart[] { line: Number(match[2]), }); lastIndex = re.lastIndex; + match = re.exec(text); } if (lastIndex < text.length) { @@ -93,15 +95,23 @@ export function InlineCodeWithRefs({ text }: InlineCodeWithRefsProps) { return ( <> - {parts.map((part, i) => { - if (part.type === "ref" && part.path !== undefined && part.line !== undefined) { + {parts.map((part) => { + if ( + part.type === "ref" && + part.path !== undefined && + part.line !== undefined + ) { return ( - + {part.value} ); } - return {part.value}; + return {part.value}; })} );