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};
})}
>
);