release: v0.50.240
## Release v0.50.240
Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures).
---
### Added
- **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282
- **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482
- **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479
- **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485
- **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481
- **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568
- **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281
- **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268
- **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269
### Fixed
- **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266
- **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278
- **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267
- **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273
---
### Test results
```
3199 passed, 2 skipped, 3 xpassed in 72.79s
```
### PRs on hold (not included)
#1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
|
|
|
"""Static UI tests for quieter tool-call rendering and shared design tokens.
|
|
|
|
|
|
|
|
|
|
These tests intentionally follow the repo's existing pytest style: read static
|
|
|
|
|
source files, isolate the relevant function/rule, and assert implementation
|
|
|
|
|
invariants before changing the UI.
|
|
|
|
|
"""
|
|
|
|
|
import pathlib
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
REPO = pathlib.Path(__file__).parent.parent
|
|
|
|
|
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
|
|
|
|
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
|
|
|
|
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _function_body(src: str, name: str) -> str:
|
|
|
|
|
match = re.search(rf"function\s+{re.escape(name)}\s*\(", src)
|
|
|
|
|
assert match, f"{name}() not found"
|
|
|
|
|
brace = src.find("{", match.end())
|
|
|
|
|
assert brace != -1, f"{name}() has no body"
|
|
|
|
|
depth = 1
|
|
|
|
|
i = brace + 1
|
|
|
|
|
in_string = None
|
|
|
|
|
escaped = False
|
|
|
|
|
in_line_comment = False
|
|
|
|
|
in_block_comment = False
|
|
|
|
|
while i < len(src) and depth:
|
|
|
|
|
ch = src[i]
|
|
|
|
|
nxt = src[i + 1] if i + 1 < len(src) else ""
|
|
|
|
|
if in_line_comment:
|
|
|
|
|
if ch == "\n":
|
|
|
|
|
in_line_comment = False
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
if in_block_comment:
|
|
|
|
|
if ch == "*" and nxt == "/":
|
|
|
|
|
in_block_comment = False
|
|
|
|
|
i += 2
|
|
|
|
|
continue
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
if in_string:
|
|
|
|
|
if escaped:
|
|
|
|
|
escaped = False
|
|
|
|
|
elif ch == "\\":
|
|
|
|
|
escaped = True
|
|
|
|
|
elif ch == in_string:
|
|
|
|
|
in_string = None
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
if ch == "/" and nxt == "/":
|
|
|
|
|
in_line_comment = True
|
|
|
|
|
i += 2
|
|
|
|
|
continue
|
|
|
|
|
if ch == "/" and nxt == "*":
|
|
|
|
|
in_block_comment = True
|
|
|
|
|
i += 2
|
|
|
|
|
continue
|
|
|
|
|
if ch in "'\"`":
|
|
|
|
|
in_string = ch
|
|
|
|
|
i += 1
|
|
|
|
|
continue
|
|
|
|
|
if ch == "{":
|
|
|
|
|
depth += 1
|
|
|
|
|
elif ch == "}":
|
|
|
|
|
depth -= 1
|
|
|
|
|
i += 1
|
|
|
|
|
assert depth == 0, f"{name}() body did not close"
|
|
|
|
|
return src[brace + 1:i - 1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestToolCallGroupingStatic:
|
|
|
|
|
def test_simplified_tool_calling_setting_is_wired_through_frontend(self):
|
|
|
|
|
assert "settingsSimplifiedToolCalling" in (REPO / "static" / "index.html").read_text(encoding="utf-8"), (
|
|
|
|
|
"Settings should expose a Compact tool activity checkbox."
|
|
|
|
|
)
|
|
|
|
|
assert "window._simplifiedToolCalling" in (REPO / "static" / "boot.js").read_text(encoding="utf-8"), (
|
|
|
|
|
"Boot should hydrate simplified_tool_calling into a runtime flag."
|
|
|
|
|
)
|
|
|
|
|
panels = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
|
|
|
|
|
assert "settingsSimplifiedToolCalling" in panels and "simplified_tool_calling" in panels, (
|
|
|
|
|
"Settings panel should load and save the simplified_tool_calling setting."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_render_messages_gates_settled_activity_grouping(self):
|
|
|
|
|
fn = _function_body(UI_JS, "renderMessages")
|
|
|
|
|
helper = _function_body(UI_JS, "ensureActivityGroup")
|
|
|
|
|
assert "isSimplifiedToolCalling()" in fn, (
|
|
|
|
|
"Settled tool/thinking grouping should be gated by the Compact tool activity toggle."
|
|
|
|
|
)
|
|
|
|
|
assert "tool-cards-toggle" in fn, (
|
|
|
|
|
"The non-simplified path should preserve the upstream loose tool-card controls."
|
|
|
|
|
)
|
|
|
|
|
assert "data-tool-call-group" in helper, (
|
|
|
|
|
"Tool-call groups need a stable data-tool-call-group attribute for CSS and tests."
|
|
|
|
|
)
|
|
|
|
|
assert re.search(r"cards\.length|toolCount|toolCalls\.length|group\.length", fn + helper), (
|
|
|
|
|
"The simplified group header should derive its summary/count from the number of tool calls."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_tool_call_groups_default_collapsed_with_summary_visible(self):
|
|
|
|
|
fn = _function_body(UI_JS, "renderMessages")
|
|
|
|
|
helper = _function_body(UI_JS, "ensureActivityGroup")
|
|
|
|
|
assert "tool-call-group-collapsed" in fn or "collapsed" in fn, (
|
|
|
|
|
"Historical tool-call groups should default to a collapsed state."
|
|
|
|
|
)
|
|
|
|
|
assert "tool-call-group-summary" in helper, (
|
|
|
|
|
"Collapsed groups must expose a visible summary/header row."
|
|
|
|
|
)
|
|
|
|
|
assert "tool-call-group-body" in helper, (
|
|
|
|
|
"Tool-card detail rows should live inside a group body that can be "
|
|
|
|
|
"expanded/collapsed."
|
|
|
|
|
)
|
|
|
|
|
assert "aria-expanded" in helper, (
|
|
|
|
|
"The expand/collapse control must expose aria-expanded."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_live_tool_cards_use_grouping_only_when_simplified(self):
|
|
|
|
|
live_fn = _function_body(UI_JS, "appendLiveToolCard")
|
|
|
|
|
settled_fn = _function_body(UI_JS, "renderMessages")
|
|
|
|
|
assert "isSimplifiedToolCalling()" in live_fn, (
|
|
|
|
|
"Live streaming tool cards should branch on the Compact tool activity toggle."
|
|
|
|
|
)
|
|
|
|
|
assert "ensureActivityGroup" in live_fn, (
|
|
|
|
|
"Compact live tool rendering should use the grouped activity container."
|
|
|
|
|
)
|
|
|
|
|
assert "toolRunningRow" in live_fn, (
|
|
|
|
|
"The non-simplified live tool path should preserve the upstream running-dots row."
|
|
|
|
|
)
|
|
|
|
|
assert "buildToolCard" in live_fn and "buildToolCard" in settled_fn, (
|
|
|
|
|
"Live and settled tool rendering should share buildToolCard() for consistent markup."
|
|
|
|
|
)
|
|
|
|
|
assert "data-live-tid" in live_fn, (
|
|
|
|
|
"Live grouping must preserve data-live-tid so tool_start/tool_complete updates still replace the correct card."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_tools_and_thinking_share_one_collapsed_activity_dropdown(self):
|
|
|
|
|
ui_min = re.sub(r"\s+", "", UI_JS)
|
|
|
|
|
assert "functionensureActivityGroup(" in ui_min, (
|
|
|
|
|
"Tool calls and thinking should share one agent-activity disclosure helper."
|
|
|
|
|
)
|
|
|
|
|
assert "data-agent-activity-group" in UI_JS, (
|
|
|
|
|
"The shared tools/thinking disclosure needs a stable data-agent-activity-group hook."
|
|
|
|
|
)
|
|
|
|
|
assert "agent-activity-thinking" in UI_JS, (
|
|
|
|
|
"Thinking content should be nested inside the shared activity dropdown, not rendered separately."
|
|
|
|
|
)
|
|
|
|
|
render_fn = _function_body(UI_JS, "renderMessages")
|
|
|
|
|
assert "isSimplifiedToolCalling()" in render_fn and "assistantThinking.set(rawIdx, thinkingText)" in render_fn, (
|
|
|
|
|
"Settled thinking should move into the shared activity dropdown only when Compact tool activity is enabled."
|
|
|
|
|
)
|
|
|
|
|
assert "seg.insertAdjacentHTML('beforeend', _thinkingCardHtml(thinkingText))" in render_fn, (
|
|
|
|
|
"The non-simplified path should preserve standalone settled thinking cards."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_live_thinking_uses_shared_activity_dropdown_only_when_simplified(self):
|
|
|
|
|
live_thinking_fn = _function_body(UI_JS, "appendThinking")
|
|
|
|
|
assert "isSimplifiedToolCalling()" in live_thinking_fn, (
|
|
|
|
|
"Live thinking should branch on the Compact tool activity toggle."
|
|
|
|
|
)
|
|
|
|
|
assert "ensureActivityGroup" in live_thinking_fn, (
|
|
|
|
|
"Compact live thinking should be inserted into the shared activity dropdown."
|
|
|
|
|
)
|
|
|
|
|
assert "thinkingRow" in live_thinking_fn, (
|
|
|
|
|
"The non-simplified live thinking path should preserve the upstream #thinkingRow card."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestToolCardDesignTokens:
|
|
|
|
|
def test_root_defines_shared_layout_design_tokens(self):
|
|
|
|
|
for token in (
|
|
|
|
|
"--radius-sm",
|
|
|
|
|
"--radius-md",
|
|
|
|
|
"--radius-card",
|
|
|
|
|
"--space-1",
|
|
|
|
|
"--space-2",
|
|
|
|
|
"--space-3",
|
|
|
|
|
"--font-size-xs",
|
|
|
|
|
"--font-size-sm",
|
|
|
|
|
"--surface-subtle",
|
|
|
|
|
"--border-subtle",
|
|
|
|
|
):
|
|
|
|
|
assert token in CSS, f"Missing design token {token} in style.css"
|
|
|
|
|
|
|
|
|
|
def test_base_dark_palette_restores_upstream_gold_tokens(self):
|
|
|
|
|
css_min = re.sub(r"\s+", "", CSS)
|
|
|
|
|
expected_tokens = (
|
|
|
|
|
"--bg:#0D0D1A",
|
|
|
|
|
"--sidebar:#141425",
|
|
|
|
|
"--border:#2A2A45",
|
|
|
|
|
"--text:#FFF8DC",
|
|
|
|
|
"--muted:#C0C0C0",
|
|
|
|
|
"--accent:#FFD700",
|
|
|
|
|
"--surface:#1A1A2E",
|
|
|
|
|
"--topbar-bg:rgba(20,20,37,.98)",
|
|
|
|
|
)
|
|
|
|
|
for token in expected_tokens:
|
|
|
|
|
assert token in css_min, f"Base dark palette token missing: {token}"
|
|
|
|
|
|
|
|
|
|
def test_base_light_palette_restores_upstream_gold_tokens(self):
|
|
|
|
|
css_min = re.sub(r"\s+", "", CSS)
|
|
|
|
|
expected_tokens = (
|
|
|
|
|
"--bg:#FEFCF7",
|
|
|
|
|
"--sidebar:#FAF7F0",
|
|
|
|
|
"--border:#E0D8C8",
|
|
|
|
|
"--text:#1A1610",
|
|
|
|
|
"--muted:#5C5344",
|
|
|
|
|
"--accent:#B8860B",
|
|
|
|
|
"--surface:#F3EEE3",
|
|
|
|
|
)
|
|
|
|
|
for token in expected_tokens:
|
|
|
|
|
assert token in css_min, f"Base light palette token missing: {token}"
|
|
|
|
|
|
2026-04-30 02:59:26 +00:00
|
|
|
def test_default_skin_preview_stays_upstream(self):
|
release: v0.50.240
## Release v0.50.240
Batch release of 13 PRs that passed full triage + code review + test suite (3199 tests, 0 failures).
---
### Added
- **Compact tool activity mode** (`simplified_tool_calling`, default on) — groups tool calls and thinking traces into a single collapsed "Activity" disclosure card per assistant turn. Also adds a new **Calm Console** theme with earth/slate palette and serif prose. @Michaelyklam — #1282
- **PDF first-page preview** — `MEDIA:` `.pdf` files render a canvas thumbnail via PDF.js CDN (4 MB cap). **HTML sandbox iframe** — `.html`/`.htm` files render inline in a sandboxed `<iframe srcdoc>` (256 KB cap). 10 i18n keys × 7 locales. @bergeouss — #1280, closes #480 #482
- **Inline Excalidraw diagram preview** — `.excalidraw` files render as pure SVG (no external deps; rectangles, ellipses, diamonds, text, lines, arrows, freehand; 512 KB cap). @bergeouss — #1279, closes #479
- **Inline CSV table rendering** — fenced `csv` blocks and `MEDIA:` CSV files render as scrollable HTML tables with auto-separator detection. @bergeouss — #1277, closes #485
- **Inline SVG, audio, and video rendering** — SVG as `<img>`, audio as `<audio controls>`, video as `<video controls>`. @bergeouss — #1276, closes #481
- **Batch session select mode** — multi-select sessions for bulk Archive/Delete/Move. 11 i18n keys × 7 locales. @bergeouss — #1275, closes #568
- **Collapsible skill category headers** — click to collapse/expand without re-render; state persists across filter cycles. @bergeouss — #1281
- **`providers.only_configured` setting** — opt-in flag to restrict the model picker to explicitly configured providers. @KingBoyAndGirl — #1268
- **OpenCode Go model catalog** — adds Kimi K2.6, DeepSeek V4 Pro/Flash, MiMo V2.5/Pro, Qwen3.6/3.5 Plus. @nesquena-hermes — #1284, closes #1269
### Fixed
- **Profile `TERMINAL_CWD` TypeError** — `_build_agent_thread_env()` helper merges env before `_set_thread_env()` call. @hi-friday — #1266
- **Service worker subpath cache bypass** — regex now matches `/api/*` under any mount prefix. @Michaelyklam — #1278
- **SSE client disconnect leaks** — `TimeoutError`/`OSError` treated as clean disconnects; server backlog 64, threads daemonized; session list renders before saved-session restore. @KayZz69 — #1267
- **i18n locale corrections** — Korean MCP strings (23), Chinese MCP strings (23), zh-Hant missing keys (41), de missing keys (229). @bergeouss — #1274, closes #1273
---
### Test results
```
3199 passed, 2 skipped, 3 xpassed in 72.79s
```
### PRs on hold (not included)
#1265 (draft), #1271 (superseded by #1266), #1272 (skipped XSS tests), #1232 (partial test run), #1222 (review questions open), #1134 (live-server tests), #1132 (superseded by #1134), #1108 (negative UX review), #1084 (empty description)
2026-04-30 00:42:32 +00:00
|
|
|
boot_min = re.sub(r"\s+", "", BOOT_JS)
|
|
|
|
|
assert "{name:'Default',colors:['#FFD700','#FFBF00','#CD7F32']}" in boot_min, (
|
|
|
|
|
"The Default skin swatch should stay aligned with the upstream gold base."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_tool_card_css_uses_design_tokens_for_chrome(self):
|
|
|
|
|
css_min = re.sub(r"\s+", "", CSS)
|
|
|
|
|
assert ".tool-card{" in css_min, ".tool-card rule missing"
|
|
|
|
|
assert "border-radius:var(--radius-card)" in css_min, (
|
|
|
|
|
".tool-card border radius should use --radius-card, not hardcoded px."
|
|
|
|
|
)
|
|
|
|
|
assert "background:var(--surface-subtle)" in css_min, (
|
|
|
|
|
".tool-card background should use --surface-subtle."
|
|
|
|
|
)
|
|
|
|
|
assert "border:1pxsolidvar(--border-subtle)" in css_min, (
|
|
|
|
|
".tool-card border should use --border-subtle."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_tool_card_header_and_text_use_spacing_and_font_tokens(self):
|
|
|
|
|
css_min = re.sub(r"\s+", "", CSS)
|
|
|
|
|
assert ".tool-card-header{" in css_min, ".tool-card-header rule missing"
|
|
|
|
|
assert "gap:var(--space-2)" in css_min, (
|
|
|
|
|
".tool-card-header gap should use --space-2."
|
|
|
|
|
)
|
|
|
|
|
assert "padding:var(--space-1)var(--space-3)" in css_min, (
|
|
|
|
|
".tool-card-header padding should use spacing tokens."
|
|
|
|
|
)
|
|
|
|
|
assert ".tool-card-name{" in css_min and "font-size:var(--font-size-xs)" in css_min, (
|
|
|
|
|
".tool-card-name should use --font-size-xs."
|
|
|
|
|
)
|
|
|
|
|
assert ".tool-card-preview{" in css_min and "font-size:var(--font-size-xs)" in css_min, (
|
|
|
|
|
".tool-card-preview should use --font-size-xs."
|
|
|
|
|
)
|