254 Commits

Author SHA1 Message Date
Zarl-prog
b89141679f fix(cookbook): scroll serve panel into view when expanded (#1180) (#1191) 2026-06-02 23:21:35 +09:00
spooky
f667667da3 fix: distinguish external cookbook runtimes (#1188) 2026-06-02 23:20:00 +09:00
spooky
5b87e69221 feat: add vllm kv cache dtype option (#1185) 2026-06-02 23:17:16 +09:00
ghreprimand
7b43fa9372 Improve calendar event text contrast (#1184)
Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com>
2026-06-02 23:14:52 +09:00
ghreprimand
21b6f9344e Normalize native select option theming (#1178)
Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com>
2026-06-02 23:09:15 +09:00
pewdiepie-archdaemon
ff93a6c63b Polish email and cookbook flows 2026-06-02 22:42:07 +09:00
Afonso Coutinho
15a2662119 fix: markdown tables drop empty cells and misalign columns (#1164)
* refactor: extract splitTableRow helper for markdown tables

* fix: keep empty interior cells in markdown tables to preserve columns

* test: splitTableRow keeps empty interior cells
2026-06-02 22:41:27 +09:00
Kenny Van de Maele
68efa8ee53 Fix docked-modal close: chat stays offset / reopen overlaps / no animation (#1158)
Docking a modal to a window edge pushes the chat aside (body padding via
right-dock-active + --right-dock-w). Three problems on close/reopen:

1. Chat stayed offset after closing a docked modal. The close-watcher only
   reacted to the `.hidden` class or DOM removal, but the draggable modals
   (calendar, plan, workspace, document, …) close via inline `display:none`.
   Watch the `style` attribute too and treat `display:none` as closed.

2. Reopening a previously-docked singleton modal floated it off to the side,
   overlapping the chat. The reused element kept its docked inline geometry.
   Clear the content's inline position/size on close so it reopens at its CSS
   default (centered).

3. Undock wasn't animated. The transition lived on `.right/left-dock-active`,
   so removing the class dropped the transition with it and padding snapped to
   0. Move the transition to the base `body` so the push animates both ways.

Files: static/js/modalSnap.js, static/style.css.
Checks: node --check static/js/modalSnap.js; verified in-browser (dock → close
→ chat animates back; reopen → centered, no overlap).
2026-06-02 22:38:20 +09:00
Robin Fröhlich
096468a29f fix: persist and display multimodal messages (image/audio attachments) (#1159)
Multimodal content (list of {type, text/image_url} blocks) couldn't be
stored in the DB Text column, causing silent persist failures. On reload
the frontend fell back to String() on the array, rendering
[object Object],[object Object] in the chat.

- Serialize list content as JSON in _persist_message()
- Deserialize back to list in _db_to_session() via _parse_msg_content()
- Extract text parts from multimodal arrays in sessions.js instead of
  String() coercion
2026-06-02 22:37:48 +09:00
3ASiC
521848da75 fix(ui): don't submit chat message on Enter during IME composition (#1091)
CJK and other IME users confirm a candidate from the input-method popup by pressing Enter. The chat composer and the in-place message editor each bind a keydown handler that treats Enter (without Shift) as "submit", but they did not exclude the composition state. Pressing Enter to accept an IME candidate therefore sent the half-composed text (e.g. a stray "ce's") instead of just confirming the candidate.

These textareas intentionally hijack Enter to submit (Enter sends, Shift+Enter inserts a newline), which bypasses the browser's native form submission and the IME guard that comes with it, so the guard has to be re-added explicitly.

Add '&& !e.isComposing' to the three Enter-to-submit handlers: static/app.js (the main composer's button-submit path and its send/new-chat path) and static/js/chat.js (the editor for an already-sent message). Normal Enter (isComposing false) still submits; Shift+Enter still inserts a newline.

Tested: node --check on both files; manually verified with a Chinese IME that pressing Enter to pick a candidate no longer sends, and a message is sent only after composition ends.
2026-06-02 22:32:50 +09:00
red person
5029c8570e Chat: prefer active model for new desktop chats 2026-06-02 21:00:50 +09:00
Robin Fröhlich
3c6ae3713e Models: add Z.AI coding endpoint and GLM vision detection 2026-06-02 20:59:17 +09:00
Nikita Rozanov
119075f368 Research: add configurable run timeout
Surfaces the research_run_timeout_seconds setting (added in #783) in
Settings → Research as a "Max Time" field, and lets 0 disable the
wall-clock cap entirely for long deep-research runs.

- settings.py: document that 0 disables the cap; default stays 1800s.
- research_handler.py: resolve 0 (or negative) to no timeout
  (asyncio.wait_for timeout=None); other values stay bounded to
  [60, 86400] as before.
- index.html / settings.js: "Max Time" input bound to
  research_run_timeout_seconds, validated to {0} ∪ [60, 86400], with
  copy making explicit that 0 = no limit (unbounded model/API cost).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:57:57 +09:00
red person
d0c925f6c8 Chat attachments: allow picker to choose any file type 2026-06-02 20:55:30 +09:00
Kenny Van de Maele
cfb7ec1c71 Accessibility: add labels and toggle states
* Accessibility: ARIA labels and toggle states

Screen readers couldn't name several icon-only controls or tell whether the
tool toggles were on. This adds accessible names and exposes toggle state,
with no behavior or layout change.

- Icon-only buttons get aria-label: web/shell tool toggles, the "more tools"
  overflow button (+ aria-haspopup), and the color-reset buttons.
- Unlabeled inputs/selects get aria-label: memory + skills search, model-picker
  search, memory sort, theme font/density selects, and the new-memory / skill
  (title, when-to-use, how, tags) fields, which only had a visual floating label.
- Toggle state via aria-pressed, kept in sync at the existing .active write
  sites: web/shell toggles (setupToggle) and the Agent/Chat mode buttons
  (initModeToggle). Static aria-pressed added in the markup so the attribute
  exists before JS runs.

Scope: first slice of the ROADMAP accessibility pass. Focus-visible/contrast,
reduced-motion, and modal dialog roles/focus-trap are left for follow-ups.

Checks: node --check static/app.js. No Python touched.

* Accessibility: mark chat log busy while streaming

The chat log is an aria-live="polite" region, so streaming a response
token-by-token made screen readers announce every partial update — noisy and
unreadable. Set aria-busy="true" on #chat-history while a response streams and
back to "false" in the stream's finally block. Assistive tech then waits for
the settled message and announces it once.

Checks: node --check static/js/chat.js.
2026-06-02 20:55:05 +09:00
ooovenenoso
fb0f8484d7 Sessions: confirm chat delete actions
- confirm sidebar/session-list chat deletes
- confirm library chat menu deletes
- confirm archived chat permanent deletes
2026-06-02 20:43:34 +09:00
Shaw
8115cb01a2 Models: allow API keys for local endpoints
Self-hosted endpoints on a LAN are sometimes protected by an API key. The admin
"Local" add/test form only sent base_url (+ model_type), so such an endpoint
could not be added — it just errored out — even though the backend
POST /api/model-endpoints and /model-endpoints/test already accept an optional
api_key form field (the cloud "API" form already uses it).

Adds an optional masked "API key" input (adm-epLocalApiKey) to the Local form
and wires it into the local Test and Add handlers, sending api_key only when
filled (an empty value is omitted so we never send a blank Bearer). The field
is cleared after a successful add, matching the cloud form.

Tested: tests/test_local_endpoint_api_key_js.py extracts the two click handlers
and runs them under node with mocked DOM/FormData/fetch, asserting api_key is
sent when the field is filled and omitted when blank, plus that the input
exists as a password field. `node --check static/js/admin.js` passes.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:36:54 +09:00
mist
0b0be3c339 Email: recognize forwarded message dividers
`_ORIG_RE` (and its JS mirror `_TALON_ORIG_RE`) already recognised the
Japanese forward marker `転送` alongside the "Original Message" delimiters,
but not the English "Forwarded message" one. So Gmail-style forwards —
including the ones Odysseus itself emits (`---------- Forwarded message
----------`, static/js/emailInbox.js) — were not treated as a quote
boundary:

  - with a following Outlook From:/Date: header block, the divider line
    leaked into the level-0 reply bubble as noise;
  - with only the divider marking the forward (no header block), the body
    was not split into turns at all.

Add `Forwarded\s+message` to the same `[-_=]{3,}`-delimited alternation in
both the server-side parser and the JS mirror, so forward dividers are
consumed as an attribution boundary like "----- Original Message -----".
Locale variants of "Forwarded message" can follow the existing pattern.

Tests cover both manifestations plus a negative control (the bare words
"forwarded message" without `[-_=]{3,}` delimiters must not split).

Checks: python -m pytest tests/test_forwarded_message_divider.py (3 passed),
python -m py_compile src/email_thread_parser.py, node --check
static/js/emailLibrary/utils.js, git diff --check.
2026-06-02 20:32:56 +09:00
tanmayraut45
b5747e3979 Sessions: ignore list keydown while typing
The list keyboard handler (_onSessionListKeydown) treats Backspace and
Delete as "delete the focused session". When the user double-clicks a
chat to rename it, an <input class="session-rename-input"> is mounted
inside the .list-item row. Backspace on the input bubbles up to the list
container, the handler walks closest('.list-item[data-session-id]') from
e.target, finds the parent row and DELETEs the session via the API —
so a single typo correction nukes the whole conversation.

Bail out at the top of the handler when e.target is an INPUT, TEXTAREA,
or contentEditable element. Arrow / Enter / Delete navigation still
works for rows themselves (the row is the focused element then, not the
input). Mirrors the guard pattern already used in ui.js, notes.js,
tasks.js, calendar.js, emailLibrary.js and galleryEditor.js.

Closes #1007.
2026-06-02 20:30:16 +09:00
ghreprimand
431b98525b Email: persist bulk read state to provider
Co-authored-by: ghreprimand <203024559+ghreprimand@users.noreply.github.com>
2026-06-02 20:28:01 +09:00
mechramc
9d0a18a5b5 Email: add explicit SMTP security mode 2026-06-02 13:15:06 +09:00
danielxb
5268a546bc Model picker: group models by provider
Rebased on current main. Integrates with the new Recent/Favorites
system — provider groups appear below Recent and Favorites in browse
mode for large catalogs (>12 models).

Changes:
- Models grouped by canonical provider with collapsible sections
- Chevron animation consistent with sidebar sections
- Domino cascade on expand (only on just-opened group)
- Provider display names (deepseek-ai -> DeepSeek, meta -> Llama, etc.)
- Alias merging (meta + meta-llama -> one Llama group)
- Search includes provider display names for filtering
- Collapsed state persists in localStorage
- No screenshot binary committed

Co-authored-by: danielxb <5981902+danielxb@users.noreply.github.com>
2026-06-02 13:14:22 +09:00
spooky
cd4f496cb4 Fix native Cookbook quant classification 2026-06-02 13:07:20 +09:00
Stephen Yue
d46c406bd8 Fix Cookbook fit column sorting
The Fit column shared the Score column's sort key, so clicking the Fit
header sorted by Score instead of by hardware fit. There was also no
fit option in the hidden sort <select> and no fit branch in the
client-side comparator.

- Give the Fit column its own sort key (fit).
- Add a fit option to the sort select (kept Score as the default so
  first-load ordering is unchanged).
- Sort by the categorical fit_level rank
  (perfect > good > marginal > too_tight), tie-broken by score, honoring
  the ascending/descending toggle.

Fixes #842

Co-authored-by: SabixMaru <285860855+SabixMaru@users.noreply.github.com>
2026-06-02 13:05:53 +09:00
Juan Pablo Jiménez
eda99360d1 Fix Cookbook dependency install completion state
* Fix Cookbook dependency install completion state

Mark Cookbook dependency installs as complete when the background runner
exits successfully, even when HuggingFace-specific download markers are
absent.

* Add focused regression coverage for cookbook dependency completion.

Keep the fix narrowly scoped while carrying env_path through dependency tasks and locking the completion reconciliation behavior with targeted tests.
2026-06-02 12:59:29 +09:00
Boody
97528be0f4 Add custom web search result count
* fixed confusing credentials prompt

* fix(setup): return status from create_default_admin function

* fix(setup): initialize admin creation status in main function

* fix(setup): enhance admin creation feedback and status handling

* Enhance admin user login messages with conditional feedback based on creation status

* Refine admin user creation feedback messages for clarity and actionability and formatted code

* Add fallback error message for admin creation failure in setup script

* Add run script for Uvicorn with dotenv integration

* Refactor server runner to use argparse for host and port configuration

* Remove captured output print statement from server runner

* Fix server runner to ensure cross-platform compatibility and improve log handling

* Remove run.py script to match main repo

* feat: add custom option for search result count in settings

* fix: enforce minimum and maximum values for custom search result count
2026-06-02 12:55:15 +09:00
spooky
0f3280ee05 Expose advanced llama.cpp serve controls 2026-06-02 12:46:16 +09:00
Zeus-Deus
19a4f823a4 Rename Character copy to Persona
Issue #234: the "Character" tab and its "Style of response" label made it
unclear that this is where a system prompt is set. Rename the user-facing
labels for clarity:

- "Character" tab + section heading -> "Persona"
- "Style of response" -> "System prompt"
- supporting strings: select placeholder, name placeholder, button/title
  text, toasts, confirm/notice text, the chat-bar indicator tooltip, the
  settings visibility toggle, and the assistant personality picker
  ("Characters" optgroup -> "Personas").

Used "Persona" rather than the issue's suggested "Preset" because the app
already has a distinct, user-facing "Presets" concept (built-in presets
like Code Analyze/Brainstorm/Reason, shown as their own group in the
assistant picker). "Persona" matches what this tab actually creates -- a
named persona with its own memories -- without colliding with that term.

Internal identifiers (element IDs, data-chartab attributes, function names)
and the character_name backend field are intentionally left unchanged so
existing saved presets and JS wiring keep working.
2026-06-02 12:42:15 +09:00
Collin
c90a7a19a5 Add dialog accessibility semantics
Screen readers got no signal that a dialog opened — not one modal carried
role="dialog" — and several close buttons had no accessible name.

- The 6 static tool windows (Brain, Theme, Prompt, Rename session, Cookbook,
  Settings) now carry role="dialog" + an accessible name. They are dockable,
  tiling windows, so they are non-modal dialogs (intentionally no aria-modal).
- The four unlabelled close buttons (theme, prompt, cookbook, settings) get an
  aria-label so they no longer read as just "heavy multiplication x".
- styledConfirm / styledPrompt ARE blocking modals: they get role="dialog" +
  aria-modal="true" + aria-labelledby/aria-describedby, and now manage focus —
  restore focus to the triggering element on close and trap Tab within the
  dialog (they already moved focus in on open).

tests/test_dialog_aria.py pins the roles, labels, and focus management.
2026-06-02 12:41:25 +09:00
Leo
6fca7e86b7 Cookbook serve profiles and engine filter
* Cookbook: Engine filter + intelligent hardware-computed serve profiles

Two related Cookbook serving improvements for accurate, hardware-aware model
serving (especially on consumer GPUs that can only run GGUF/llama.cpp).

Engine filter
- New "Engine" dropdown (All / llama.cpp / vLLM / SGLang) beside the quant
  picker. Pure client-side view filter over the fetched list via the same
  _detectBackend() the serve commands use, so what you filter to is exactly what
  would launch. Re-renders from cache (no refetch). Empty-state message + the
  instant-cache-paint path account for it too.

Intelligent serve profiles (Quality / Balanced / Speed)
- services/hwfit/profiles.py: compute_serve_profiles() turns detected VRAM +
  model size into concrete llama.cpp flags (n_gpu_layers, n_cpu_moe, cache-type,
  context). Encodes the by-hand tuning: a too-big MoE offloads experts to CPU
  instead of failing; a model that fits stays fully on GPU; quant tracks profile
  intent; vision models keep image-encoder headroom. Reuses models.py VRAM math
  so filtering and serving agree on what fits. Pure/deterministic (no t/s claims
  — partial-offload speed isn't reliably predictable; fit is what's computed).
- /api/hwfit/profiles endpoint returns the profiles + the model's trained
  context limit, with loose name matching (strips org/ prefix, -GGUF suffix,
  quant tag) so a local GGUF folder name resolves to its catalog entry.
- _buildServeCmd (llama.cpp) now emits --n-cpu-moe / --flash-attn /
  --cache-type-k/v when set, with llama-cpp-python fallback equivalents. It
  previously only set -ngl/-c, which is why it OOM'd or ran slow.
- Serve panel: profile chips that fill the fields on click, plus CPU-MoE / KV
  Cache / Flash Attn fields. Context is clamped to the model's trained limit
  (and an absolute 1M sanity ceiling) on type/blur/profile-load and at launch —
  fixes a crash where a stale 256k/16M preset + quantized KV cache caused an
  amdgpu ErrorDeviceLost.

Tests: tests/test_serve_profiles.py (7) — offload vs full-GPU fit, never exceed
VRAM, context cap, launchable flags, vision headroom, no-GPU empty.
Checks: py_compile + node --check pass; pytest test_serve_profiles + test_hwfit_amd
green; verified live on an RDNA4 box (gfx1200) — Balanced lands ~ncm18 q4 128k,
matching hand-tuning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Cookbook: make column-header sorting discoverable (incl. Newest)

Sorting in Cookbook is via clickable column headers (pewds' design), but the
headers had no visual cue that they're interactive — so sorting in general, and
the Newest sort on the Model header specifically, was undiscoverable.

- Style sortable headers as interactive: pointer cursor, hover underline, and
  the active sort column bolded/highlighted. There was no CSS for
  .hwfit-sortable / .hwfit-sort-active at all; this helps every existing sort,
  not just Newest.
- The Model column header sorts by release_date (newest first), reusing the
  existing header-click sort wiring and the "newest" SORT_KEY.

No new sort control — uses the existing column-header paradigm.

Checks: node --check passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Cookbook serve profiles: keep the on-disk file's quant fixed (don't propose Q6/Q2)

In the Serve tab the model is a specific GGUF file already on disk, so its quant
can't change — but the profiles were suggesting "Quality · Q6_K" / "Speed · Q2_K"
as if you could re-quantize it. That's meaningless when serving a fixed file.

- compute_serve_profiles gains serve_weights_gb / serve_quant. When set (SERVE
  mode), the quant is locked to the file's and profiles differ only in the real
  serving knobs — n_cpu_moe, KV-cache type, context. _weights_gb / _cpu_moe_for_budget
  use the file's actual size instead of a quant-derived estimate. DOWNLOAD mode
  (no override) still varies the quant to show download options.
- /api/hwfit/profiles accepts serve_weights_gb & serve_quant.
- The Serve panel parses the file's size (from m.size "20.6 GB") and quant (from
  the repo/file name) and passes them, so profiles match what's actually served.

Result for a 20.6 GB Q4_K_M file: all three profiles stay Q4_K_M and differ by
KV/ctx/offload (Quality q8 KV 128k ncm21, Balanced q4 128k ncm17, Speed q4 32k
ncm15) — no nonsensical quant changes.

Tests: test_serve_mode_keeps_fixed_quant. Full serve-profile suite green (9).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Cookbook serve: Vision toggle (auto-find mmproj) + live VRAM/RAM-spillover monitor

Two serve-panel additions:

1. **Vision toggle.** A "Vision" checkbox that serves the model with its
   multimodal projector so it can read images. The mmproj path is resolved at
   runtime (find mmproj-*.gguf next to the model), so dropping an mmproj file in
   the model folder makes the toggle just work; `--mmproj … --image-max-tokens
   1024` (native) / `--clip_model_path` (llama-cpp-python) only when on + found.

2. **Live GPU-memory monitor.** A readout that polls /api/cookbook/gpus every 4s
   while the panel is open and shows VRAM used/total/%, free, and — crucially on
   a discrete card — **RAM spillover** (AMD gtt_used_mb), with a plain-language
   health hint: green/healthy, amber/tight, red/"spilled to RAM — slow (raise
   CPU MoE or lower context)". Surfaces gtt_used_mb from the gpus endpoint
   (previously read for total only and discarded for 'used').

Lets you see at a glance whether a config fits VRAM (fast) or is paging to system
RAM over PCIe (slow) instead of guessing.

Checks: node --check + py_compile pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:34:42 +09:00
spooky
8b3c0d8ad4 feat: select cached gguf artifacts for serve (#891) 2026-06-02 12:32:40 +09:00
Sirsyorrz
517aa593e0 Cookbook: clearer tooltips on saved-config badge and GPU chip (#850)
Two small polish items in the Cookbook Serve panel.

Saved-config badge
The little count badge next to the Save button ("3 ▾" etc.) had a
generic "Saved launch configs" tooltip, so the number reads like a
notification dot. Make it spell out what it is and what clicking does:
"3 saved launch configs for <model> — click ▾ to load or delete"
(and "No saved launch configs for <model> yet — click Save to add
one" when empty). Tooltip stays in sync via _updateSavedToggleLabel
so save/delete updates both the count and the hint.

GPU chip on mixed-GPU boxes (#711)
The chip label was `${gpuCount}x ${gpu_name}`, where gpu_name is
just gpus[0].name — so a 4090 + 3060 reads as "2x RTX 4090". The
backend already emits gpu_groups (identical cards grouped, used by
the serve flow to pin CUDA_VISIBLE_DEVICES) and a per-card gpus[]
array, so use them:

- Label renders each homogeneous pool: "1× RTX 4090 + 1× RTX 3060".
  Homogeneous setups keep the existing "2× RTX 4090" form.
- Tooltip lists each GPU with its index + VRAM, useful for picking
  the right device when launching.

Refs #711.
2026-06-02 12:30:24 +09:00
lolwuttav
c99193041a fix(cookbook): default Ollama serve to loopback (#872) 2026-06-02 12:27:04 +09:00
pewdiepie-archdaemon
966b53df77 Improve Cookbook serve diagnostics and recommendations 2026-06-02 12:15:47 +09:00
NovaUnboundAi
3319310942 Allow longer deep research extraction timeouts (#651)
Co-authored-by: NovaUnboundAi <NovaUnboundAi@users.noreply.github.com>
2026-06-02 11:50:03 +09:00
Achilleas90
247df16e82 Fix ordered list rendering in markdown preview (#645) 2026-06-02 11:49:44 +09:00
Rasmus
1882ad68ea fix: open #document deep-links on refresh and surface load errors (#631)
Add a hashchange handler for #document-<id> so refresh / URL-bar nav opens the document, and replace the silent console.error in loadDocument with a user-facing toast.

Closes #560
2026-06-02 11:48:54 +09:00
Christopher Milian
35ba56fa0c fix: remove ollama backend filter conflict (#613) 2026-06-02 11:48:35 +09:00
nsgds
5645cce6d0 Support vLLM 0.20.2 / NIM reasoning-parser output end-to-end (surface + agent context + render) (#602)
* fix(stream): read 'reasoning' SSE field for vLLM 0.20.2 / NIM

vLLM 0.20.2 / NVIDIA NIM emit reasoning-parser output in the `reasoning` delta field; older builds use `reasoning_content`. stream_llm() read only the latter, so reasoning from models like Nemotron-3-Nano (--reasoning-parser) was silently dropped and never rendered. Accept either field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent): keep reasoning_content only on the latest assistant turn

The agent loop echoed each round's reasoning back as `reasoning_content` on every assistant turn, assuming vendors ignore it. Nemotron's chat template re-injects ALL prior reasoning_content as <think> blocks, and the loop is trimmed only once (before it starts) — so reasoning accumulated unbounded across rounds, bloating context and feeding the model its own prior reasoning, which reinforced repetition/looping. Strip reasoning_content from earlier assistant turns so only the most recent round carries it (still satisfies DeepSeek's thinking-mode follow-up requirement).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent-ui): wrap each round's reasoning in its own <think> block

The streamed think-tag wrapper gated on whole-message substring checks (accumulated.includes('<think>')), which only ever wrapped ONE reasoning block per message. A multi-round agent response has a reasoning phase per round, so once round 1 closed its <think>...</think>, rounds 2+ reasoning was emitted unwrapped and leaked into the visible answer. Replace the substring checks with a stateful open/close flag that toggles per think/answer cycle, so each round's reasoning gets its own collapsible block. Single-turn chat is unchanged (one open, one close).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(stream): reasoning/reasoning_content delta surfaces as thinking chunk

Covers @pewdiepie-archdaemon's requested regression: a streamed {reasoning: ...} delta emits a thinking chunk while {content: ...} streams as normal content; plus the older reasoning_content field for backward compat. Mirrors the #591 scenario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:48:17 +09:00
nsgds
a857d2016d fix: don't bill self-hosted models reached by a container/service hostname (#596)
* fix(cost): treat dotless container hostnames as local (free)

getModelCost() substring-matches model names against a cloud price table, so a self-hosted 'nemotron'/'llama' model was billed at cloud rates. isLocalEndpoint() only recognized IPs / localhost / .local, not bare Docker service names (nim-nano, llamaswap), so the local-is-free guard missed them. A single-label hostname (no dot) can never be a public API -> treat as local.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cost): isLocalEndpoint classifies service names local, cloud FQDNs billable

Covers @pewdiepie-archdaemon's requested cases: llamaswap/nim-nano + localhost/private-IPs/.local => local (free); api.openai.com/openrouter.ai/etc => not local. Drives the real function via node --input-type=module (same approach as test_reply_recipients_js.py), skips when node is absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:47:58 +09:00
william-napitupulu
649cacfa05 Importing files bug (#582)
* Update Styles.css

Small update to the styles that bothered me, i noticed in the window/modal for calendar when editing a day the time icons had a mask that overlapped the icon.  I simply added 'background-image: none' prop to it/

* Importing files bug

I found a bug that wouldn't let me upload files in the library window during the documents tab, when a user selected a file, the code grabbed a reference to fileInput.files and immediately cleared the input value (fileInput.value = '') to allow for re-uploading the same file later. However, because fileInput.files is a live FileList tied directly to the DOM element, clearing the input inherently emptied our saved variable as well, resulting in lost file data.

Note this error might be browser specific as it worked fine on Zen/Firefox but failed on Edge and chrome

Fix use Array.From which copies the value into files instead of using refrences
2026-06-02 11:47:25 +09:00
Sirsyorrz
cb3d86608c Cookbook: pick the correct vLLM tool-call-parser for Qwen2.5 (#580)
The model-name detector treated every Qwen model as a Qwen3, falling
into the qwen3_xml parser:

    if (n.includes('qwen3') && n.includes('coder')) return 'qwen3_coder';
    if (n.includes('qwen')) return 'qwen3_xml';   // catches qwen2.5 too

qwen3_xml is the parser for Qwen3 reasoning/instruct models. Qwen2.5
(and Qwen2, Qwen1.5) ship with hermes-style tool calling, so the
qwen3_xml parser never recognises their tool calls — they leak through
as plain text in the assistant reply and the agent silently fails to
execute anything.

Reproduces with:
  vllm serve Qwen/Qwen2.5-Coder-14B-Instruct-AWQ ... \
    --enable-auto-tool-choice --tool-call-parser qwen3_xml
  → ask the agent to call any tool → JSON shows up in chat, no call runs.

Fix the ordering:
  qwen3 + coder → qwen3_coder
  qwen3         → qwen3_xml
  qwen          → hermes   (Qwen2.5 / Qwen2 / Qwen1.5)

Verified against the model matrix:

  Qwen2.5-Coder-14B-Instruct-AWQ → hermes
  Qwen2.5-7B-Instruct            → hermes
  Qwen3-8B                       → qwen3_xml
  Qwen3-32B                      → qwen3_xml
  Qwen3-Coder-30B-A3B            → qwen3_coder
  Qwen2-72B-Instruct             → hermes
  Qwen1.5-7B-Chat                → hermes
2026-06-02 11:47:15 +09:00
ooovenenoso
15c7cb58e7 fix(cookbook): retry 0% HF download stalls sooner (#691)
Co-authored-by: Kevin <120500656+oooindefatigable@users.noreply.github.com>
2026-06-02 11:42:59 +09:00
Afonso Coutinho
634c16a019 fix: reply-all Cc's the user's own other addresses (multi-account) (#672)
* feat: publish all configured email addresses for reply-all exclusion

* fix: exclude all of the user's own addresses from reply-all, not just the active one

* test: reply-all excludes all of the user's configured addresses
2026-06-02 11:42:20 +09:00
James Arslan
6776c7d691 Surface silent model fallback instead of masking it (#868)
When the selected model fails before producing output, stream_llm_with_fallback
quietly switches to the next candidate and the reply is shown under the
originally selected model's name, so a misconfigured provider looks like it
works. (Concretely: a Bedrock gateway that 400s every Anthropic/Claude request
appears fine because another model silently answers under the Claude label.)

Emit a `fallback` SSE event ({selected_model, answered_by, reason}) the first
time a non-primary candidate produces output, forward it through the agent loop
and both chat-route paths, stamp the response metrics with the model that
actually answered, and show a notice + relabel the reply in the UI.

Tested: python -m pytest tests/test_llm_core_fallback.py (3 pass);
python -m py_compile src/llm_core.py src/agent_loop.py routes/chat_routes.py;
node --check static/js/chat.js.
2026-06-02 11:37:25 +09:00
BarsatZulkarnine
00f16d66a3 Fix test suite: ESM module loading and stub isolation (#844)
* Fix test suite: ESM loading and stub isolation (refs #605)

Three targeted fixes to reduce suite failures from 9 → 1:

1. package.json: add "type": "module" so Node loads static/js/**
   as ES modules. Fixes 7 tests in test_compare_js.py and
   test_reply_recipients_js.py that fail with
   "SyntaxError: Unexpected token 'export'".

2. test_null_owner_gates.py: add Base and ChatMessage to the
   core.database stub. Without Base the scheduler test cannot
   import at collection time; without ChatMessage core/__init__.py
   fails mid-load when session_manager.py tries to import it,
   leaving core partially initialised in sys.modules and poisoning
   the auth manager migration test that runs later in the same file.

3. test_task_scheduler_session_delivery.py: skip gracefully when
   core.database is stubbed (Base is a MagicMock) rather than
   crashing. The test passes correctly when run in isolation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Scope ESM declaration to static/js/ and document isolation workaround

Per review feedback on #844:

1. Move "type": "module" from root package.json to static/js/package.json.
   The root package.json had no type field (defaulted to CJS) and should
   stay that way — vendored UMD bundles in static/lib/ use require() internally
   and would break if Node ever tried to load them as ES modules. Node resolves
   the nearest package.json, so adding it in static/js/ scopes the ESM
   declaration to just the files the JS unit tests actually load
   (compare/state.js, emailLibrary/replyRecipients.js).

2. Expand the module-level skip comment in test_task_scheduler_session_delivery
   to document that it is a temporary isolation workaround, explain root cause
   (test_null_owner_gates installs a module-level sys.modules stub with no
   cleanup), record before/after suite numbers, and note the clean path
   (refactor to fixture-scoped stub).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 11:29:29 +09:00
James Arslan
b3599d84f7 Fix drag-and-drop files landing behind the panes in Compare (#818)
In Compare each pane renders into a sandboxed <iframe>. A file dropped on
a pane was handled by the iframe (browser default), so the browser loaded
the file *inside* the pane — appearing 'behind' the app — instead of
attaching it. The existing #chat-container drop handler never sees the
event because drag events don't bubble out of an iframe.

While a file drag is active in Compare, raise a single full-window drop
shield above the panes/iframes so the drop lands on the parent document,
then route the files into the shared composer (the same pending-files
pipeline the file picker and paste already use). Scoped to Compare via the
.compare-active class, so normal chat and the tool dropzones (gallery, RAG,
document editor, …) are unaffected.

Verified with a headless-Chromium integration test: synthetic file
dragover raises the shield, drop attaches the file to the composer, and
non-Compare mode is unaffected. Also ran node --check static/app.js.
2026-06-02 11:14:59 +09:00
CocoLng
8e918dfdbb Ignore AltGr keystrokes in Ctrl+Alt keyboard shortcuts (#825)
* Ignore AltGr keystrokes in Ctrl+Alt keyboard shortcuts

Browsers report AltGr (right Alt on AZERTY/QWERTZ and most non-US
layouts, used to type @ # { } [ ] | \ and the euro sign) as
ctrlKey+altKey. The default keybinds map destructive actions to
Ctrl+Alt+<letter> (delete_session, new_session, incognito,
open_calendar), so a non-US user typing a special character could
silently fire them.

Guard the shortcut matcher, the editor keydown handler, and the rebind
capture with getModifierState('AltGraph'), which is true for AltGr but
false for a genuine left Ctrl+Alt. macOS is excluded: there the Option
key legitimately sets AltGraph and there is no AltGr/Ctrl+Alt collision
to guard against, so the guard would otherwise break Ctrl+Option /
Cmd+Option shortcuts (notably in Firefox).

The detection lives in one place — isAltGrEvent / IS_MAC in
static/js/platform.js — and all three call sites route through it, so the
guards can't drift apart.

The editor handler only skips the Ctrl+Alt chord block, so layout
shortcuts reachable via AltGr (e.g. [ ] brush size = AltGr+5/+8 on
AZERTY) keep working.

* Require Ctrl+Alt for the AltGr guard and consolidate keybind test marks

isAltGrEvent now also checks ctrlKey+altKey so it only suppresses the
"AltGr reported as Ctrl+Alt" collision; an event asserting AltGraph on
its own (a Linux ISO_Level3_Shift layout, a stray modifier) is left
alone. Pin it with test_isaltgr_false_when_altgraph_set_but_not_ctrl_alt.

Collapse the 12 per-test node skipif marks into one module-level
pytestmark, and note in platform.js why IS_MAC intentionally covers
iPad/iPhone and mirrors the isMac checks in calendar.js / sessions.js.
2026-06-02 11:12:54 +09:00
Tatlatat
aba15e7b6d fix(cookbook): sort by Fit when the Fit header is clicked (#842) (#860)
The Cookbook Scan/Download (hwfit) table gave the Fit column key:'score', so
clicking the Fit header sorted by score instead of by fit. Give the Fit column
its own 'fit' sort key, add a matching option to the #hwfit-sort select, and
rank fit_level (perfect > good > marginal > too_tight > no_fit) in the
client-side sort. Default puts the best fit first; clicking again reverses it.
Score still sorts by score.

Closes #842
2026-06-02 11:09:18 +09:00
pewdiepie-archdaemon
96618b01c0 Polish task UI slash commands and Ollama serving 2026-06-02 09:36:03 +09:00