From 20ae666e8110d8957781a6710faa8e92d43b3ce9 Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Wed, 30 Sep 2026 15:52:02 -0400 Subject: [PATCH 01/10] Save and test summaries for Gemini, OpenAI and Anthropic, with an editable prompt Switching providers clears a key saved for the previous one, credentials are checked against the chosen provider, and the default prompt and its source link come from diffr's schema. Agent-Session: 459ae7ec-7302-42c5-b473-4e315a7e4e19 Agent-Session: 203ee435-a867-494c-bb56-ddde9edf962e Agent-Session: ff765ffc-78bb-499b-9e86-9f7a942edbf0 Agent-Session: 78694b58-9718-40bf-a6b8-661e21afe339 Agent-Session: a9cd3da4-c06c-4d2b-b8e8-e71439cee705 Agent-Session: 0e24d827-3a5e-4ed7-aa02-bbda8d39a5e0 Agent-Session: 01a0f3f9-35d2-7bd1-a229-ec501a87447e --- packages/review-protocol/src/contracts.ts | 11 ++ .../review/src/server/diffr-config.test.ts | 118 +++++++++++++++++- packages/review/src/server/diffr-config.ts | 115 ++++++++++++++--- 3 files changed, 224 insertions(+), 20 deletions(-) diff --git a/packages/review-protocol/src/contracts.ts b/packages/review-protocol/src/contracts.ts index a6c88e9b3..392bc099f 100644 --- a/packages/review-protocol/src/contracts.ts +++ b/packages/review-protocol/src/contracts.ts @@ -461,13 +461,22 @@ export interface ReviewCanvasTutorialBridge { export interface ReviewDiffrConfig { values: JsonObject; credentialSource: "config" | "environment" | "missing"; + defaultPrompt?: string; + defaultPromptUrl?: string; changed?: boolean; error?: string; } +export const reviewDiffrProviders = ["gemini", "openai", "anthropic"] as const; + +export type ReviewDiffrProvider = (typeof reviewDiffrProviders)[number]; + export const reviewDiffrSummarizerInputSchema = z.object({ enabled: z.boolean(), + provider: z.enum(reviewDiffrProviders), model: z.string().trim().min(1), + endpoint: z.string().trim(), + systemPrompt: z.string().trim().min(1), tests: z.boolean(), apiKey: z.string().optional(), }); @@ -479,6 +488,8 @@ export type ReviewDiffrSummarizerInput = z.infer< const reviewDiffrConfigSchema = z.object({ values: z.custom(isJsonObject), credentialSource: z.enum(["config", "environment", "missing"]), + defaultPrompt: z.string().optional(), + defaultPromptUrl: z.string().optional(), changed: z.boolean().optional(), error: z.string().optional(), }); diff --git a/packages/review/src/server/diffr-config.test.ts b/packages/review/src/server/diffr-config.test.ts index e7b4133ee..797563b89 100644 --- a/packages/review/src/server/diffr-config.test.ts +++ b/packages/review/src/server/diffr-config.test.ts @@ -17,6 +17,8 @@ const roots: string[] = []; beforeEach(() => { vi.stubEnv("GEMINI_API_KEY", ""); vi.stubEnv("GOOGLE_API_KEY", ""); + vi.stubEnv("OPENAI_API_KEY", ""); + vi.stubEnv("ANTHROPIC_API_KEY", ""); vi.clearAllMocks(); }); @@ -27,7 +29,19 @@ afterEach(async () => { ); }); -const draft = { enabled: true, model: "test-model", tests: true }; +const DEFAULT_PROMPT = "Summarize each fold."; + +const PROMPT_URL = + "https://github.com/devdotfast/diffr/blob/main/plugins/summarize/plugin.toml#L68-L74"; + +const draft = { + enabled: true, + provider: "gemini" as const, + model: "test-model", + endpoint: "", + systemPrompt: DEFAULT_PROMPT, + tests: true, +}; async function fakeDiffr(key = "") { const root = await mkdtemp(path.join(tmpdir(), "review-diffr-config-")); @@ -46,6 +60,7 @@ async function fakeDiffr(key = "") { enabled: false, model: "old", tests: false, + system_prompt: DEFAULT_PROMPT, api_key: key, }, context: { lines: 3, enabled: true }, @@ -61,7 +76,10 @@ const args = process.argv.slice(2); const state = ${JSON.stringify(state)}, log = ${JSON.stringify(log)}; fs.appendFileSync(log, JSON.stringify(args) + '\\n'); const config = JSON.parse(fs.readFileSync(state, 'utf8')); -if (args[0] === 'config' && args[1] === 'show') { +if (args[0] === 'config' && args[1] === 'schema') { + const summarize = { type: 'object', properties: { system_prompt: { type: 'string', default: ${JSON.stringify(DEFAULT_PROMPT)}, description: ${JSON.stringify(`Unset uses the default: ${PROMPT_URL}`)} } } }; + console.log(JSON.stringify({ properties: { plugins: { properties: { bundled: { properties: { summarize } } } } } })); +} else if (args[0] === 'config' && args[1] === 'show') { if (!args.includes('--reveal') && config.plugins.bundled.summarize.api_key) config.plugins.bundled.summarize.api_key = ''; console.log(JSON.stringify(config)); } else if (args[0] === 'config' && args[1] === 'set') { @@ -79,6 +97,7 @@ if (args[0] === 'config' && args[1] === 'show') { fs.writeFileSync(${JSON.stringify(path.join(root, "test-config"))}, fs.readFileSync(temporary)); fs.writeFileSync(${JSON.stringify(path.join(root, "test-path"))}, temporary); const mode = process.env.TEST_MODE; + fs.writeFileSync(${JSON.stringify(path.join(root, "test-env"))}, JSON.stringify({ gemini: process.env.GEMINI_API_KEY, openai: process.env.OPENAI_API_KEY })); if (mode === 'reject') { console.error(process.env.GEMINI_API_KEY); process.exit(2); } if (mode === 'hang') { setTimeout(() => {}, 10000); } else { @@ -137,7 +156,7 @@ test("saves credentials and options before enabling and preserves blank keys", a await saveDiffrSummarizer({ ...draft, apiKey: "test-secret" }); const writes = (await fake.calls()).filter((args) => args[1] === "set"); expect(writes.map((args) => args[2])).toEqual( - ["api_key", "model", "tests", "enabled"].map( + ["api_key", "provider", "model", "tests", "enabled"].map( (key) => `plugins.bundled.summarize.${key}`, ), ); @@ -296,3 +315,96 @@ test("saved changes invalidate cached comparisons while no-op saves reuse them", cache.close(); } }); + +test("reads the default prompt and its source link from diffr's schema", async () => { + await fakeDiffr(); + expect(await readDiffrConfig()).toMatchObject({ + defaultPrompt: DEFAULT_PROMPT, + defaultPromptUrl: PROMPT_URL, + }); +}); + +test("saves a changed prompt and leaves an unchanged one alone", async () => { + const fake = await fakeDiffr("test-secret"); + await saveDiffrSummarizer({ ...draft, enabled: false }); + expect( + (await fake.calls()).some((args) => args[2]?.endsWith(".system_prompt")), + ).toBe(false); + await saveDiffrSummarizer({ + ...draft, + enabled: false, + systemPrompt: "Be terse.", + }); + expect( + JSON.parse(await readFile(fake.state, "utf8")).plugins.bundled.summarize + .system_prompt, + ).toBe("Be terse."); +}); + +test("switching providers clears the saved key unless a new one is entered", async () => { + const fake = await fakeDiffr("gemini-secret"); + const state = async () => + JSON.parse(await readFile(fake.state, "utf8")).plugins.bundled.summarize; + await saveDiffrSummarizer({ ...draft, enabled: false, provider: "anthropic" }); + expect(await state()).toMatchObject({ provider: "anthropic", api_key: "" }); + await saveDiffrSummarizer({ + ...draft, + enabled: false, + provider: "openai", + apiKey: "openai-secret", + }); + expect(await state()).toMatchObject({ + provider: "openai", + api_key: "openai-secret", + }); +}); + +test("credentials are checked against the draft provider", async () => { + await fakeDiffr("gemini-secret"); + expect( + await saveDiffrSummarizer({ ...draft, provider: "anthropic" }), + ).toMatchObject({ changed: false, error: expect.stringContaining("API key") }); + vi.stubEnv("ANTHROPIC_API_KEY", "env-secret"); + expect( + (await saveDiffrSummarizer({ ...draft, provider: "anthropic" })).error, + ).toBeUndefined(); + expect( + ( + await saveDiffrSummarizer({ + ...draft, + provider: "openai", + endpoint: "http://127.0.0.1:11434/v1", + }) + ).error, + ).toBeUndefined(); +}); + +test("the synthetic test never sends a saved key to another provider and uses the draft prompt", async () => { + const fake = await fakeDiffr("gemini-secret"); + await expect( + testDiffrSummarizer({ ...draft, provider: "openai" }), + ).rejects.toThrow("Add an API key"); + vi.stubEnv("OPENAI_API_KEY", "env-openai"); + expect( + await testDiffrSummarizer({ + ...draft, + provider: "openai", + systemPrompt: "Draft prompt.", + }), + ).toBe("count positive values"); + expect( + JSON.parse(await readFile(path.join(fake.root, "test-env"), "utf8")), + ).toEqual({ gemini: "", openai: "env-openai" }); + const config = await readFile(path.join(fake.root, "test-config"), "utf8"); + expect(config).toContain('provider = "openai"'); + expect(config).toContain("Draft prompt."); +}); + +test("reads the environment key of the saved provider", async () => { + await fakeDiffr(); + await saveDiffrSummarizer({ ...draft, enabled: false, provider: "anthropic" }); + vi.stubEnv("GEMINI_API_KEY", "gemini-env"); + expect((await readDiffrConfig()).credentialSource).toBe("missing"); + vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-env"); + expect((await readDiffrConfig()).credentialSource).toBe("environment"); +}); diff --git a/packages/review/src/server/diffr-config.ts b/packages/review/src/server/diffr-config.ts index 185742116..3d6f4663f 100644 --- a/packages/review/src/server/diffr-config.ts +++ b/packages/review/src/server/diffr-config.ts @@ -13,8 +13,10 @@ import { } from "@dev.fast/json"; import { type ReviewDiffrConfig, + type ReviewDiffrProvider, type ReviewDiffrSummarizerInput, decodeStructuralDiffEvent, + reviewDiffrProviders, reviewDiffrSummarizerInputSchema, } from "@dev.fast/review-protocol"; import { stringify } from "smol-toml"; @@ -69,12 +71,16 @@ async function values( reveal = false, signal?: AbortSignal, ): Promise { - const output = await diffr( - ["config", "show", "--json", ...(reveal ? ["--reveal"] : [])], - rootPath, - { signal }, + return json( + await diffr( + ["config", "show", "--json", ...(reveal ? ["--reveal"] : [])], + rootPath, + { signal }, + ), ); +} +function json(output: string): JsonObject { try { const parsed = parseJsonText(output); @@ -97,8 +103,49 @@ function valueAt(object: JsonObject, key: string): JsonValue | undefined { const prefix = "plugins.bundled.summarize"; -function environmentKey(): string | undefined { - return process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || undefined; +const keyVariables: Record = { + gemini: ["GEMINI_API_KEY", "GOOGLE_API_KEY"], + openai: ["OPENAI_API_KEY"], + anthropic: ["ANTHROPIC_API_KEY"], +}; + +function savedProvider(config: JsonObject): ReviewDiffrProvider { + const value = valueAt(config, `${prefix}.provider`); + + return ( + reviewDiffrProviders.find((provider) => provider === value) ?? "gemini" + ); +} + +function environmentKey(provider: ReviewDiffrProvider): string | undefined { + return keyVariables[provider] + .map((name) => process.env[name]) + .find((key) => !!key); +} + +function keyOptional(provider: ReviewDiffrProvider, endpoint: string): boolean { + return provider === "openai" && endpoint !== ""; +} + +/** The default prompt and the link its description gives to it. */ +async function defaultPrompt( + rootPath?: string, +): Promise> { + const schema = json(await diffr(["config", "schema"], rootPath)); + const prompt = valueAt( + schema, + "properties.plugins.properties.bundled.properties.summarize.properties.system_prompt", + ); + + if (!isJsonObject(prompt)) return {}; + const description = isStringValue(prompt.description) + ? prompt.description + : ""; + + return { + defaultPrompt: isStringValue(prompt.default) ? prompt.default : undefined, + defaultPromptUrl: /https:\/\/\S+/.exec(description)?.[0], + }; } async function read(rootPath?: string): Promise { @@ -108,7 +155,7 @@ async function read(rootPath?: string): Promise { const credentialSource = isStringValue(saved) && saved.length > 0 ? "config" - : environmentKey() + : environmentKey(savedProvider(config)) ? "environment" : "missing"; @@ -124,7 +171,11 @@ async function read(rootPath?: string): Promise { } } - return { values: config, credentialSource }; + return { + values: config, + credentialSource, + ...(await defaultPrompt(rootPath)), + }; } export function readDiffrConfig(rootPath?: string): Promise { @@ -188,16 +239,20 @@ export function saveDiffrSummarizer( return serialized(async () => { const current = await read(rootPath); + const switching = draft.provider !== savedProvider(current.values); + const savedKey = !switching && current.credentialSource === "config"; if ( draft.enabled && !draft.apiKey && - current.credentialSource === "missing" + !savedKey && + !environmentKey(draft.provider) && + !keyOptional(draft.provider, draft.endpoint) ) { return { ...current, changed: false, - error: "Add a Gemini API key before enabling summaries.", + error: "Add an API key before enabling summaries.", }; } @@ -206,6 +261,17 @@ export function saveDiffrSummarizer( if (!draft.enabled) entries.push([`${prefix}.enabled`, false]); if (draft.apiKey) entries.push([`${prefix}.api_key`, draft.apiKey]); + else if (switching) entries.push([`${prefix}.api_key`, ""]); + entries.push([`${prefix}.provider`, draft.provider]); + + // Unset keys read as undefined; skip writing an empty string over them. + for (const [key, value] of [ + ["endpoint", draft.endpoint], + ["system_prompt", draft.systemPrompt], + ] as const) { + if (value !== (valueAt(current.values, `${prefix}.${key}`) ?? "")) + entries.push([`${prefix}.${key}`, value]); + } entries.push( [`${prefix}.model`, draft.model], [`${prefix}.tests`, draft.tests], @@ -239,24 +305,33 @@ export async function testDiffrSummarizer( if (!isJsonObject(saved)) throw new Error("The summarizer is not available in this configuration."); + const { provider, endpoint, model, systemPrompt } = parsed.data; + const switching = provider !== savedProvider(config); + const apiKey = parsed.data.apiKey || - (isStringValue(saved.api_key) ? saved.api_key : undefined) || - environmentKey(); + (!switching && isStringValue(saved.api_key) ? saved.api_key : "") || + environmentKey(provider); - if (!apiKey) throw new Error("Add a Gemini API key to test summaries."); + if (!apiKey && !keyOptional(provider, endpoint)) + throw new Error("Add an API key to test summaries."); directory = await mkdtemp(join(tmpdir(), "review-summary-")); const options: JsonObject = { ...saved, enabled: true, - model: parsed.data.model, + provider, + model, + system_prompt: systemPrompt, min_lines: 1, retries: 0, request_timeout_ms: 45_000, }; delete options.api_key; + + if (endpoint) options.endpoint = endpoint; + else delete options.endpoint; await mkdir(join(directory, "diffr")); await writeFile( join(directory, "diffr", "config.toml"), @@ -290,9 +365,13 @@ export async function testDiffrSummarizer( { env: { ...process.env, + ...Object.fromEntries( + Object.values(keyVariables) + .flat() + .map((name) => [name, ""]), + ), + ...(apiKey && { [keyVariables[provider][0]]: apiKey }), XDG_CONFIG_HOME: directory, - GEMINI_API_KEY: apiKey, - GOOGLE_API_KEY: "", }, signal: deadline, }, @@ -319,7 +398,9 @@ export async function testDiffrSummarizer( ) && annotation ) { - return annotation.label.split(apiKey).join("[redacted]"); + return apiKey + ? annotation.label.split(apiKey).join("[redacted]") + : annotation.label; } } catch { /* Never return provider output or parser causes. */ From cc1578767acf7dbb9b4095b82a3353375c74bb3e Mon Sep 17 00:00:00 2001 From: thesiti92 Date: Wed, 30 Sep 2026 15:54:34 -0400 Subject: [PATCH 02/10] Choose the summary provider, endpoint and prompt in Settings The prompt shows whether it is the default, links to the default in diffr's source, and can be reset. Choice moves to its own file so both settings screens share it. Agent-Session: 459ae7ec-7302-42c5-b473-4e315a7e4e19 Agent-Session: 203ee435-a867-494c-bb56-ddde9edf962e Agent-Session: ff765ffc-78bb-499b-9e86-9f7a942edbf0 Agent-Session: 78694b58-9718-40bf-a6b8-661e21afe339 Agent-Session: a9cd3da4-c06c-4d2b-b8e8-e71439cee705 Agent-Session: 0e24d827-3a5e-4ed7-aa02-bbda8d39a5e0 Agent-Session: 01a0f3f9-35d2-7bd1-a229-ec501a87447e --- .../src/diffr-config-section.browser.test.tsx | 90 +++++++++++- .../review/app/src/diffr-config-section.tsx | 138 ++++++++++++++++-- packages/review/app/src/settings-choice.tsx | 46 ++++++ packages/review/app/src/settings-page.tsx | 45 +----- packages/review/app/src/settings-styles.ts | 6 + .../review/src/server/diffr-config.test.ts | 19 ++- packages/review/src/server/diffr-config.ts | 3 + 7 files changed, 288 insertions(+), 59 deletions(-) create mode 100644 packages/review/app/src/settings-choice.tsx diff --git a/packages/review/app/src/diffr-config-section.browser.test.tsx b/packages/review/app/src/diffr-config-section.browser.test.tsx index ef4a8b527..0a090a9cb 100644 --- a/packages/review/app/src/diffr-config-section.browser.test.tsx +++ b/packages/review/app/src/diffr-config-section.browser.test.tsx @@ -16,9 +16,14 @@ afterEach(async () => { document.body.replaceChildren(); }); +const PROMPT_URL = + "https://github.com/devdotfast/diffr/blob/main/plugins/summarize/plugin.toml#L68-L74"; + function config(): ReviewDiffrConfig { return { credentialSource: "config", + defaultPrompt: "Default prompt.", + defaultPromptUrl: PROMPT_URL, values: { plugins: { bundled: { @@ -28,7 +33,12 @@ function config(): ReviewDiffrConfig { "removed-runs": { enabled: true }, group: { enabled: true }, "hide-files": { enabled: true, deleted: true, tags: ["test"] }, - summarize: { enabled: false, model: "test-model", tests: true }, + summarize: { + enabled: false, + model: "test-model", + tests: true, + system_prompt: "Default prompt.", + }, }, }, }, @@ -53,7 +63,10 @@ async function mount() { bundled: { summarize: { enabled: input.enabled, + provider: input.provider, model: input.model, + endpoint: input.endpoint, + system_prompt: input.systemPrompt, tests: input.tests, }, }, @@ -166,7 +179,10 @@ test("tests draft settings without saving, then saves and clears the key", async }); expect(actions.saveSummarizer).toHaveBeenCalledWith({ enabled: false, + provider: "gemini", model: "test-model", + endpoint: "", + systemPrompt: "Default prompt.", tests: true, apiKey: "test-secret", }); @@ -257,3 +273,75 @@ test("shows partial save errors with authoritative values and a reload prompt", .element(page.getByRole("button", { name: "Reload window", exact: true })) .toBeVisible(); }); + +test("defaults to Gemini and switching provider resets the model and key", async () => { + const { actions } = await mount(); + await open(); + await expect + .element(page.getByRole("radio", { name: "Gemini" })) + .toHaveAttribute("aria-checked", "true"); + await act(async () => { + await page.getByLabelText("API key", { exact: true }).fill("typed-secret"); + }); + await act(async () => { + await page.getByRole("radio", { name: "Anthropic" }).click(); + }); + await expect + .element(page.getByLabelText("Model", { exact: true })) + .toHaveValue("claude-haiku-4-5"); + await expect + .element(page.getByLabelText("API key", { exact: true })) + .toHaveValue(""); + await expect + .element(page.getByText("Saving clears the key saved for Gemini.")) + .toBeVisible(); + await act(async () => { + await page + .getByLabelText("Endpoint URL", { exact: true }) + .fill(" https://proxy.example/anthropic "); + }); + await act(async () => { + await page.getByRole("button", { name: "Save summaries" }).click(); + }); + expect(actions.saveSummarizer).toHaveBeenCalledWith({ + enabled: false, + provider: "anthropic", + model: "claude-haiku-4-5", + endpoint: " https://proxy.example/anthropic ", + systemPrompt: "Default prompt.", + tests: true, + apiKey: "", + }); +}); + +test("edits the prompt, links its default, and resets it", async () => { + const { actions } = await mount(); + await open(); + const prompt = page.getByLabelText("Prompt", { exact: true }); + await expect.element(prompt).toHaveValue("Default prompt."); + await expect + .element(page.getByText("Default", { exact: true })) + .toBeVisible(); + await expect + .element(page.getByRole("link", { name: "Default prompt on GitHub" })) + .toHaveAttribute("href", PROMPT_URL); + await act(async () => { + await prompt.fill("Be terse."); + }); + await expect + .element(page.getByText("Customized", { exact: true })) + .toBeVisible(); + await act(async () => { + await page.getByRole("button", { name: "Test setup" }).click(); + }); + expect(actions.testSummarizer).toHaveBeenCalledWith( + expect.objectContaining({ systemPrompt: "Be terse." }), + ); + await act(async () => { + await page.getByRole("button", { name: "Reset to default" }).click(); + }); + await expect.element(prompt).toHaveValue("Default prompt."); + await expect + .element(page.getByRole("button", { name: "Save summaries" })) + .toBeDisabled(); +}); diff --git a/packages/review/app/src/diffr-config-section.tsx b/packages/review/app/src/diffr-config-section.tsx index c88d536ca..2815f45c2 100644 --- a/packages/review/app/src/diffr-config-section.tsx +++ b/packages/review/app/src/diffr-config-section.tsx @@ -1,15 +1,18 @@ import { Button } from "@canvas/ui/button"; -import { TextField } from "@canvas/ui/text-field"; +import { TextField, fieldStyles } from "@canvas/ui/text-field"; import { type JsonValue, type ReviewDiffrConfig, type ReviewDiffrConfigActions, + type ReviewDiffrProvider, type ReviewDiffrSummarizerInput, isJsonObject, + reviewDiffrProviders, } from "@dev.fast/review-protocol"; import * as stylex from "@stylexjs/stylex"; import { type ReactNode, useEffect, useRef, useState } from "react"; +import { Choice } from "./settings-choice"; import { settingsStyles as styles } from "./settings-styles"; const displaySettings = [ @@ -34,10 +37,39 @@ function setting( return value; } +const PROVIDER_LABELS: Record = { + gemini: "Gemini", + openai: "OpenAI", + anthropic: "Anthropic", +}; + +const DEFAULT_MODELS: Record = { + gemini: "gemini-3.8-flash", + openai: "gpt-5-mini", + anthropic: "claude-haiku-4-5", +}; + +const DEFAULT_ENDPOINTS: Record = { + gemini: "https://generativelanguage.googleapis.com", + openai: "https://api.openai.com/v1", + anthropic: "https://api.anthropic.com", +}; + +function provider(config: ReviewDiffrConfig): ReviewDiffrProvider { + const value = setting(config, "summarize.provider"); + + return reviewDiffrProviders.find((choice) => choice === value) ?? "gemini"; +} + function summaryDraft(config: ReviewDiffrConfig): ReviewDiffrSummarizerInput { return { enabled: setting(config, "summarize.enabled") === true, + provider: provider(config), model: String(setting(config, "summarize.model") ?? ""), + endpoint: String(setting(config, "summarize.endpoint") ?? ""), + systemPrompt: String( + setting(config, "summarize.system_prompt") ?? config.defaultPrompt ?? "", + ), tests: setting(config, "summarize.tests") === true, apiKey: "", }; @@ -117,7 +149,7 @@ export function DiffrConfigSection({ const unavailable = !config || setting(config, "summarize.enabled") === undefined; - const summaryValid = !!draft?.model.trim(); + const summaryValid = !!draft?.model.trim() && !!draft.systemPrompt.trim(); const hiddenTags = config ? setting(config, "hide-files.tags") : undefined; return ( @@ -192,8 +224,8 @@ export function DiffrConfigSection({ ))}

AI summaries

- Sends source-file contents to Gemini to summarize large new - functions and tests. + Sends source-file contents to the selected provider to + summarize large new functions and tests.

{unavailable && (

@@ -215,6 +247,22 @@ export function DiffrConfigSection({ } /> + + + setDraft({ + ...draft, + provider: choice, + model: DEFAULT_MODELS[choice], + apiKey: "", + }) + } + /> +

- {config.credentialSource === "config" - ? "Saved key" - : config.credentialSource === "environment" - ? "Environment key available" - : "Not configured"} - . Replacement keys are stored in diffr’s config. Leave - blank to keep the current key. + {draft.provider !== provider(config) + ? `Saving clears the key saved for ${PROVIDER_LABELS[provider(config)]}.` + : `${ + config.credentialSource === "config" + ? "Saved key" + : config.credentialSource === "environment" + ? "Environment key available" + : "Not configured" + }. Replacement keys are stored in diffr’s config. Leave blank to keep the current key.`}

+ + + setDraft({ ...draft, endpoint: event.target.value }) + } + /> + +

+ Optional. With OpenAI, point this at any compatible + server, such as OpenRouter or Ollama. +

+