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);
|
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", () => {
|
describe("Chat message rendering — unified tool call UI", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
capturedWsHandlers = null;
|
capturedWsHandlers = null;
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ interface ChatProps {
|
|||||||
export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||||
const { messages, setMessages, clearMessages } = useChatHistory(projectPath);
|
const { messages, setMessages, clearMessages } = useChatHistory(projectPath);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [model, setModel] = useState("llama3.1");
|
const [model, setModel] = useState("claude-code-pty");
|
||||||
const [enableTools, setEnableTools] = useState(true);
|
const [enableTools, setEnableTools] = useState(true);
|
||||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||||
const [claudeModels, setClaudeModels] = useState<string[]>([]);
|
const [claudeModels, setClaudeModels] = useState<string[]>([]);
|
||||||
@@ -244,8 +244,6 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
const savedModel = await api.getModelPreference();
|
const savedModel = await api.getModelPreference();
|
||||||
if (savedModel) {
|
if (savedModel) {
|
||||||
setModel(savedModel);
|
setModel(savedModel);
|
||||||
} else if (sortedModels.length > 0) {
|
|
||||||
setModel(sortedModels[0]);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(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", () => {
|
it("returns a single text part for plain text with no code refs", () => {
|
||||||
const parts = parseCodeRefs("Hello world, no code here");
|
const parts = parseCodeRefs("Hello world, no code here");
|
||||||
expect(parts).toHaveLength(1);
|
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", () => {
|
it("detects a simple code reference", () => {
|
||||||
const parts = parseCodeRefs("src/main.rs:42");
|
const parts = parseCodeRefs("src/main.rs:42");
|
||||||
expect(parts).toHaveLength(1);
|
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", () => {
|
it("detects a code reference embedded in surrounding text", () => {
|
||||||
const parts = parseCodeRefs("See src/lib.rs:100 for details");
|
const parts = parseCodeRefs("See src/lib.rs:100 for details");
|
||||||
expect(parts).toHaveLength(3);
|
expect(parts).toHaveLength(3);
|
||||||
expect(parts[0]).toEqual({ type: "text", value: "See " });
|
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" });
|
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");
|
const re = new RegExp(CODE_REF_PATTERN.source, "g");
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
while ((match = re.exec(text)) !== null) {
|
match = re.exec(text);
|
||||||
|
while (match !== null) {
|
||||||
if (match.index > lastIndex) {
|
if (match.index > lastIndex) {
|
||||||
parts.push({ type: "text", value: text.slice(lastIndex, match.index) });
|
parts.push({ type: "text", value: text.slice(lastIndex, match.index) });
|
||||||
}
|
}
|
||||||
@@ -33,6 +34,7 @@ export function parseCodeRefs(text: string): CodeRefPart[] {
|
|||||||
line: Number(match[2]),
|
line: Number(match[2]),
|
||||||
});
|
});
|
||||||
lastIndex = re.lastIndex;
|
lastIndex = re.lastIndex;
|
||||||
|
match = re.exec(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex < text.length) {
|
if (lastIndex < text.length) {
|
||||||
@@ -93,15 +95,23 @@ export function InlineCodeWithRefs({ text }: InlineCodeWithRefsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{parts.map((part, i) => {
|
{parts.map((part) => {
|
||||||
if (part.type === "ref" && part.path !== undefined && part.line !== undefined) {
|
if (
|
||||||
|
part.type === "ref" &&
|
||||||
|
part.path !== undefined &&
|
||||||
|
part.line !== undefined
|
||||||
|
) {
|
||||||
return (
|
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}
|
{part.value}
|
||||||
</CodeRefLink>
|
</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