Commit graph

19 commits

Author SHA1 Message Date
Etherll
9eb0778628 Merge remote-tracking branch 'origin/main' into feature/rag
# Conflicts:
#	studio/backend/core/inference/llama_cpp.py
#	studio/backend/routes/__init__.py
#	studio/backend/routes/inference.py
#	studio/frontend/package.json
#	studio/frontend/src/components/assistant-ui/sources.tsx
#	studio/frontend/src/components/assistant-ui/thread.tsx
#	studio/frontend/src/features/chat/api/chat-adapter.ts
#	studio/frontend/src/features/chat/chat-settings-sheet.tsx
#	studio/frontend/src/features/chat/shared-composer.tsx
#	studio/frontend/src/features/chat/stores/chat-runtime-store.ts
#	studio/frontend/src/features/settings/settings-dialog.tsx
2026-05-28 00:38:58 +03:00
Roland Tannous
86b52503dd Studio: trim verbose comments/docstrings across RAG code 2026-05-26 13:51:11 +04:00
Daniel Han
cc68720385
Studio: surface external-provider cache hits and writes in context bar (#5736)
* Studio: surface external-provider cache hits and writes in context bar

The Anthropic / OpenAI Responses streaming paths already emit an
include_usage-style SSE chunk carrying prompt_tokens_details.cached_tokens
and cache_creation_input_tokens / cache_read_input_tokens (see
_build_usage_chunk in external_provider.py), but the chat-adapter only
read the local llama-server timings.cache_n field. As a result, the
context-usage tooltip never showed cache hits or writes for external
providers, even though the backend was computing them.

Read the external usage envelope as a fallback when timings.cache_n is
absent, and surface Anthropic cache_creation_input_tokens as a separate
"Cache writes" line in the tooltip so users can tell a cache miss from a
cache hit on a turn that both reads and writes the cache.

- ServerUsage gains optional prompt_tokens_details.cached_tokens,
  cache_creation_input_tokens, cache_read_input_tokens.
- contextUsage store entry gains optional cacheWriteTokens.
- ContextUsageBar gains optional cacheWrites tooltip line.
- chat-page wires both fields through to the bar.

* Studio: render cache stats for external providers too

Reviewer round on the original PR caught three asymmetric-fix sites
where the producer side surfaced external prompt-cache stats but the
consumer side still gated on ggufContextLength (which is only ever set
for the local llama-server runtime). Result: the entire cache-stats
PR shipped invisible for Anthropic / OpenAI Responses / Gemini, which
is exactly the set of providers it was added for.

- chat-page.tsx: drop the ggufContextLength precondition on the
  ContextUsageBar mount. The bar already tracks usage; let it decide
  what to render based on what it knows.
- context-usage-bar.tsx: make `total` optional. When absent, drop the
  "/ total" ratio + percentage progress bar + "approaching limit"
  helper, and just show per-turn counters + cache stats. Bootstrap
  guard tightened so an all-zero, all-undefined state still renders
  nothing.
- runtime-provider.tsx: external-provider rehydration was rejected by
  the `store.ggufContextLength` check. Keep the "fits inside window"
  sanity check when a local context window IS known, drop it when
  it isn't.
- message-timing.tsx: the per-message timing popover used a separate
  "Cache hits" code path that only read llama-server's timings.cache_n.
  Fall through to custom.contextUsage for external providers, and add
  a parallel "Cache writes" line for Anthropic cache_creation events.

* Studio: tighten cache-stats comments

* Scope contextUsage to active checkpoint

Three follow-ups on #5736 so the relaxed external-provider render
gate does not show stale token / cache stats from a different model:

1) setCheckpoint now clears contextUsage on a real checkpoint
   change. setActiveThreadId and clearCheckpoint already did this;
   the most-traveled transition path (the user switching models from
   the picker) leaked the prior turn's counts because they were never
   cleared.

2) The external-selection branch in chat-page.tsx now also clears
   contextUsage at the same time it nulls ggufContextLength /
   activeNativePathToken. Without this an in-session switch from a
   local model to an external provider would visibly carry the
   previous local turn's counters into the new provider's bar.

3) exitCompare's rehydration is now scoped: restore the saved
   usage only when the message's modelId matches the active
   checkpoint AND, for local turns where a context window is known,
   when the saved total fits inside that window. Without this the
   bar could render a stale local-model usage on top of an external
   provider, or an oversized usage object that exceeds the now-
   active window.

Typecheck clean.

* Plug remaining stale-contextUsage paths

Follow-up to 042e0ac4 that catches four asymmetric-fix sites the
checkpoint-scoping pass missed:

1) setParams now also clears contextUsage on a real checkpoint
   change. The local model load path in use-chat-model-runtime calls
   setParams(mergeBackendRecommendedInference(...)) which mutates
   params.checkpoint before refresh() eventually fires setCheckpoint;
   the intermediate window rendered the previous model's counters
   under the new checkpoint.

2) chat-adapter.ts setContextUsage on stream completion now gates on
   the captured params.checkpoint still being active. A late
   completion from provider A used to clobber the context bar after
   the user switched to provider B mid-stream.

3) chat-page.tsx exitCompare rehydration no longer accepts a saved
   modelId-stamped usage when the active checkpoint is empty. A user
   who entered compare, cleared the model, and exited compare would
   otherwise see the cleared model's stats reappear.

4) runtime-provider.tsx thread-load no longer restores legacy
   unscoped usage (no modelId) unless a local context window is
   known. With the relaxed external-provider render gate, old
   pre-PR persisted messages without a modelId stamp could attach
   their counts to an unrelated active provider.

Also switches message-timing.tsx cache-hit fallback from || to ??
so an explicit cache_n=0 is not replaced by a stale cachedTokens.

Typecheck clean.

* Shorten cache-stats comments for PR #5736
2026-05-25 23:37:04 -07:00
Roland Tannous
30856de739 Studio: route in-thread doc uploads through RAG ingest
When ragToolEnabled is on, the in-thread composer's + button now
opens a doc-only picker and uploads selected files to the per-thread
RAG pipeline (matching SharedComposer). Pending chips show
Uploading -> Indexing -> Ready status; Send is blocked while any
doc is in-flight. Previously these files were base64'd inline as
native attachments, which bypassed RAG entirely.
2026-05-24 16:13:05 +04:00
Lee Jackson
61ed4cac51
Studio: persist chat history in backend storage (#5272)
* feat: Persist chat history in backend storage

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Address chat tombstone batching review

* fix: update desktop auth routes stub

* chat db settings storage

* chat db settings routes

* chat db settings client

* chat db settings store

* chat db settings wiring

* chat db history storage

* chat db settings migration

* chat db settings fallback

* chat db container metadata

* chat db legacy migration fixes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chat ci auth background reads

* chat auth storage fixes

* chat migration final fixes

* chat export batch message lookup

* chat history review fixes

* chat prune sync fix

* chat settings hydration retry

* gate settings persistence

* Scope chat-history rows by subject; fix hijack, clear-confirm, hydrate race

Backend storage and routes:
- chat_threads / chat_messages / chat_settings carry a NOT NULL subject
  column with composite PRIMARY KEY (id, subject). Two authenticated
  identities can no longer see or wipe each other's data.
- Pre-existing rows on an existing studio.db migrate under sentinel
  subject __legacy_unscoped__ via rename + rebuild + copy; single-user
  installs see no behavior change.
- ON CONFLICT(id, subject) DO UPDATE ... WHERE chat_messages.thread_id =
  excluded.thread_id refuses cross-thread re-parenting via upsert.
  upsert_chat_message + sync_chat_messages now raise
  ChatMessageThreadMismatch which the routes map to HTTP 409.
- replace_thread_messages rejects body messages whose threadId does not
  match the URL thread (HTTP 400) instead of silently rewriting them.
- DELETE /api/chat requires ?confirm=true, returns row count, logs the
  subject and count.
- upsert_chat_settings_merge does read + deep-merge + write inside a
  single BEGIN IMMEDIATE so concurrent writers no longer drop each
  other's updates. The route delegates to this helper.
- New POST /api/chat/messages:batch returns {thread_id -> messages[]}
  for many threads in one HTTP call. Subject-scoped. Unknown ids return
  empty lists instead of 404 so the sidebar/search caller can rebuild
  atomically.

Frontend:
- chat-runtime-store: hydrate-failure catch sets settingsHydrated:true
  so a transient backend blip no longer permanently disables
  persistence. setParams bumps inferenceParamMutationVersions
  unconditionally so a slow hydration response cannot clobber a
  pre-hydrate user edit. saveSettingsPatch replaces the serial chain
  with a debounced pendingPatch + deep merge; flush on beforeunload.
- chat-history-storage: clearStoredChats returns ClearStoredChatsResult
  distinguishing backend / legacy / both outcomes.
  listStoredChatThreadsWithMessages uses the batched fetch (one HTTP
  call) instead of Promise.all per-thread; legacy Dexie fallback only
  fires when the batch result is empty.
- chat-api: batchListChatMessages with graceful 404 / 405 fallback to
  per-thread listChatMessages for older servers.
- chat-thread-tombstones: store {id, deletedAt} tuples with 90-day GC
  and a 5000-entry cap so localStorage stays bounded. Back-compat reads
  pre-fix plain strings. Adds removeChatThreadTombstones (rollback) and
  clearAllChatThreadTombstones (post-legacy-purge clean-up).
- use-chat-sidebar-items: deleteChatItem tombstones synchronously
  BEFORE the backend round-trip and rolls back on failure (restores
  pre-PR optimistic UX). 300 ms trailing debounce on
  CHAT_HISTORY_UPDATED_EVENT plus requestSeq guard so stream-time event
  bursts produce at most one fetch per quiet window.

Tests:
- studio/backend/tests/pr5272_sim/ adds 64 regression tests covering
  schema migration from pre-fix shape, subject scoping, cross-thread
  hijack, bulk-replace mismatch, clear-confirm, concurrent settings,
  unicode + 2MB content + SQL-injection-safe binding, chunking
  boundary at 900 and 901 ids, batched endpoint (multi-subject + 1200
  ids + per-thread order), and grep contracts for the frontend patches.
  test_chat_history_storage.py updated to pass subject.

Verified locally on Linux + macOS + Windows GitHub Actions runners
(staging fork): 64 pass + 2 from the PR's own backend test on all
three OSes.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Drop subject scoping and clear-confirm gate (Studio is single-user)

Per maintainer feedback: subject scoping, cross-thread message hijack
guard, and DELETE /api/chat ?confirm=true gate are unnecessary because
Studio is intentionally single-user (the client already shows a confirm
dialog before clear-all).

This commit reverts those backend changes and keeps only the
non-multi-user pieces from the earlier fix commit:

- studio_db.py: restored to pre-fix shape; adds upsert_chat_settings_merge
  which does atomic read + deep-merge + write under BEGIN IMMEDIATE so
  two concurrent slider drags cannot drop one another's updates.
- routes/chat_history.py: restored; put_settings now calls the atomic
  merge instead of doing the read-merge-write across three separate
  connections. Adds POST /api/chat/messages:batch to collapse the
  sidebar/search rebuild from N round-trips to 1.
- frontend/api/chat-api.ts: align batchListChatMessages request and
  response keys with the backend (threadIds / messagesByThreadId).
- tests/test_chat_history_storage.py: add atomic-merge concurrency test,
  deep-merge nested-key test, and 901-id chunking-boundary test.
- Drop the pr5272_sim test directory (those tests covered the reverted
  subject-scoping/hijack/confirm behavior).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix sidebar delete crash, keepalive on settings beforeunload flush, search rebuild race

Two correctness bugs and one perf race surfaced by a fresh code review of
the prior fix commit:

- chat-api.ts: notifyChatHistoryUpdated was declared as a non-exported
  function, but use-chat-sidebar-items.ts imports it. The import would
  fail tsc with TS2305 and at runtime the optimistic-delete and
  delete-failure rollback paths would both throw.
- chat-runtime-store.ts + chat-settings-api.ts + chat-settings-storage.ts:
  the beforeunload settings flush is now actually keepalive. Without it
  the browser cancels the in-flight PUT on tab close, so the last slider
  drag is silently dropped (which is exactly the case the
  debounce+beforeunload combination was meant to protect against).
- use-chat-search-index.ts: rebuilds now coalesce with a 300ms trailing
  debounce and discard out-of-order responses via a requestSeq guard.
  Matches the sibling pattern in use-chat-sidebar-items.ts so two rapid
  CHAT_HISTORY_UPDATED_EVENTs (run-start + run-end save during a turn)
  cannot land with stale data winning.
- chat-thread-tombstones.ts: drop dead clearAllChatThreadTombstones with
  no call sites; Dexie is never wiped so the function has no use.

* fix(studio): protect chat persistence writes

* fix(studio): align chat history clear semantics

* fix(studio): show partial chat clear feedback

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): preserve chat persistence fallbacks

* fix(studio): harden chat thread persistence checks

* Preserve chat message timestamps

* Gate chat stream on history save

* Make chat thread backfill best effort

* Avoid chat message 404 probe

* Tighten chat legacy fallbacks

* chat: server-side ledger so legacy Dexie import is recoverable

The boolean localStorage sentinel
(unsloth_chat_legacy_imported_to_studio_db) made importLegacyChatsIfNeeded
non-recoverable: deleting studio.db while the browser keeps the flag
silently hides every legacy Dexie thread from the sidebar (verified by
the 3-GPU validation probe; matches the third review comment on PR
#5272). Same trap fires for browser-profile sync to a fresh machine
and any other path that wipes studio.db while keeping IndexedDB.

Source of truth moves into studio.db itself via a new
chat_legacy_import_log table keyed by legacy thread id. The ledger
disappears together with studio.db, so the next launch re-runs the
import from whatever Dexie still holds. localStorage stays as a
per-session perf hint only.

Performance, all bounded by the three new fast-paths before any
backend work:

  A) localStorage hint says "imported earlier in this session" -- 0
     network, ~0 ms. Covers the warm sidebar mount.

  B) indexedDB.databases() reports no "unsloth-chat" DB -- 0 network,
     ~1 ms. Covers every new user who never had the old browser-only
     Studio (the common case after launch).

  C) db.threads.count() + db.messages.count() are both 0 -- 0 network,
     ~5 ms. Covers returning users who migrated long ago and Dexie was
     never repopulated.

Only when all three miss does the code talk to the backend
(GET /api/chat/import-ledger -> diff vs Dexie -> existing import path
-> POST /api/chat/import-ledger to record what was just imported).
Per-thread tracking is enough because Dexie is read-only after this
PR; a thread's message set does not grow.

Backend deployments that predate the import-ledger routes are
handled transparently: the client treats 404/405 as an empty ledger
and re-runs the (idempotent via UPSERT) import on next launch.

Changes:
- storage/studio_db.py: new chat_legacy_import_log table (WITHOUT
  ROWID, PK on legacy_thread_id) + list_chat_legacy_import_log() +
  record_chat_legacy_import_log() (idempotent batch UPSERT).
- routes/chat_history.py: GET + POST /api/chat/import-ledger with the
  obvious request/response models.
- frontend api/chat-api.ts: listChatImportLedger() (returns a Set for
  O(1) diff) + recordChatImportLedger(), both with 404/405 fallback.
- frontend utils/chat-history-storage.ts: importLegacyChatsIfNeeded
  gains three fast-paths, ledger fetch on the slow path, and writes
  the ledger after a successful import. The localStorage helper is
  unchanged on the surface; it just stops being authoritative.
- tests: 5 new test_legacy_import_log_* cases (empty default, record
  + list round-trip, idempotency, input dedup, empty/null ignore).
  All 9 pre-existing tests still pass.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Make the legacy-import recovery actually recoverable

The previous commit added a server-side ledger to make Dexie -> studio.db
import recoverable after a studio.db wipe, but the localStorage perf hint
still short-circuited the import gate before the ledger was ever consulted.
After a wipe, the hint stayed "true" and the bulk re-import never ran -- the
ledger sat empty and only the per-thread lazy materialize-on-continue path
restored data.

Changes:

- Remove the localStorage short-circuit from importLegacyChatsIfNeeded so
  the ledger is checked on every fresh tab. legacyChatImportPromise keeps
  the per-session cache; the hint now only matters for the listing paths.
- Batch the slow path: one db.messages.where().anyOf().toArray() and one
  batchListChatMessages() instead of 2N round-trips. At 1k threads this
  drops a multi-second blocking import to a single request pair.
- recordChatImportLedger returns {accepted, inserted, supported}. The
  localStorage hint is only flipped when supported is true, so old
  backends (404 / 405 / 501) no longer permanently poison recovery.
- Ledger backfill: threads already present in chat_threads but missing
  from the ledger now get added too, so old-FE-then-new-FE deployments
  don't redo the diff every launch.
- Backend response field renamed recorded -> {accepted, inserted}.
  accepted is the deduped non-empty input count; inserted is the rows
  actually new (via INSERT ... RETURNING). Bounded by Field(max_length=
  10_000) on the request payload.
- Storage helpers renamed: chat_legacy_import_log -> chat_legacy_imports,
  record_* -> upsert_* to match the existing noun/verb conventions.
- DEXIE_DB_NAME exported from db.ts; duplicate constant in
  chat-history-storage.ts removed.
- 3 new route-level tests for /api/chat/import-ledger covering the
  round-trip, the (accepted, inserted) split, and the 10k payload cap.

All 18 chat-history tests pass.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: shine1i <wasimysdev@gmail.com>
Co-authored-by: danielhanchen <michaelhan2050@gmail.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-05-22 06:18:05 -07:00
Michael Han
309af17366
studio/frontend: swap Hugeicons spokes spinner for CSS ring (#5531) 2026-05-18 03:46:47 -07:00
Lee Jackson
bc28a5d80e
Studio: code execution config visual polish (#5471)
* style: polish code execution

* studio/chat: optimistic insert for created OpenAI containers

Container creation now prepends the row immediately with a "Creating"
pill instead of waiting on /v1/containers, which is eventually
consistent and can lag the create response by several seconds.
A 5s follow-up refresh reconciles with the server view.

---------

Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: Roland Tannous <rolandtannous@gravityq.ai>
2026-05-16 21:14:01 +04:00
Lee Jackson
ba0cae1aff
Stop: drop Ollama API key, clean up code execution UI (#5464)
* chat: drop Ollama API key, clean up code execution UI

* studio/chat: fix undefined candidateId + keyboard a11y on container list

- Auto-bind effect referenced `candidateId`, which is not declared in
  this scope (only `candidate` is) — would fail the TS/Next build.
  Use `candidate.id` to match the variable that's actually defined.
- Container list items get `role="button"` when `canActivate` is true
  but had no keyboard activation. Add `onKeyDown` for Enter/Space and
  `tabIndex={0}` so the row is focusable and activatable from the
  keyboard, matching the existing onClick behavior.

* studio/chat: restore declarations dropped by the main merge

The 75646444d auto-merge with main (#5466) silently dropped the
declarations a4f19171c added in regions #5466 also rewrote, while
leaving the usages further down in the file. No textual conflict
markers, but the result referenced undeclared names:

- REFRESH_POLL_MS constant (drives the 30s list refresh interval).
- pendingDelete / setPendingDelete / deleting / setDeleting state
  (drives the in-sheet AlertDialog delete confirm — replaces the
  window.confirm() that landed via #5466).
- Per-row locals inside the container list .map callback: running,
  isActive (recomputed with running), ttlMinutes, canActivate,
  statusLabel (drive click-to-activate, expired/active badges, and
  the muted styling for expired containers).

Also wire setDeleting(false) + setPendingDelete(null) into the
confirmDelete finally so the AlertDialog closes after the delete
call resolves; previously the busy state never cleared.

The all-containers list now iterates sortedContainers (matches the
picker above and the "newest-active first" UX) instead of the
unsorted visibleContainers.

---------

Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: Roland Tannous <rolandtannous@gravityq.ai>
2026-05-16 02:17:03 +04:00
Roland Tannous
a70bf02bb8
studio/chat: OpenAI container picker delete reliability (#5466)
* studio/chat: fix OpenAI container delete UX (expired filter, TTL cap, idempotent 404, refresh-on-error)

- Filter status="expired" from /containers/list so the picker only
  shows usable containers. OpenAI keeps expired entries in the list
  indefinitely, which made delete look broken.
- Cap ttl_minutes at 20 (backend Field + frontend TTL_MAX + persistence
  clamp). OpenAI's actual hard limit is 20; the prior 10080 cap caused
  integer_above_max_value rejections on create.
- Treat 404 on delete as idempotent success in the frontend client so
  already-gone containers don't surface a scary error toast.
- Run refresh() in finally for onCreate/onDelete so the picker stays
  in sync with OpenAI even when the call errors.
- Add route-level test for the expired filter.

* studio/chat: add diagnostic logging for OpenAI /containers DELETE

Trace what arrives at /external/openai/containers/delete (subject,
container_id, base_url) and what we send to OpenAI (URL, presence
of Authorization, value of OpenAI-Beta) plus the full response
status + body (capped at 300 chars). Helps confirm whether the
beta header is on the wire and whether OpenAI's response actually
reports deleted=true, when users report the delete "not taking".

No secrets are logged — Authorization is reported as a boolean.

* studio/chat: log raw /containers list response from OpenAI

Sibling to the delete diagnostics. After a confirmed delete
(deleted=true on the wire), we want to see whether the very next
list call returns the just-deleted id — that distinguishes
"OpenAI eventually-consistent list" from "frontend stale state".
Logs each entry's id + status only; no names, no timestamps.

* studio/chat: fingerprint decrypted API key for container CRUD

Logs kind (sk-proj-/sk-/other), length, and last-4 chars only —
never the full secret. Lets us compare what the backend actually
uses against the key the user expects, since the same DELETE
request shape can produce different results across keys
(project-scoped containers: list is permissive but delete requires
the owning project's key).

* studio/chat: use fresh httpx client for /v1/containers DELETE

Same key, same headers, same URL via the shared _http_client
returned deleted=true but the container persisted in subsequent
list calls. A fresh httpx.AsyncClient with the identical request
shape (verified with a standalone reproducer) deleted the same
container cleanly. Suspect connection-pool state from earlier
chat-completion streams interferes at the edge — switching to a
per-call client side-steps it entirely. Scoped to delete only;
list/create keep using the shared pool until we can confirm the
same fix is needed there.

* studio/chat: log OpenAI response headers on container DELETE

Adds cf-ray / x-request-id / openai-organization / openai-project /
openai-processing-ms to the delete-response diagnostic line. Lets
us cross-reference a failing delete against OpenAI support (or
against a working standalone reproducer) using the unique
request-id and edge node.

* studio/chat: client-side tombstone for just-deleted OpenAI containers

OpenAI's /v1/containers DELETE returns {"deleted": true} but the
list endpoint can keep returning the same container for several
minutes (replica lag or in-use silent no-op — undocumented per
developers.openai.com/api/docs/guides/tools-shell). Our backend
sends the correct DELETE with OpenAI-Beta: containers=v1 and a
standalone reproducer shows the same behavior, so the right fix
is UI-side rather than waiting on OpenAI.

After a successful delete, the id goes into a per-component
tombstone map with a 5-minute expiry. visibleContainers (now the
single chokepoint feeding sortedContainers, auto-bind, and the
all-containers list) filters those ids out. A 30s sweep clears
expired tombstones so the picker recovers automatically if OpenAI
eventually catches up (or the container's TTL elapses).

* studio/chat: tombstones live for the page lifetime; drop API key fingerprint log

- Tombstones change from Map<id, expiry> to Set<id>: once tombstoned,
  the id stays hidden from the picker until page reload. OpenAI's list
  can keep returning a deleted id for an undocumented and variable
  amount of time; automatically un-tombstoning after a fixed window
  surfaces it again and creates more confusion than it solves. The
  container's own TTL eventually expires the entry on OpenAI's side,
  and the expired-status filter at the backend list route hides it
  anyway.
- Remove the periodic sweep effect (dead code without expiries).
- Remove the api-key fingerprint log added during debugging — it
  served its purpose (confirmed parity) and isn't needed long-term.
2026-05-16 01:53:13 +04:00
Roland Tannous
2622b79606
studio/chat: built-in code execution for OpenAI + Anthropic (#5461)
* studio/chat: built-in code execution for Anthropic Claude 4.x

Wire Anthropic's server-side code_execution_20250825 tool to the
existing Code pill in the composer. Pill lights up only for Claude
Opus/Sonnet/Haiku 4.x models that the docs list as compatible; pairs
independently with Search. Backend appends the tool entry plus the
code-execution-2025-08-25 beta header, and translates the SSE
server_tool_use / *_tool_result blocks (bash + text_editor sub-tools)
into the _toolEvent shape the frontend renderer consumes. File
uploads via the Files API are a deliberate follow-up.

* studio/chat: enable code execution pill in in-thread composer too

thread.tsx renders its own composer with a separate CodeToolsToggle
that was still gated on supportsTools only, so the pill stayed
disabled inside an active thread even after picking Anthropic 4.x.
Surface the capability through the runtime store
(supportsBuiltinCodeExecution, set from chat-page alongside
supportsBuiltinWebSearch) and read it in the toggle.

* studio/chat: built-in code execution for OpenAI cloud gpt-5.5

Extend the Code pill to OpenAI cloud's gpt-5.5 / gpt-5.5-pro via the
shell tool on /v1/responses. Per-thread container reuse: capture the
container_id from each response on a synthetic container_ready event,
persist it onto the ThreadRecord, and pass it back as
environment.type="container_reference" on follow-up turns so the
model sees filesystem state from prior turns until OpenAI's idle
expiry. Stale ids surface a container_invalidated event that clears
the thread record so the next turn falls back to container_auto.

Gated strictly on OpenAI cloud (api.openai.com base URL) — Ollama,
llama.cpp, vLLM, and custom OpenAI-compat presets won't see the
shell tool entry even when their providerType collapses to "openai".

* studio/chat: OpenAI shell-tool container management UI

Side-panel section (settings sheet → Code Execution) for managing
OpenAI's shell-tool containers per thread. Three controls:

- New-container idle timeout (provider-level default, pre-fills the
  create dialog and is used by the lazy-create path on a thread's
  first turn when set to a non-default value).
- Active container picker for the active thread — pick any existing
  container or stay on "Auto-create per thread".
- Inline create form (name + idle TTL) and per-row delete actions.

Three new backend endpoints under /api/inference/external/openai/
containers/{list,create,delete} proxy to OpenAI /v1/containers using
the encrypted API key. All three reject non-cloud base URLs up front
so the picker stays scoped to api.openai.com.

Deleting a container clears all thread bindings pointing at it; the
next turn falls back to auto-create.

* studio/chat: inherit container across threads + styled active picker

New threads on the same OpenAI provider now default to the most
recently used container instead of "Auto-create per thread" — both
in the chat-adapter (so a send works even if the side panel was
never opened) and in the side panel itself (auto-binds the active
thread when the dropdown loads on a thread that has no container).

Picker is visually emphasized with an accent panel and the
currently-active row in the list below is highlighted with the same
accent so the two views stay in sync.

* studio/chat: friendly English-word names for auto-created containers

Replaces the "chat-<thread-id-slug>" auto-name with a random
English-word + short hex suffix (e.g. "kestrel-3f9c"). Applies only
to the chat-adapter's lazy-create path; the OpenAI container_auto
path stays unnamed (only fires when no custom TTL is set).

* studio/chat: always pre-create OpenAI containers via frontend

Drops the TTL-based gate on the chat-adapter's lazy-create path so
every code-execution container the user ever sees in the picker has
a friendly English-word name. The backend's container_auto fallback
stays as a safety net (used only if the POST /v1/containers call
fails); in practice that branch should be rare.

* studio/chat: send OpenAI-Beta header for /v1/containers CRUD

Without OpenAI-Beta: containers=v1, OpenAI returns 200
{"deleted": true} for DELETE /v1/containers/{id} but does not
actually remove the container. The list call then keeps returning it,
making it look like Studio's "Delete container" button is broken.

Verified 2026-05-15 against api.openai.com: DELETE with the beta
header returns 200 and removes the container; the same DELETE without
the header returns the same 200 deleted:true body but the container
stays alive.

- Add _container_headers() that merges OpenAI-Beta on top of the
  shared auth headers; route list / create / delete through it.
- Verify the DELETE response body reports {"deleted": true}; raise
  httpx.HTTPError otherwise so the route surfaces a 5xx instead of
  silently reporting success on a silent no-op.
- Add tests covering header propagation and the deleted-flag guard
  (true, false, missing key, non-JSON body, 4xx passthrough).

* studio/chat: surface unpersisted-thread picker no-op as a toast

The "Active for this thread" container picker uses
db.threads.update(activeThreadId, ...), which silently returns 0 rows
affected when the thread record isn't yet in IndexedDB. That happens
on a brand-new thread where the user toggles code execution on and
opens settings before sending the first message — the chat adapter
only materializes the thread row on first send. The picker would
appear to ignore the user's selection and snap back to "Auto-create
per thread".

- onPick now awaits the update and toasts an actionable hint
  ("Send a message first to pin a container to this thread.") when
  the update affected zero rows.
- Auto-bind effect comment clarifies why it stays best-effort silent.

The auto-bind effect itself is unchanged: it's a heuristic that
should not nag the user when it can't apply.

* studio/chat: let user pick OpenAI container before first send

Previously the picker silently no-op'd until the user sent the first
message, because Dexie's ThreadRecord is only materialized inside the
runtime-provider's `initialize` hook (assistant-ui's first-message
callback). That kept users from binding a thread to an existing
OpenAI container up front; they had to either send a message and
risk the chat adapter auto-creating one, or accept the cross-thread
inheritance default.

- Export `ensureThreadRecord` from runtime-provider so other surfaces
  can materialize the row idempotently.
- In OpenAICodeExecSection.onPick, await ensureThreadRecord before
  the update, with modelType="base" (the settings sheet that hosts
  this section is only rendered in single-thread mode).

Behaviour after this commit:
- New thread + user picks a container in the sidebar → thread row is
  created with that container_id; first send uses it, no auto-create.
- New thread + user does nothing → row still absent; first send goes
  through the existing inherit/lazy-create path as before.
- The auto-bind effect remains silent best-effort: it does not
  eagerly create the thread row, so it cannot pre-empt the user's
  pick on a fresh thread.

* studio/chat: drop "Auto-create per thread" option, default to latest

The dropdown previously offered "Auto-create per thread" as an
explicit value (null in storage), with the chat-adapter then
inheriting from the most recent container at send-time. That made
the picker display disagree with what the backend would actually do:
the picker said "auto", but the backend was reusing an existing
container.

Behaviour after this commit, when code execution is enabled on an
OpenAI cloud provider:
- Containers list non-empty: dropdown defaults to the container with
  the latest lastActiveAt, eagerly bound via ensureThreadRecord +
  db.threads.update so the bind survives even when the thread row
  has not been materialized by the chat adapter yet. User can pick
  any other container in the list.
- Containers list empty: render a disabled placeholder "(none yet —
  will be created on first send)". The chat-adapter's lazy-create
  path (chat-adapter.ts:1040-1082) mints the first container on
  first send and writes it back to the thread; the next refresh
  surfaces it in the picker.

Expiration mid-operation is unchanged: the existing
container_invalidated _toolEvent clears the thread's stored id and
the next turn re-creates.

* studio/chat: fix picker stuck on "Selecting most recent…" + manual-create binding

Two follow-up fixes to the picker rework in d0cbeb99b.

1) The dropdown was getting stuck on the "Selecting most recent…"
   placeholder option even after the auto-bind write completed,
   because the select was controlled by `activeContainerId` (whatever
   sits in Dexie) and there's a brief window between the auto-bind
   firing and useLiveQuery propagating the new row back. Decoupled
   the rendered value from the Dexie state: compute the displayed id
   locally as `activeContainerId ?? sortedContainers[0]?.id`, so the
   most-recent container's name shows up immediately. The auto-bind
   effect still writes the bind to Dexie so the chat adapter sees it
   on send. Dropped the placeholder option entirely.

2) The manual "Create container" flow (`onCreate`) bound the new
   container to the active thread with a bare `db.threads.update`.
   On a brand-new thread that hadn't been materialized yet, the
   update affected 0 rows; the user's next send then went through
   cross-thread inheritance / lazy-create and could land on a stale
   container, surfacing as "container does not exist". Same fix as
   `onPick`: ensureThreadRecord before update so the bind lands.
2026-05-15 23:39:06 +04:00
Lee Jackson
b65a7450ca
Studio: Dark theme refactor, right sidebar redesign, and chat UI polish (#5150)
* Dark theme refactor, right sidebar redesign, and chat UI polish

- Dark theme refactor
- Redesign right sidebar
- Further left sidebar adjustments
- Wider chat and content area; layout tweaks for chat content
- Rounded corners across elements for consistency
- Show chat message menu icons on menu-area hover, not only on message hover
- Assistant message menu icons now always visible; user messages keep on-hover
- Redesigned copy icon used consistently across chat blocks and messages
- Redesigned trash icon, applied consistently
- Unified icon sizing and style with the sidebar
- Adjusted icon colors across chat
- Fix on-hover background design for chat icons
- Fix tooltip from 'more' button staying visible after clicking elsewhere
- Adjust position and design of generation speed info text below messages
- Adjust design of token speed info popup
- Adjust sidebar scrollbar to cover recent chats only

* Recents sidebar rename, UI/theme refactor, layout and chat polish

UI & Theme:
- Dark theme refactor
- Consistent rounded corners across elements
- CSS polish and cleanup
- Remove unused logo image assets

Recents sidebar:
- Add 'more' button for options menu
- Support renaming conversations and training runs
- Confirmation dialog before deleting chats
- Add optional display_name column to training_runs (idempotent ALTER TABLE) so renaming doesn't lose model_name/dataset_name from the run config
- New PATCH /api/train/runs/{run_id} endpoint accepts { display_name: string | null }; empty/whitespace clears the override
- Sidebar shows display_name ?? model_name and exposes Rename in the row's More menu, mirroring the chat rename flow
- Cache last list response in localStorage and hydrate from it on mount, so recents paint instantly on F5 / route revisit; cached items are shape-validated and dropped if malformed
- Optimistic updates on rename and delete (apply locally + cache before background refresh)
- Visible toast on rename/delete failure instead of swallowed errors

Layout:
- Redesigned right sidebar
- Further left sidebar adjustments
- Updated chat content layout; chat and content area slightly widened
- Sidebar scrollbar covers recent chats only

Icons:
- Redesigned copy icon, unified across chat blocks and messages
- Redesigned trash icon to match
- Consistent icon sizing and style across chat and sidebar
- Adjusted icon colors across chat
- Fix icon on-hover background design

Chat messages:
- Menu icons now appear on hover over the menu area, not just the message
- Assistant message menu icons always visible; user messages keep on-hover (next/previous response stays visible for edited prompts)
- Repositioned and restyled generation speed info text below messages
- Restyled token generation speed popup

Tooltips:
- Removed tooltip on hover for previous/next assistant response icons
- Unified tooltip design across sidebars and chat
- Removed tooltip animations (also fixes related lag)

Model & Chat Template config:
- Merged Chat Template config into Model Configuration section
- Added revert-to-original for chat template
- Fix Chat Template config disappearing on page refresh until model reload

Performance & scroll:
- Removed chatbox movement animations across pages/navigation (fixes related UI lag)
- Fix scroll flicker at end of streaming when a code block is the final element
- Additional chat scroll improvements

Bug fixes:
- Fix 'more' button tooltip remaining visible after clicking elsewhere

* Remove sidebar localStorage cache and optimistic updates

Drops the localStorage hydration and optimistic rename/delete logic from the recents sidebar; reverts to fetching fresh on mount.

* Fix missing cn import in shared-composer (regression from merge)

* chore(sidebar): import sidebar deps from feature indexes

Re-export deleteChatItem / renameChatItem / useChatSidebarItems / SidebarItem / useChatSearchStore / ChatSearchDialog from @/features/chat, and removeTrainingUnloadGuard from @/features/training. Switch app-sidebar.tsx to consume them via the public feature indexes instead of deep paths, clearing the no-restricted-imports eslint errors. No behavior or UX change.

* fix(studio/frontend): reload training Recents sidebar after F5 refresh

The Recents sidebar showed empty after a hard refresh. The hook's inFlightRef dedup guard collided with React StrictMode's double-mount in dev: the second mount's fetch returned silently with no error, no retry, and no toast — leaving the sidebar empty until navigation.

Replace skip-if-busy dedup with abort-previous via a hook-level AbortController. This also fixes a latent race where a slow poll could resurrect a just-deleted row by clobbering the optimistic update.

Changes (all in use-training-history-sidebar.ts):
- fetchRuns aborts any in-flight request before starting a new one; post-await signal.aborted check drops stale responses.
- Optimistic helpers (applyRunUpdate, removeRun) abort in-flight fetches so they don't depend on caller discipline to invalidate stale data.
- Initial load gets bounded retry-with-backoff (500ms / 1.5s / 3.5s) and surfaces a sonner toast with a Retry action on final failure.
- Failure toast auto-dismisses on any successful load (initial retry, Retry click, or polling recovery).
- Polling pauses while the tab is hidden and catches up on visible, avoiding wasted requests during long training runs.
- Both effects own their teardown explicitly (abort + clear timer).

* Apply unified tooltip design and behavior across remaining pages for consistency

* UI polish: spacing, tooltip on source icons, letter spacing, smaller icons, consistent edit icon

- Adjust tiny spacing between elements around the UI for subtle polish
- Redesign tooltip on source icons for web search / tool use, consistent with the new design
- Adjust chat text letter spacing
- Smaller icon sizes
- Replace 'edit message' icon in chat with the new Rename icon used in Recents for consistency

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Adjust CSS for right sidebar

* Fix scrollbar UI compatibility across browsers

* fix: preserve chat preset settings on model load

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): remove duplicate chat template status field

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore: remove creative preset assumption

* fix(studio): align speculative decoding default

* fix(studio/chat): snap numeric param inputs to step grid

- Type a value in any param input (Temperature, Top K, Max Tokens, etc.)
  now clamps to [min, max] and snaps to the slider's step grid, killing
  off-grid values like 1.051234 and FP residue from slider drags.
- Branch picker chevrons share the action bar's 32px height + 10px radius
  via a new .aui-branch-chevron-btn utility; hover area aligns visually
  while staying narrower than the sibling icon buttons.

* fix(studio/chat): keep training-run polls converging and drop dead preset code

- Keep training-run polls converging when responses outrun the 5s interval
  (don't unconditionally abort prior in-flight; skip if one is still pending,
  mutation race still guarded).
- Drop dead Creative/Precise preset code paths (remove 'builtin-fixed' source
  variant + unreachable branches).

* fix(studio): training-run cards show custom name + model + dataset

- Training-run cards now display custom display_name + model + dataset,
  with cross-view sync on rename/delete.
- Enhance clarity of borders and colors in dark theme on export etc.

* fix(studio): match active state green to unsloth brand color

* fix(studio): preserve can_resume on training rename

* fix(studio): keep GGUF chat template override distinct

* fix(studio): treat audio input models as multimodal

* fix(studio): cancel numeric draft on Escape

* fix(studio): use default speculative mode on toggle

* fix(studio): detect GGUF audio VLM input models

* fix(studio): address final PR review findings

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): refresh sidebar/history when a new training run starts so it appears without a manual reload

* fix: API and svg

* fix(studio/sidebar): align run rename dirty check with displayed baseline

* fix(studio/sidebar): use leading-tight on account block to prevent descender clipping with truncate

---------

Co-authored-by: sneakr <hauzin@hotmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Co-authored-by: shine1i <wasimysdev@gmail.com>
2026-05-07 14:33:31 +04:00
Wasim Yousef Said
b01e9af124
feat(studio): replace navbar with collapsible sidebar (#4936)
* feat(studio): replace navbar navigation with collapsible sidebar

Add an app-wide sidebar with hover-expand and pin-to-dock behavior.
Navigation items (Studio, Recipes, Export, Chat) move from the center
pill navbar to the sidebar. Chat threads and recipes render as
collapsible sub-lists. Navbar simplified to logo + update + close.

- Extend SidebarProvider with pinned/hovered state model
- New AppSidebar with animated active indicator, sloth profile menu,
  theme toggle, guided tour, back/forward navigation
- Chat page refactored to URL-driven view state via search params
- Extract reusable hooks for chat thread and recipe sidebar data
- Guard startViewTransition for browser compatibility
- Wrap chat deletions in Dexie transaction for data integrity

* feat(studio): move logo to sidebar and make navbar overlay

- Sidebar is now full-height with logo in SidebarHeader
- Collapsed sidebar shows sticker.png, expanded shows full logo
- Navbar is absolute-positioned overlay (no layout space)
- Main content extends to top, aligning with navbar controls

* feat(studio): full-height sidebar with recents, edge-to-edge nav buttons

- Sidebar outside max-w-7xl, pinned to left edge
- Remove sidebar rounding, menu buttons rounded-md
- Nav buttons flush to sidebar edges with no left rounding
- Replace collapsible recipes/chat with flat nav items
- Add Recents section with chat history (1 item when not on chat, full on chat)
- New Chat as first nav item with PencilEdit02Icon
- Cursor pointer on all sidebar buttons
- Navbar temporarily hidden for screenshots

* fix(studio): fix chat scroll, action bar hover, collapsible recents

- Fix sticky composer by removing `relative` override on viewport footer
- Action bar buttons only show on hover (autohide=always)
- Remove floating border/shadow from action bar
- Add scroll space above composer for last message actions
- Back/forward buttons use router history (stay in-app)
- Recents section collapsible with chevron on chat route
- Set html/body/#root height for proper h-full chain

* fix(studio): address review feedback, clean up unused code

- Unhide navbar (was left hidden from screenshot)
- Remove unused imports: SidebarMenuSub*, BubbleChatIcon, ColumnInsertIcon
- Remove unused vars: recipeItems, activeRecipeId, canCompare, recipesOpen
- Include compare query id in active sidebar selection
- Use store type for contextUsage instead of inline type
- Simplify noop in sidebar.tsx
- Remove empty className prop

* feat(studio): add mobile sidebar, recent runs section, and misc UX fixes

* feat(studio): scaffold settings feature module with dialog store

* feat(studio): add tri-state theme store for settings

* feat(chat): add clear-all-chats and export-chat-history utils

* feat(studio): add settings dialog shell with tab rail

* feat(studio): add appearance tab with theme and sidebar pin

* feat(studio): add settings general tab with hf token, auto-title, reset prefs

* feat(studio): add settings chat tab with export and clear

* feat(studio): add api keys tab with list and revoke flow

* feat(studio): add create-key form and reveal dialog

* feat(studio): add usage examples panel to api keys tab

* feat(studio): add settings about tab with update and shutdown

* feat(studio): add settings dropdown item and cmd-comma shortcut

* feat(studio): remove legacy api-keys route and chat-sheet preference rows

* fix(studio): settings dialog a11y + polish pass

* feat(studio): inline api key reveal card replacing nested dialog

* fix(studio): hide revoked keys from settings list

* refactor(studio): strip navbar and hoist training unload guard

* feat(studio): explicit sidebar toggle, remove hover-open and pin icons

* fix(studio): use SidebarRight01Icon for collapsed sidebar open toggle

* fix(studio): address code review findings for settings dialog

* feat(studio): collapsible navigate group with standalone new-chat and compare

* fix(studio): chat-only standalone actions, use ColumnInsertIcon for compare

* fix(studio): sidebar new-chat/compare state reset and icon-mode collapsible

* feat(studio): add compact logo assets for sidebar header

* Fixed sidebar design

* fix(studio): sidebar delete icon hover contrast and sizing

* feat(studio): route-gate sidebar recents (chats off /studio, runs on /studio)

* feat(studio): add chat search store

* feat(studio): add chat search index hook with snapshot-on-open

* feat(studio): add chat search command dialog with global shortcut

* feat(studio): wire chat search into sidebar

* fix(studio): trim hf token on save, add show/hide toggle, commit on close

* revert(studio): restore original sidebar/border colors, brighten sidebar

* feat(studio): forward overlayClassName through CommandDialog

* fix(studio): wrap search dialog in Command context, redesign as flat 635px card

* fix(studio): reserve right padding on recent items so delete icon stops overlapping title

* fix(studio): skip hf token unmount-commit during reset-prefs reload

* chore(studio): drop unused icon import and unreachable runs navigate branch

* fix(studio): chat search index filters archived before limit, batches message query, picks up reasoning text

* fix(studio): keep CommandEmpty in tree so empty state renders correctly

* fix(studio): cap system prompt and chat template textareas so they scroll instead of growing

* fix(studio): attach chat-compare tour anchor to sidebar compare button

* fix(studio): persist system theme explicitly so next-themes does not clobber on reload

* fix(studio): auto-switch to history tab when selecting a recent run from sidebar

* UI overhaul: chatbox, scrollbar, sidebar, and compare view

UI Changes:
- Redesigned the Compare UI with general cleanup
- Redesigned the Chatbox UI
- Reduced the width of the user chat bubble for improved readability
- Narrowed the user chat box across the content page
- Adjusted thinking-box text color to be slightly darker
- Removed faded text effect from chat messages
- Removed faded text effect from the thinking box
- Added a small LLM chat safety note at the bottom of the chatbox
- Restyled the scrollbar

Layout & Behavior:
- Reworked the scrollbar to span the full height of the page (no top/bottom padding) and remain persistently visible when content is scrollable, rather than only on hover
- Reworked the Configuration sidebar to span full height — removed rounded corners and borders, with the scrollbar adjusted to match the full top-to-bottom layout
- Adjusted the top menu and bottom chatbox content areas to work correctly with the new full-page scroll behavior
- Made chat content match the chatbox width, with content sliding slightly behind the chatbox when scrolling
- Aligned chat text width with the chatbox for visual consistency, including how far the text extends behind the chatbox

Fixes:
- Fixed the chatbox not auto-expanding when typing multi-line input while bottom-positioned during an active chat (previously only worked before a chat had started)
- Fixed positioning and design of the user chat hover menu buttons to match the assistant chat box — now displayed below the chat bubble instead of on the left side

* Fix user message layout in thread component

* swap code icon

* fix compare layout

* fix compare pane flex

* Sidebar improvements and fixes

- Added scrolling support to the sidebar so menus and recent chats no longer get hidden
- Recent chats are now always visible in the sidebar, not hidden when in Studio, Recipes, or Export
- Recent chat is now deselected when selecting other navigations
- Fixed sidebar glitch where browser resize could make the sidebar and expand button disappear completely
- Fixed glitch where the open-sidebar hover tooltip appeared above the logo when clicking expand sidebar
- Reduced sidebar width on mobile to around 2/3 of the screen (was too wide)
- Made the close-sidebar hover tooltip consistent with the rest of the design
- Removed sidebar collapse/expand animation
- Small adjustment to chat width

* Fix route scrolling, polling, and theme sync issues

* Fix Studio page scrolling

---------

Co-authored-by: sneakr <hauzin@hotmail.com>
2026-04-16 08:46:16 -07:00
Daniel Han
5aa8c15246
Studio: hard-stop at n_ctx with a 'Context limit reached' toast (#5021)
* Studio: hard-stop at n_ctx with a dedicated 'Context limit reached' toast

llama-server's default behavior when the KV cache fills is to silently
drop the oldest non-``n_keep`` tokens and keep generating. The UI has
no way to tell the user that earlier turns were evicted -- they just
see degraded continuity and a confusing ``5,361 / 4,096`` on the
context usage bar.

Launch llama-server with ``--no-context-shift`` so it returns a clean
error once the request would exceed ``n_ctx``. In the chat adapter,
catch the error, identify it as a context-limit error via
``isContextLimitError()``, and surface a dedicated toast that names
the exact control to adjust: the ``Context Length`` field in the chat
Settings panel.

Also add a lightweight tooltip hint on ``ContextUsageBar`` when usage
crosses 85%, so users see the "raise Context Length in Settings"
suggestion before they hit the hard stop.

Tests:

  * ``test_llama_cpp_no_context_shift.py`` pins the ``--no-context-shift``
    flag in the static launch-command template, and pins it inside the
    unconditional ``cmd = [ ... ]`` block so a future refactor can't
    hide it behind a branch.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Shorten --no-context-shift comment to 1 line

* Match backend _friendly_error rewrite in isContextLimitError

Codex review on PR caught that ``backend/routes/inference.py::_friendly_error``
rewrites the raw llama-server text
  "request (X tokens) exceeds the available context size (Y tokens)"
into
  "Message too long: X tokens exceeds the Y-token context window. ..."
on the main streaming GGUF path. The heuristic only looked for
"context size" / "exceeds the available context" / "context shift",
none of which survive the rewrite, so the new "Context limit reached"
toast would never fire for the most common case. Add matches for
"message too long" and "context window" so both wordings hit.

Also addresses Gemini feedback on the launch-flag test:
  * Use ``inspect.getsource(LlamaCppBackend.load_model)`` instead of
    reading ``__file__`` directly; scopes the assertions to the
    function that actually launches llama-server.
  * Replace the hardcoded ``"            ]"`` indent search with a
    line-at-a-time scan for a line that is just ``]``, so the test
    survives reformatting.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-04-14 10:58:20 -07:00
Daniel Han
5861a7ce15
Studio: split model-load progress label across two rows (#5020)
* Studio: split model-load progress label across two rows

The chat flow and training overlay both compose a progress label like
"112.6 of 122.3 GB • 331.0 MB/s • 30s left" and render it next to the
percent badge in a single flex row. Once the rate + ETA part shows up,
the label outgrows the row width and wraps mid-phrase, orphaning the
percent ("19 left %") onto a second ragged line.

Fix in model-load-status.tsx: split the label on the first " • " into
a primary (size) chunk that stays on row 1 with the percent, and a
secondary (rate/ETA) chunk that renders on its own muted row below.
Labels without a bullet (e.g. "22.8 GB downloaded") collapse cleanly
to one row. The inline-status variant keeps only the primary and
surfaces the full label via the tooltip.

Also extracts the rate/ETA math out of useTransferStats into a pure
``transfer-stats.ts`` module (appendSample + computeTransferStats) so
it can be reasoned about and tested without React. The hook is now a
thin wrapper that feeds sample history through the pure functions.

Backend: adds two companion test files for load_progress():

  * test_llama_cpp_load_progress_matrix.py (21 tests) -- platform
    matrix (Linux /proc, macOS/Windows absence), VmRSS parsing
    variants (tab/space/missing/malformed), filesystem edges (HF-cache
    symlinks, broken symlinks, nonexistent paths, relative paths),
    shard aggregation (partial multi-shard, two series in same dir,
    mmproj-* exclusion, single-file), lifecycle races, concurrent
    sampling (10 threads x 50 iters against real /proc), fraction
    bounds.
  * test_llama_cpp_load_progress_live.py (5 tests) -- no-mock live
    integration: real subprocess allocating 100 MB to match VmRSS,
    real ready phase, real dead-pid degradation, real shard
    aggregation, repeated polling. Skipped on non-Linux.

Both complement the existing test_llama_cpp_load_progress.py.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Hoist splitProgressLabel out of JSX IIFE (review feedback)

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-04-14 10:58:16 -07:00
Wasim Yousef Said
50cccfd55e
feat(chat): server-side timings, context display & source hover cards (#4467)
* feat(chat): add server-side timings and context display for GGUF

Extract timings/usage metadata from llama-server SSE stream and forward
through the full stack. Replace client-side estimates with accurate
server-reported metrics (prompt eval, tok/s, token counts, cache hits).
Add context window usage bar to chat top nav.

* feat(chat): source badges with hover cards and 2-row collapse

- Add hover cards to source badges showing favicon, title, URL and
  snippet description on hover
- Limit source badges to 2 rows with +X more expand/collapse
- Parse snippet from web search results for hover card descriptions
- Replace individual Source rendering with grouped SourcesGroup component

* fix(chat): add null guards for server timings edge cases

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(chat): reset contextUsage on thread switch, remove unused context-display

* fix(chat): stop double-counting completion tokens in tool-calling path

* fix(chat): skip metadata events in llm_assist consumers

* fix(chat): hide context usage bar in compare mode

* fix(chat): harden timings pipeline and context usage persistence

Accumulate prompt_ms, predicted_ms, and predicted_n from intermediate
tool-detection passes so the final metadata reflects total server work.
Persist contextUsage in message metadata (Dexie) and restore on thread
load. Add type guard in gguf_stream_chunks for unexpected dict events.
Clear contextUsage when entering compare mode.

* feat(chat): make GGUF stream metadata OpenAI-compatible

* fix(chat): address PR review feedback

* feat(chat): address PR review feedback

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-03-20 23:42:01 -07:00
Lee Jackson
9232126734
fix(studio): use explicit Cancel for model load toast (#4377) 2026-03-17 22:39:51 -07:00
Manan Shah
93ab09d195
[Feature] compare for 2 diff models (#4356)
* compare for 2 diff models

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* resolving gemini comments

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(studio): refine model-load toast stop action and compare selector sizing (#4369)

Co-authored-by: imagineer99 <samleejackson0@gmail.com>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: imagineer99 <samleejackson0@gmail.com>
2026-03-17 22:58:34 +04:00
Daniel Han
8ffd86012f Change "Stop loading" to outlined "Stop" button 2026-03-16 02:46:56 -07:00
Daniel Han
9cbeecc16a Incorporate PR #4304 toast UX improvements
Merge the toast UX refactor from PR #4304 (by @Shine1i):
- Toast duration 5s default with close button (X) for manual dismiss
- Inline progress bar component (ModelLoadInlineStatus) shown in the
  header after toast is dismissed
- Model switch warning only for image compatibility (not generic)
- activeThreadId tracked in store via ActiveThreadSync
- Loading state cleanup via resetLoadingUi helper
- Toast uses Infinity duration during loading with onDismiss handler

Re-applied non-GGUF download progress additions on top:
- getDownloadProgress for all models (not just GGUF)
- hasShownProgress flag, loadingModelRef race condition checks
- First poll at 500ms, bytes-only fallback when expected size unknown
2026-03-16 02:46:56 -07:00