* feat(podcasts): adopt podcast-creator 0.13.1
podcast-creator 0.13 requests schema-validated JSON for outlines and
transcripts, raises the default output budget to 8192 tokens for both, and
adds a ~400 ms pause between turns; 0.13.1 falls back to plain JSON when an
endpoint rejects json_schema (lfnovo/podcast-creator#51).
- Floor raised to podcast-creator>=0.13.1.
- The episode profile's Max output tokens help no longer claims 3000/5000
defaults (all 14 locales); docs cover the 8192 default, when to lower it,
the turn pause and the JSON fallback.
- Tests render our shadowed prompts/podcast templates with the variables
0.13 passes, and check they are ours (not the bundled ones).
Closes#1435
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* fix(i18n): translate the full max tokens guidance in every locale
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* fix(i18n): use Gesprächsrunde for 'turn' in the German max tokens help
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(sources): OpenDocument uploads and content-core 2.2 settings docs
- Upload accepts .odt, .ods, .odp (content-core 2.2 office route) and .htm;
the multi-file hint lists the OpenDocument formats in every locale.
- Docs: OpenDocument in the supported file types; CCORE_YOUTUBE_PROXY,
CCORE_YOUTUBE_COOKIES_FILE and CCORE_AUDIO_SEGMENT_MINUTES in the
environment reference (set on the worker; Compose example for cookies);
a YouTube "blocked" note pointing at them.
Closes#1439
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* fix(sources): close the cookies code block; list HTML/HTM in the upload hint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(transformations): refuse to run on sources with no text
create_source_insight now returns 400 when the source's full_text is empty
or whitespace, so no job is queued, and run_transformation raises
InvalidInputError before calling the model as defence in depth. The source
detail view shows the server's reason instead of a generic error toast.
Closes#1394
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* fix(transformations): raise InvalidInputError for empty sources
Uses the registered typed-exception handler instead of a bare HTTPException.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* fix(transformations): address review on empty-source guard
- Worker: InvalidInputError is a permanent failure for run_transformation
(stop_on + re-raise), so an empty source is not retried five times.
- Graph message is neutral ("There is no text content to transform"); it
also guards the direct /transformations/execute path.
- UI: the empty-source error is mapped to a translated apiErrors key in all
locales, and a network error without a server detail keeps the localized
generic message.
- Tests assert the exact queued command and the worker's permanent failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* fix(transformations): skip whitespace-only text in source processing
- transform_content (source graph) skips whitespace-only text like empty
text, so process_source never hits the transformation graph's guard.
- The graph's message maps to the same translated key, and the polled
command failure toast goes through getApiErrorMessage.
- Rename the worker test to what it checks (permanent-failure handling).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* docs(changelog): move entry to avoid merge conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* test(frontend): cover the empty-source error mapping
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(notebooks,transformations): persist a cleared description
NotebookHeader and TransformationEditorDialog sent `description || undefined`,
so an empty description was dropped from the update. They now send it as-is
(a cleared transformation title falls back to the name, as on create). The
update endpoints use model_fields_set: an explicit "" or null clears the
field, an absent one leaves it untouched.
Closes#1396
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* fix(transformations): leave title handling unchanged; test notebook clear
The form schema already requires a title, so the empty-title fallback was
unreachable; the title is sent as before. Adds a NotebookHeader test that
clearing the description sends `description: ""`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* test(transformations): clearing the description sends an empty string
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* docs(changelog): move entry to avoid merge conflicts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(source-chat): stream only the new answer; restore context badge on load
The SSE generator emitted an ai_message for every AI message in the
checkpointed history, and the hook concatenated them into one bubble. Only
the last message is emitted now. The session-load effect also sets the
context indicators returned by GET session, so the badge survives refresh.
Closes#1393
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* fix(source-chat): clear context badge when the session goes away
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
* style: drop unrelated formatting changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBYpxzSS62Y3TZkgB9NUZy
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* docs: design-system decision record, contributor styling rule, refreshed brand assets
- ADR-008: visual identity as a token contract in globals.css, reviewed
through /dev/design (laws, dark-mode resolution constraint, hex
rationale, alternatives)
- frontend/AGENTS.md: hard rule — styling via semantic tokens only, no
raw palette classes or hand-written dark: twins
- README: new Quiet Green hero mark and a current workspace screenshot
replacing the pre-redesign UI; drop a dead image reference
* docs: renumber design-token record to ADR-009 and index it
ADR-008 was taken by the notebook-scoped-search record (#1331) after this
branch was opened. Renames the file, updates the frontend/AGENTS.md link
and adds the missing index row.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnMHW3qrSc8Nhi7xTPfZuv
* docs: renumber the design-token ADR to 011 and index ADR-009
ADR-009 was taken by the PBKDF2 credential record (#1360), which also
never got its row in decisions/README.md; ADR-010 is reserved for the
source-chat checkpointer bridge (#1332). Update the frontend/AGENTS.md
link accordingly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScFn54FhBq3cvVbcbQJjBZ
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix(frontend): silence noisy KaTeX warning on Unicode symbols
AI-generated content often contains Unicode punctuation (en/em dashes,
smart quotes) inside single-dollar math spans, e.g. a price range like
"$5 – $10". Models legitimately emit inline math as $...$ per this
project's own prompts ("MATH FORMATTING" sections), so remark-math
correctly reads it as math - but KaTeX's default strict mode then logs
a console warning for every character outside its symbol table.
The warning is harmless (KaTeX still renders a best-effort glyph) but
noisy: 66+ occurrences in a few days of normal use on one deployment.
Fix: pass KaTeX's own `strict` option as a function that ignores only
the `unknownSymbol` error code, leaving every other strict check (e.g.
deprecated commands) at its default. All three markdown-rendering call
sites (chat/insight/source rendering, the note editor live preview,
and the transformation playground) now share one KATEX_OPTIONS
constant instead of duplicating the same override three times.
Verified: 140/140 frontend tests pass (including the existing test
that asserts single-dollar inline math still renders via KaTeX),
tsc --noEmit clean, eslint clean (0 errors), production build clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* address review: lock down unknownSymbol suppression with tests
Per cubic's review on this PR:
- Add a regression test asserting the em/en-dash-in-price-range
pattern that motivated this fix produces no [unknownSymbol] warning,
and a second test asserting a different strict violation
([commentAtEnd]) is still warned on - proving the ignore is scoped
to exactly one error code, not silently widened.
- Reword the KATEX_OPTIONS comment: suppressing unknownSymbol isn't
purely cosmetic. KaTeX's own parser falls back unrecognized
codepoints to mode: 'text' rather than proper math-mode metrics, so
an ignored symbol can render with approximated spacing rather than a
real glyph. Acceptable for stray punctuation, not a blanket license
to route arbitrary Unicode through math mode.
Verified: 142/142 frontend tests pass (up from 140 - the two new
tests), tsc --noEmit clean, eslint clean (0 errors).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Luis Novo <lfnovo@gmail.com>
Frontend: next 16.3.8 (critical next/og ImageResponse RCE advisory; the
app does not use next/og), axios 1.20.0, and the postcss (8.5.23) and
brace-expansion (1.1.21) npm overrides.
Python (uv.lock): anyio 4.14.2, pyjwt 2.15.1, tornado 6.5.10,
urllib3 2.8.0, virtualenv 21.14.5 (filelock 4.0.9 follows).
Dependabot now ignores odd-numbered (non-LTS) Node majors for the
image base. vitest stays at 4.1.9: the bump trips an npm 10 tree bug
and only clears moderate test-runner findings.
Claude-Session: https://claude.ai/code/session_01ScFn54FhBq3cvVbcbQJjBZ
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Bumps the npm-security group with 1 update in the /frontend directory: [sharp](https://github.com/lovell/sharp).
Updates `sharp` from 0.34.5 to 0.35.4
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.34.5...v0.35.4)
---
updated-dependencies:
- dependency-name: sharp
dependency-version: 0.35.4
dependency-type: indirect
dependency-group: npm-security
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Luis Novo <lfnovo@gmail.com>
* feat(search): scope Search and Ask to selected notebooks
Search and Ask always ran against the whole knowledge base; `notebook_id`
was accepted by POST /api/search and silently ignored because the request
model never declared it. Both endpoints now take an optional notebook
scope and the Search/Ask page gets a shared notebook selector.
- Migration 24: fn::text_search / fn::vector_search gain an optional
trailing `$notebook_ids: option<array<record<notebook>>>` parameter.
NONE/empty keeps the unfiltered path; otherwise sources are limited via
the `reference` edge and notes via the `artifact` edge (chunks and
insights follow their parent source). The 4-/5-arg call shapes still
work, so existing callers are unaffected; `24_down` restores the
previous definitions.
- API: SearchRequest / AskRequest accept `notebook_ids` (list) and
`notebook_id` (single, the shape clients already send); ids are
validated (400 for non-notebook ids, 404 for unknown notebooks) before
reaching the query so a typo never masquerades as "no matches".
- Domain: text_search / vector_search take `notebook_ids`; the highlight
overflow fallback preserves the scope.
- Ask graph: scope travels in the thread state and reaches every fan-out
vector search.
- Frontend: NotebookScopeSelector (collapsible checkbox list, "All
notebooks" by default, Clear action) shared by the Ask and Search tabs;
strings in all 14 locales.
- Docs: user guide section on scoping; API reference note.
Closes#574Closes#87
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U53d3BNjGghLsf6tufRTKX
* fix(search): harden notebook scope validation after review
- Raise typed InvalidInputError / NotFoundError instead of bare
HTTPException, per the API error convention.
- Check notebook existence with a single query for the whole scope; a
database failure now surfaces as 500 instead of a misleading 404.
- Bound notebook_ids to 50 entries; keep empty ids so they are rejected
(400) rather than silently widening the scope to everything.
- Replace the pluralized "N notebooks selected" string with a neutral
"N selected" form: the locale parity test forbids per-language extra
keys, so _few/_many forms for ru/pl were not an option.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U53d3BNjGghLsf6tufRTKX
* refactor(search): move scope resolution to the domain, add ADR-008
- resolve_notebook_scope() now lives in open_notebook/domain/notebook.py
next to text_search / vector_search; the router only calls it.
- Reject malformed ids such as "notebook:" (empty key) and anything
RecordID.parse cannot handle with InvalidInputError instead of letting
them surface as 500.
- ADR-008 records the notebook-scope semantics, the compatibility choice
(notebook_id + notebook_ids, optional trailing SurrealQL parameter) and
how it relates to the #1315 retrieval contract.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U53d3BNjGghLsf6tufRTKX
* test(search): cover migration 24, validate Ask scope before model lookups
- Ask endpoints resolve the notebook scope first, so a malformed or
unknown scope returns 400/404 before any model or embedding lookup.
- Registration and content tests for 24.surrealql / 24_down.surrealql,
following the existing migration test pattern.
- ADR-008 context trimmed to the template's sentence budget.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U53d3BNjGghLsf6tufRTKX
* fix(search): surface a typed error when scope resolution hits the database
resolve_notebook_scope() now logs the raw driver error and raises
DatabaseOperationError with a fixed message, matching text_search /
vector_search, so connection details never reach the 500 response body.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U53d3BNjGghLsf6tufRTKX
* fix(search): compare notebook scope ids as strings
The driver returns `id` as RecordID objects, which are unhashable and
never equal to the request strings, so every real scoped request raised
TypeError before reaching the search. Normalize to str before comparing
and add a regression test that mocks RecordID rows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014kRBoPiBD1NKeKHCo6EyrE
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* refactor: rename "API Keys" settings page to "Models" (#1199)
Rename the provider credentials settings area from "API Keys" to "Models":
- Route: /settings/api-keys -> /settings/models with a permanent redirect
- UI copy: page title, description, and learn-more now use "Models" wording
- i18n: updated display values across all 14 locales (key names unchanged)
- Internal links: sidebar, setup banner, and command palette point to /settings/models
- Docs: "Settings → API Keys" references updated to "Settings → Models"
* docs(quick-start): update model settings navigation to Manage → Models
* fix(frontend): highlight only the active sidebar item on nested routes
Use longest-prefix matching so parent items like Settings don't also
highlight when viewing a child page such as Models. Add test coverage
for the Models page active state.
---------
Co-authored-by: Luis Novo <lfnovo@gmail.com>
* fix(frontend): keep Ask loading state clearable under React Strict Mode
The mountedRef effect only declared a cleanup function and never set the
ref back to true on mount. React Strict Mode mounts, unmounts and remounts
in development, so the first cleanup left mountedRef.current false forever
and every guard depending on it became dead code.
That silently disabled all five paths that end the loading state:
stopStreaming on final_answer and complete, the normal end-of-stream call,
the error handler, and the idle watchdog added in #777. The Ask spinner ran
forever and the answer was never rendered, even when the stream completed
successfully.
* test(frontend): cover Ask loading state across StrictMode remount
Renders useAsk inside StrictMode so the mount → unmount → remount sequence
runs, then drives a mocked SSE stream to completion and to an in-band error
event. Both assertions fail on the pre-fix hook with `expected true to be
false`, because mountedRef.current stays false and every setState becomes a
no-op — the existing suite could not catch this since nothing exercised a
remount.
---------
Co-authored-by: Luis Novo <lfnovo@gmail.com>
Resolves the three open high-severity Dependabot alerts (two pyasn1
entries in uv.lock, one brace-expansion in the frontend lockfile pinned
via overrides).
* docs(search): document content coverage for text and vector search
- Add 'Content searched'sections to the user guide explaining
which source/note fields are matched by text and vector search.
- Surface a short coverage hint below the search input in the UI.
- Add translations across all supported locales.
Fixes#1267
* feat(search): make coverage hint reflect selected search type
- Show vector-specific coverage hint when vector search is selected.
- Split searchCoverage locale key into searchCoverageText and searchCoverageVector across all supported locales.
---------
Co-authored-by: Luis Novo <lfnovo@gmail.com>
Let the speaker-profile row wrap and constrain the voice-model badge
(min-w-0, max-w-full, truncate with a title tooltip) so long
provider/model names no longer clip past the card edge.
Fixes#1196
Co-authored-by: Luis Novo <lfnovo@gmail.com>
The SSE consumer in useSourceChat decoded and split each network chunk in
isolation (decoder.decode(value) with no { stream: true }, and text.split('\n')
with no carry-over). A data: line split across two reads was dropped (first half
fails JSON.parse, second half fails the 'data: ' guard) and a multibyte char
straddling a chunk boundary decoded to U+FFFD.
Carry the incomplete trailing line across reads and use a streaming decoder,
mirroring the sibling hook useAsk in use-ask.ts.
Co-authored-by: pacocartones <pacocartones@users.noreply.github.com>
Co-authored-by: Luis Novo <lfnovo@gmail.com>
* feat(frontend): design foundation — Quiet Green tokens, fonts, primitives, /dev/design
- Port the ratified design token system into globals.css: core palette,
semantic surfaces/ink/hairlines, content-type hues, evidence classes,
context states, squared 4-6px radii, shadow scale (popovers own the
only real shadow)
- Swap Inter for Bricolage Grotesque (display) + Instrument Sans (UI) +
Spline Sans Mono (data) via next/font
- Restyle shadcn primitives to the foundation laws: fern primary button,
red reserved for destructive, hairline cards without default shadow,
underline tabs, quiet neutral badges, teal focus ring, single-shadow
overlays
- Add /dev/design living styleguide (dev-only, 404 in production)
rendering all tokens and primitives with a theme toggle
* refactor(frontend): sweep hard-coded palette colors into design tokens
Replace raw Tailwind palette classes (red/amber/green/blue/emerald/sky/
purple…) across 38 components with the semantic token utilities:
errors -> destructive, warnings -> warn (clay), success -> fern,
system/info -> teal, context states -> ctx-* tokens, model-type chips
redistributed over the owned palette. Light/dark duplicates collapse
into single token classes since tokens re-resolve per theme.
* docs: changelog entry for the Quiet Green design foundation
* feat: add 'add source' button to sources page
* feat(i18n): add sources.newSource translation to all locales
* refactor(frontend): deduplicate AddSourceDialog in sources page
* i18n(de-DE): add missing sources.newSource key
The new sources.newSource key was added to the other 13 locales but not
de-DE, so the locale parity test (src/lib/locales/index.test.ts) failed
and de-DE users would fall back to the raw key. Add the German string.
---------
Co-authored-by: Luis Novo <lfnovo@gmail.com>
* fix: prevent Ask streaming loading from getting stuck on dangling connections
Adds a safety-net timeout (5 min) to useAsk that forces the loading
state to end even when the SSE stream's 'done' signal doesn't reach
the browser — which happens in Docker/standalone deployments where
the Next.js proxy connection is left open without proper termination.
Changes:
- use-ask.ts: AbortController per request, mountedRef for cleanup,
STREAM_TIMEOUT_MS that resets isStreaming, stopStreaming() helper
that centralizes cleanup + clears the timeout
- search.ts: pass AbortSignal through to fetch() so the timeout can
actually cancel the HTTP request
The complete event now also carries final_answer content as a
backup path in case the final_answer event is missed.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore: reduce stream timeout from 5 min to 75s
* fix(ask): make stream watchdog idle-based and abort dangling connections
The Ask stream safety timeout was a fixed 75s wall-clock cap measured from
stream start and never reset by incoming SSE events. A legitimately slow but
healthy Ask (slow local models — Ollama, LM Studio) would have its loading
state killed mid-stream, showing a partial/empty answer while the backend was
still producing — the exact slow-hardware scenario in #777.
- Re-arm the timeout on every received chunk so it fires only when the
connection goes genuinely silent (idle timeout, not wall-clock).
- Align the idle window to the API client budget (NEXT_PUBLIC_API_TIMEOUT_MS,
default 10 min; 0 disables) so slow local models aren't cut off. Export
API_TIMEOUT_MS from the client for this.
- On fire, abort the in-flight request so a truly dangling connection frees the
socket/reader instead of leaking it (the catch already swallows AbortError).
---------
Co-authored-by: dyzur <dyzur@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Luis Novo <lfnovo@gmail.com>
* fix: prevent source dialog action overlap
* fix(sources): pad only the right of the detail header to clear close button
px-12 cleared the overlap but also indented the header's left edge 40px
past the tab content below (which stays px-2), misaligning the title. Keep
px-2 and add pr-12 so the header left-aligns with the body while the actions
menu still clears the dialog close button on the right.
---------
Co-authored-by: Luis Novo <lfnovo@gmail.com>
* fix(frontend): support allowedDevOrigins via env var
Accessing the Next.js dev server from a LAN IP fails all HMR/dev
resource requests: Next.js blocks cross-origin dev requests (including
the webpack-hmr websocket) from any host not in an explicit allowlist,
as a DNS-rebinding guard. Server log:
Blocked cross-origin request to Next.js dev resource
/_next/webpack-hmr from "<lan-ip>".
Add NEXT_ALLOWED_DEV_ORIGINS (comma-separated hostnames) to populate
Next's allowedDevOrigins config, so LAN/custom-hostname dev access can
be enabled per-deployment without hardcoding a host into next.config.ts.
* fix(frontend): treat whitespace-only NEXT_ALLOWED_DEV_ORIGINS as unset
A whitespace-only or comma-only value parsed to an empty array, which is
truthy and still spread allowedDevOrigins: [] into the config, overriding
Next's default dev-origin behavior. Only set the key when at least one
origin survives trimming.
---------
Co-authored-by: Luis Novo <lfnovo@gmail.com>
The episode profile "Max output tokens" field (#991) confused users:
nothing explained what it controls, and whether it was a
per-conversation, per-turn, or per-call limit. An empty field also
showed "Must be a positive integer" — a scary error on an optional
field — because a transient/empty number-input state produced NaN,
which slipped past the nullable() guard into the positive() rule.
- Add help text (all 14 locales) stating it caps output tokens per
generation step — the outline and the full transcript, not a per-turn
or conversation limit — and that blank uses the built-in defaults
(3000 outline / 5000 transcript, from podcast-creator).
- Add a placeholder ("Leave blank for defaults") so the empty field
reads as intentionally optional rather than unfilled.
- Map empty and NaN inputs to null in setValueAs, so an empty or
half-typed value reads as "unset" and never trips the positive-integer
rule. Genuinely invalid entries (0, negatives, decimals) still error.
Field stays an optional override — no behavior change to generation.
Verified in the browser: help renders, placeholder shows, empty field
has no error, zero console errors. Frontend lint/test/build green.
Bump two dependencies flagged by Dependabot ahead of the v1.14.0 release:
- mcp 1.25.0 -> 1.28.1 (3 high alerts). Transitive via content-core ->
fastmcp; the advisories cover MCP *server* transports (missing
Host/Origin validation on the WebSocket transport, HTTP transports
serving session requests without verifying the authenticated
principal, and experimental task handlers exposing other clients'
tasks). Open Notebook does not serve MCP, so none of them are
reachable here, but fastmcp accepts the newer release and the bump
resolves cleanly with no other dependency churn.
- axios 1.16.0 -> 1.18.1 (medium alert). Prototype pollution allowing
Basic auth injection through auth subfields. Direct frontend
dependency; pulls nested agent-base/https-proxy-agent, which newer
axios uses for proxy handling.
Verified: uv sync + content-core/fastmcp import clean, frontend
npm ci + npm run build green, npm audit reports 0 vulnerabilities.
Surface content-core 2.x's docling_formulas (formula extraction) and
docling_vision (image/chart vision) enrichment flags in Settings →
Content Processing, mirroring the existing OCR toggle. Both default off
and are gated on Docling availability in the UI. The settings persist
via GET/PUT /api/settings and are threaded into content-core extraction
alongside docling_ocr. Migration 23 backfills the new fields on the
existing content_settings record. Labels and help are translated across
all 14 locales.
Closes#1131
* fix(models): stop auto-assign from re-filling cleared optional defaults
Auto-assign treated every empty default slot as "missing" and filled it,
so an optional slot a user deliberately cleared (to fall back to the chat
model) got silently re-populated on the next run, undoing the intent.
- Auto-assign now fills only the required slots (chat, embedding); the
optional slots (transformation, tools, large context, TTS, STT) are
left untouched.
- get_default_model("large_context") now falls back to the chat model
when unset, matching transformation/tools (TTS/STT still return None).
- Settings UI shows an inline hint on each empty optional slot: the text
slots show "using chat model (<name>)"; TTS/STT show a not-configured
hint. Required slots remain non-clearable. New i18n keys across all 14
locales.
Closes#1098
* test: guard await_args against None for mypy
The locale parity test and TranslationShape type only compared keys, not
the interpolation placeholders inside values. This let single-brace drift
(a stray {count} instead of i18next's {{count}}) slip through unnoticed.
Extend the parity suite to, for every leaf string, compare the set of
{{...}} placeholders against en-US and flag any stray single-brace token
that maps to a real en-US placeholder. Aligns zh-CN/zh-TW
podcasts.generationStartedDesc, which referenced a {{name}} placeholder
absent from the en-US reference.
Expose Esperanto's built-in omlx OpenAICompatibleProfile in Settings with
Ollama-style UX (default http://localhost:11435/v1, optional API key,
language+embedding discovery). No openai-compatible remapping or
OPENAI_COMPATIBLE_* env mirroring.
Fixes#1048
Co-authored-by: Luis Novo <lfnovo@gmail.com>
* perf(chat): isolate composer state to fix IME lag in long chats
Move the chat composer input state into a dedicated ChatComposer child so
keystrokes (including IME composition) no longer re-render the whole message
history. Memoize each message row with React.memo and make the reference-click
handler stable with useCallback so historical messages don't re-render on
composer input. Send behavior and the Ctrl/Cmd+Enter shortcut are preserved.
Closes#1147
* fix(chat): stabilize modal callbacks and use realistic shortcut tests
Memoize openModal/closeModal in useModalManager with useCallback so their
identities stay stable across renders. This keeps handleReferenceClick (which
depends on openModal) stable, so the memoized ChatMessage rows no longer
re-render on every ChatPanel render — completing the IME-lag fix.
Split the Ctrl/Cmd+Enter test into two realistic paths (Mac metaKey, non-Mac
ctrlKey) driven by a mocked userAgent, instead of an impossible combined
ctrlKey+metaKey event.
* fix(i18n): improve Japanese translation quality
- Replace 'トランスフォーメーション' with natural '変換'
- Improve various phrasing for natural Japanese
- Add spacing in duration format
- Refine accessibility labels and error messages
* fix(i18n): remove redundant 含める in ja-JP includeAllInsights
---------
Co-authored-by: Lovin <lovin@example.com>
Co-authored-by: Luis Novo <lfnovo@gmail.com>
Adds an optional "Max output tokens" number input to the episode profile form, mirroring num_segments handling: zod validation (positive integer, clearable to null), form defaults/reset, and submit payload. Shows the value in episode card details and adds max_tokens to the episode profile types.
Includes maxTokens/maxTokensInteger/maxTokensPositive translation keys in all 14 locales (the locale parity test requires new keys everywhere).
Closes#991
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Docling and local Crawl4AI are now opt-in, installed on first container startup
via OPEN_NOTEBOOK_ENABLE_DOCLING / OPEN_NOTEBOOK_ENABLE_CRAWL4AI, keeping the
default image lean (Crawl4AI un-bundled). Downloads cache on the /app/data
volume; installs are blocking with loud logs and degrade-don't-die on failure.
A GET /api/capabilities probe reports actual availability and the Settings UI
gates the Docling/Crawl4AI engines and OCR toggle (with env-var hints) until the
runtime is present. Docs folded from #1121 and rewritten to opt-in; ADR-007 added.
Closes#1122. Closes#1105.
Expose content-core's docling_ocr flag as a user setting. OCR runs on
scanned PDFs and images when the Docling engine handles them; it's on by
default (matching content-core) and can be turned off for faster
processing of text-native documents.
- New `docling_ocr` boolean on ContentSettings (default True), plumbed
through the settings API and into ContentCoreConfig in the source graph.
- "Enable OCR" checkbox in the Content Processing settings card, with
label + help translated across all 14 locales.
Part of #939. Stacked on #432.
* feat(sources): add Crawl4AI URL engine and honor persisted engine settings (#432)
- Add "crawl4ai" as a selectable URL processing engine (domain Literal,
settings API validation, SettingsForm select, and label across all 14
locales; urlHelp updated in en-US to describe the new fallback chain).
- The source graph now loads the persisted ContentSettings and passes the
document/URL engine choices to ContentCoreConfig. Previously it built a
hard-coded ContentSettings with "auto" engines, so a user's selection in
Settings never took effect. Falls back to defaults if settings can't load.
- Crawl4AI Docker mode is driven by content-core's native CRAWL4AI_API_URL
env var (documented separately under #1105).
Part of #939.
* fix(432): bundle Crawl4AI runtime + address review
- Bundle the Crawl4AI runtime so its local, no-API-key mode works out of
the box: depend on content-core[crawl4ai] and install the Chromium
browser via playwright in the Docker runtime-base (both image variants).
Footprint is modest (no torch/transformers/CUDA); image grows ~300 MB
from Chromium + system libs.
- Preserve the server-side traceback when persisted content settings fail
to load (logger.opt(exception=True)) instead of only the message.
- Reset the ContentSettings singleton between domain tests (clear_instance)
so a non-default value can't leak into neighboring tests.
Addresses review on #432.
* i18n(432): translate urlHelp Crawl4AI description across all 13 non-en locales
The Crawl4AI engine label was already localized; this brings the URL-engine
'help me choose' text in line with en-US in every locale — describing
Crawl4AI (local JS rendering, no API key) and its place in the auto
fallback chain (Firecrawl -> Jina -> Crawl4AI -> simple).
Since #1112 dropped the legacy provider/model strings, EpisodeCard's outline,
transcript and speaker rows showed "— / —" for new episodes. The API now
resolves the snapshot's model references (outline_llm/transcript_llm/voice_model)
to provider/name display fields at serialization time, batched into a single
query per request via Model.get_display_info_for_ids so listing episodes never
does a per-row model lookup. The card falls back to the legacy snapshot strings
for old episodes and degrades to a dash when a referenced model was deleted.
Closes#1114
Migration 22 best-effort maps profiles whose outline_llm/transcript_llm/
voice_model references are still empty to existing model records
(provider + name + type, no auto-create), clears the legacy values and
drops the 6 columns. The startup data migration that retried this
mapping on every boot (open_notebook/podcasts/migration.py) is deleted
along with its api/main.py lifespan hook; the legacy fields are removed
from the Pydantic models, API schemas and frontend types/panels.
Accepted trade-off: profiles whose mapping never converged lose the
legacy strings and stay unresolved - they were already non-functional
and the UI already flags them; the user re-picks models once.