story-206: default to claude-code-pty on first use

Squash merge of feature/story-206

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-26 15:02:16 +00:00
parent 17fd3b2dc2
commit a2465f476a
4 changed files with 85 additions and 11 deletions

View File

@@ -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(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
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(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
// 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(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
// 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;

View File

@@ -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<string[]>([]);
const [claudeModels, setClaudeModels] = useState<string[]>([]);
@@ -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);

View File

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

View File

@@ -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 (
<CodeRefLink key={`ref-${i}`} path={part.path} line={part.line}>
<CodeRefLink
key={`ref-${part.path}:${part.line}`}
path={part.path}
line={part.line}
>
{part.value}
</CodeRefLink>
);
}
return <span key={`text-${i}`}>{part.value}</span>;
return <span key={`text-${part.value}`}>{part.value}</span>;
})}
</>
);