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:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
|
||||
|
||||
@@ -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>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user