v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
import re
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_messages_zero_skips_effective_model_resolution():
|
|
|
|
|
src = (ROOT / "api" / "routes.py").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
assert re.search(
|
|
|
|
|
r"effective_model\s*=\s*\(\s*"
|
|
|
|
|
r"_resolve_effective_session_model_for_display\(s\)\s*"
|
|
|
|
|
r"if resolve_model\s*else None\s*\)",
|
|
|
|
|
src,
|
|
|
|
|
), "messages=0 metadata requests must not resolve the model catalog"
|
|
|
|
|
assert 'resolve_model_default = "1" if load_messages else "0"' in src
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_full_message_load_updates_viewed_count_after_metadata_fast_path():
|
|
|
|
|
src = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
assert "_setSessionViewedCount(S.session.session_id, Number(data.session.message_count || 0));" in src
|
|
|
|
|
assert "_setSessionViewedCount(sid, Number(S.session.message_count || msgs.length));" in src
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_lazy_message_load_skips_model_resolution():
|
|
|
|
|
src = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
assert "messages=1&resolve_model=0" in src
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_session_switch_defers_model_resolution_without_blocking():
|
|
|
|
|
src = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
ui = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
assert "messages=0&resolve_model=0" in src
|
|
|
|
|
assert "function _resolveSessionModelForDisplaySoon" in src
|
|
|
|
|
assert "messages=0&resolve_model=1" in src
|
|
|
|
|
assert "_modelResolutionDeferred=true" in src
|
|
|
|
|
assert "deferModelCorrection" in ui
|
2026-05-07 02:48:31 +00:00
|
|
|
assert "if(fallback&&!deferModelCorrection)" in ui
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
|
|
|
|
|
|
2026-05-17 05:14:34 +00:00
|
|
|
def test_deferred_model_resolution_refreshes_context_metadata():
|
|
|
|
|
src = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
start = src.index("function _resolveSessionModelForDisplaySoon")
|
|
|
|
|
end = src.index("const _INITIAL_MSG_LIMIT", start)
|
|
|
|
|
block = src[start:end]
|
|
|
|
|
|
|
|
|
|
assert "S.session.context_length" in block, (
|
|
|
|
|
"deferred model resolution must also hydrate context_length so a "
|
|
|
|
|
"resumed high-context session does not keep the old model's limit"
|
|
|
|
|
)
|
|
|
|
|
assert "S.session.threshold_tokens" in block
|
|
|
|
|
assert "_syncCtxIndicator" in block
|
2026-06-05 20:45:15 +00:00
|
|
|
compact = block.replace(" ", "")
|
|
|
|
|
assert "constresolvedContextLength=data.session.context_length||S.session.context_length||0;" in compact
|
|
|
|
|
assert "context_length:resolvedContextLength||u.context_length||0" in compact
|
2026-05-17 05:14:34 +00:00
|
|
|
|
|
|
|
|
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
def test_boot_does_not_block_session_restore_on_model_catalog():
|
|
|
|
|
src = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
|
2026-05-20 12:55:17 +00:00
|
|
|
assert "if(s.default_model){" in src
|
|
|
|
|
assert "window._defaultModel=s.default_model;" in src
|
2026-06-27 00:07:49 +00:00
|
|
|
assert "const _hydrateModelDropdown=({redirectIfUnauth=null}={})=>populateModelDropdown({" in src
|
2026-05-20 12:55:17 +00:00
|
|
|
assert "window._modelDropdownReady=null;" in src
|
2026-06-27 00:07:49 +00:00
|
|
|
assert "window._startBootModelDropdown=_startBootModelDropdown;" in src
|
|
|
|
|
assert "window._ensureModelDropdownReady=_startModelDropdown;" in src
|
v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031)
* fix: remove orphaned i18n keys from top-level LOCALES object
Three Traditional Chinese translation keys (cmd_status, memory_saved,
profile_delete_title) were placed outside any locale block between the
en and ru blocks in static/i18n.js. They became top-level properties
of the LOCALES object, causing them to appear as invalid language
options in the Settings > Preferences dropdown.
The correct translations already exist in the zh-Hant locale block.
Fixes #1008
* fix: block stale SSE events from polluting new session's DOM
- appendThinking(): guard with !S.session||!S.activeStreamId to drop
events from a previous session's SSE stream during a session switch
- appendLiveToolCard(): same guard for consistency
- finalizeThinkingCard(): scroll thinking-card-body to top when
scroll is pinned, so completed response is immediately visible
- appendThinking(): auto-scroll thinking card body to bottom while
streaming if user is watching (scroll pinned)
* Fix empty agent sessions in sidebar
* fix: resolve cron UI UX issues — icon ambiguity, toast overlap, running status
Fixes #995 — three sub-issues in the Cron Jobs UI:
1. Dual play icons ambiguous: Resume button now shows a distinct
play+bar icon (play triangle + vertical line) instead of the
identical triangle used by Run now.
2. Toast notification overlapping header buttons: Added
position:relative; z-index:10 to .main-view-header so it
stacks above the fixed toast (z-index:100 within its layer).
3. No running status after trigger: After triggering a job, the
status badge immediately shows 'running…' with a CSS spinner
animation, and polls the cron list every 3s (up to 30s) to
refresh when the job completes.
- Added cron_status_running i18n key in all 5 locales (en, es, de, ru, zh, zh-Hant)
- Added .detail-badge.running CSS class with spinner animation
- New functions: _setCronDetailStatus(), _startCronRunningPoll()
* fix(#1011): address review feedback — poll cleanup, badge persistence, 30s fallback
- _clearCronDetail() now clears _cronRunningPoll interval on navigation
- Poll re-applies 'running' badge after loadCrons() re-render (prevents flicker)
- When poll ends (30s max), detail re-renders with actual status as fallback
* feat: create folder and add space directly from UI (#782)
- After creating a folder via the file tree New folder button, offer to add it as a space via confirm dialog
- Add Create folder if it doesnt exist checkbox in the New Space form
- Backend: support create flag in /api/workspaces/add to mkdir before validation
- i18n: 4 new keys (folder_add_as_space_title/msg/btn, workspace_auto_create_folder) in all 6 locales
* fix: validate workspace path before mkdir to prevent orphan directories
Review feedback (critical): the previous code called mkdir() before
validate_workspace_to_add(), which meant a rejected path (e.g. system dir)
would leave an orphan directory on disk.
New flow:
1. Resolve path and check against blocked system roots BEFORE any mutation
2. mkdir() only if path passes the blocklist check
3. Full validation (exists, is_dir) after mkdir
Also imports _workspace_blocked_roots for the pre-mutation blocklist check.
* fix(#1014): classify model-not-found errors with helpful message
- Add model_not_found error type to streaming.py exception classifier
- Detect 404, 'not found', 'does not exist', 'invalid model' patterns
- Strip HTML tags from provider error messages (nginx 404 pages, etc.)
- Add model_not_found branch to apperror handler in messages.js
- Add i18n key model_not_found_label in all 6 locales
- 15 tests covering detection, sanitization, frontend, and i18n
* feat(ui): add live TPS stat to header
Adds a TPS (Tokens Per Second) chip to the right of the header title bar
that updates live while AI output is streaming.
Metering (api/metering.py)
- Tracks per-session output + reasoning tokens via GlobalMeter singleton
- Per-session TPS = total_tokens / elapsed_time
- Global TPS = average of active sessions' TPS values
- HIGH/LOW are max/min of global_tps snapshots over a 60-minute rolling
window (only recorded when > 0, so idle periods are excluded)
- Thread-safe with a single lock
Metering events emitted from streaming.py
- Throttled at 100ms from token/reasoning/tool callbacks so the display
updates rapidly during fast token streams
- 1Hz ticker as fallback for slow streams (exits when no active sessions)
- Final stats emitted on stream end
Routes (api/routes.py)
- Removed POST /api/metering/interval endpoint (dynamic interval via
focus/blur was replaced with simple always-1s-when-active approach)
UI (static/messages.js, index.html, style.css)
- TPS chip in titlebar: shows 'N.N t/s . N.N high . N.N low'
- Default: '0.0 t/s . 0.0 high' when idle
- Display updates on every metering SSE event (throttled to 100ms)
* feat: session restore speed + title gen reasoning hardening (#1025, #1026)
PR #1025 (@franksong2702): Speed up large session restore paths
- GET /api/session?messages=0 now parses only metadata before the messages array
- Metadata-only loads no longer populate the full-session LRU cache
- Frontend lazy fetch uses resolve_model=0 to avoid cold model-catalog lookup
- Hard reload no longer waits for populateModelDropdown() before restoring session
PR #1026 (@franksong2702): Harden auto title generation for reasoning models
- Raises title-gen completion budget to 512 tokens (reasoning-safe)
- Retries once with 1024 tokens on empty content / finish_reason:length
- Applies retry to both auxiliary and active-agent fallback routes
- Preserves underlying failure reason in title_status on local fallback
Co-authored-by: Frank Song <franksong2702@gmail.com>
* feat: session attention indicators in right slot + last_message_at timestamps (#1024)
PR #1024 (@franksong2702): Polish session attention indicators
- Streaming spinners and unread dots now reuse the right-side actions slot
- Running/unread rows hide timestamps; idle/read rows keep right-aligned timestamps
- Date group carets point down when expanded, right when collapsed
- Pinned group no longer repeats pinned-star icon per row
- Running indicators appear immediately after send (local busy state while /api/sessions catches up)
- Sidebar sorting/grouping/timestamps now prefer last_message_at (derived from last real message)
so metadata-only saves don't make old sessions appear under Today
Co-authored-by: Frank Song <franksong2702@gmail.com>
* docs: v0.50.207 release notes — 10 PRs, 2169 tests (+36)
---------
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
Co-authored-by: Josh <josh@fyul.link>
Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-04-25 20:07:35 +00:00
|
|
|
assert "await populateModelDropdown()" not in src
|
2026-05-20 12:55:17 +00:00
|
|
|
|
|
|
|
|
|
2026-05-24 18:08:41 +00:00
|
|
|
def test_boot_primes_model_catalog_without_awaiting_it():
|
|
|
|
|
"""The boot-time prime must NOT await the model-catalog hydration before
|
|
|
|
|
rendering the session list. A later awaited hydration inside the saved-
|
|
|
|
|
session restore path at ``if(S.session) await _startBootModelDropdown();``
|
|
|
|
|
is intentional — that one re-applies the saved session's model after the
|
|
|
|
|
live catalog hydrates so the chip never shows a stale static default
|
|
|
|
|
(see comment in static/boot.js next to the saved-session restore).
|
|
|
|
|
"""
|
|
|
|
|
src = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
|
2026-06-27 00:07:49 +00:00
|
|
|
ensure_pos = src.index("window._ensureModelDropdownReady=_startModelDropdown;")
|
2026-05-24 18:08:41 +00:00
|
|
|
prime_pos = src.index("Promise.resolve(_startBootModelDropdown()).catch(()=>{});", ensure_pos)
|
|
|
|
|
session_restore_pos = src.index("await renderSessionList();", prime_pos)
|
|
|
|
|
|
|
|
|
|
assert ensure_pos < prime_pos < session_restore_pos
|
|
|
|
|
|
|
|
|
|
# No await on the boot-prime path itself: between ensure_pos and the first
|
|
|
|
|
# session_restore await, the dropdown is fired-and-forgotten.
|
|
|
|
|
boot_prelude = src[ensure_pos:session_restore_pos]
|
|
|
|
|
assert "await _startBootModelDropdown()" not in boot_prelude, (
|
|
|
|
|
"Boot prelude must not await _startBootModelDropdown — the prime is "
|
|
|
|
|
"fire-and-forget so the sidebar can render before /api/models returns."
|
|
|
|
|
)
|
|
|
|
|
assert "await populateModelDropdown()" not in boot_prelude
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_failed_boot_model_catalog_prime_is_retryable():
|
|
|
|
|
src = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
2026-05-24 19:17:59 +00:00
|
|
|
# #2726 parameterized the call: populateModelDropdown({preferProfileDefaultOnFreshBoot:true})
|
|
|
|
|
# Match either signature shape — empty args (legacy) OR opts arg (post-#2726).
|
2026-06-27 00:07:49 +00:00
|
|
|
needle = "const _hydrateModelDropdown=({redirectIfUnauth=null}={})=>populateModelDropdown({"
|
|
|
|
|
start = src.index(needle)
|
2026-05-24 18:08:41 +00:00
|
|
|
end = src.index("const _startBootModelDropdown=()=>", start)
|
|
|
|
|
block = src[start:end]
|
|
|
|
|
|
|
|
|
|
assert "window._modelDropdownReady=null;" in block
|
|
|
|
|
assert "throw e;" in block
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 12:55:17 +00:00
|
|
|
def test_boot_primes_visible_default_model_without_catalog_fetch():
|
|
|
|
|
src = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
default_block_start = src.index("if(s.default_model){")
|
|
|
|
|
default_block = src[default_block_start:src.index("window._sessionJumpButtonsEnabled", default_block_start)]
|
|
|
|
|
|
|
|
|
|
assert "if(s.default_model_provider) window._activeProvider=s.default_model_provider;" in src
|
|
|
|
|
assert "const existingDefaultOpt=Array.from(sel.options).find(o=>o.value===s.default_model);" in default_block
|
|
|
|
|
assert "existingDefaultOpt.dataset.provider=window._activeProvider;" in default_block
|
|
|
|
|
assert "if(!existingDefaultOpt)" in default_block
|
|
|
|
|
assert "opt.dataset.custom='1'" in default_block
|
|
|
|
|
assert "opt.dataset.provider=window._activeProvider||''" in default_block
|
|
|
|
|
assert "_applyModelToDropdown(s.default_model,sel,window._activeProvider||null)" in default_block
|
|
|
|
|
assert "populateModelDropdown()" not in default_block
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_settings_exposes_default_model_provider_for_lazy_boot_catalog():
|
|
|
|
|
src = (ROOT / "api" / "config.py").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
assert 'settings["default_model_provider"]' in src
|
|
|
|
|
assert 'model_cfg = get_config().get("model", {})' in src
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_boot_renders_session_list_before_workspace_and_onboarding_settle():
|
|
|
|
|
src = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
workspace_start = src.index("const _workspaceListReady=loadWorkspaceList();")
|
|
|
|
|
onboarding_start = src.index("const _onboardingReady=_bootSettings.onboarding_completed?Promise.resolve(false):loadOnboardingWizard();")
|
|
|
|
|
render_pos = src.index("await renderSessionList();", onboarding_start)
|
|
|
|
|
workspace_await = src.index("await _workspaceListReady;", render_pos)
|
|
|
|
|
onboarding_await = src.index("await _onboardingReady;", render_pos)
|
|
|
|
|
|
|
|
|
|
assert workspace_start < render_pos < workspace_await
|
|
|
|
|
assert onboarding_start < render_pos < onboarding_await
|
|
|
|
|
assert "_bootSettings.onboarding_completed" in src
|