···88## [Unreleased]
991010### Added
1111+- AI chat endpoint URL validation (v0.59.0, #675) — the AI chat settings panel now validates the Endpoint field before persisting. Invalid inputs (empty strings, `ftp://…`, `javascript:`, `file://`, bare hostnames without a protocol, protocol-relative `//…`, malformed URLs) no longer overwrite a working endpoint in localStorage: the previous value is retained. Invalid inputs also set `aria-invalid="true"` on the input and surface an inline error message (`role="alert"`) explaining the accepted formats (`/api/ai` same-origin path or an absolute https URL). A permanent helper hint sits below the input so users understand the format up-front; both elements are linked via `aria-describedby`. Built on the existing `validateEndpoint()` helper, which was previously only consulted to toggle the onboarding banner. 17 new tests (7 unit tests for `validateEndpoint`, 10 jsdom tests for the persist flow + a11y attributes).
1112- Sheets fit-and-finish (v0.58.0) — four Excel/Sheets-parity wins for daily spreadsheet work:
1213 - **Auto-format on entry**: typing `$1,234.56`, `75%`, `2025-03-15`, or `1,234` in a cell now stores the parsed numeric value and stamps an appropriate format (`currency`, `percent`, `date`, `number`) instead of keeping the raw string. Preserves existing cell formats so explicit user choices aren't clobbered. New `src/sheets/auto-format.ts` module with 30 unit tests.
1314 - **Preset color palette**: both the text-color and background-color pickers now have a ▾ dropdown beside the native `<input type="color">` showing 14 curated swatches (black/grays/white + 9 accent colors in a 7×2 grid). The native picker still works; the palette short-circuits it for the common case. New `src/sheets/color-palette.ts` module with 15 jsdom tests covering render, open/close, click-outside, and Escape.
···22 * AI Chat — shared event wiring for the chat panel across all editor types.
33 */
4455-import { isConfigured, saveConfig, MODEL_OPTIONS } from './types.js';
55+import { isConfigured, saveConfig, validateEndpoint, MODEL_OPTIONS } from './types.js';
66import type { ChatConfig, ChatPosition, ChatState } from './types.js';
77import type { EditorType } from './system-prompt.js';
88import type { createChatSidebar } from './sidebar-dom.js';
···173173 ? (chatUI.container.querySelector('#ai-model-custom') as HTMLInputElement).value.trim()
174174 : chatUI.modelSelect.value;
175175176176+ // #675 — validate endpoint before persisting. An invalid URL shown in
177177+ // the input blocks the save, flags the input with aria-invalid=true,
178178+ // and renders an error message in the adjacent role=alert element.
179179+ // Other fields (model, etc.) still persist so the user doesn't lose
180180+ // unrelated edits because of a typo in the endpoint.
181181+ const rawEndpoint = chatUI.endpointInput.value.trim();
182182+ const endpointValid = validateEndpoint(rawEndpoint);
183183+ if (!endpointValid) {
184184+ chatUI.endpointInput.setAttribute('aria-invalid', 'true');
185185+ if (chatUI.endpointError) {
186186+ chatUI.endpointError.textContent =
187187+ rawEndpoint.length === 0
188188+ ? 'Endpoint is required.'
189189+ : 'Invalid endpoint. Use a same-origin path (e.g. /api/ai) or an absolute https URL.';
190190+ chatUI.endpointError.hidden = false;
191191+ }
192192+ } else {
193193+ chatUI.endpointInput.removeAttribute('aria-invalid');
194194+ if (chatUI.endpointError) {
195195+ chatUI.endpointError.textContent = '';
196196+ chatUI.endpointError.hidden = true;
197197+ }
198198+ }
199199+176200 chatConfig = {
177201 ...chatConfig,
178178- endpoint: chatUI.endpointInput.value.trim(),
202202+ // Keep the previous endpoint if the new one is invalid — don't
203203+ // overwrite a working URL with garbage.
204204+ endpoint: endpointValid ? rawEndpoint : chatConfig.endpoint,
179205 model: model || 'anthropic/claude-sonnet-4.6',
180206 };
181207 opts.chatConfig = chatConfig;