The .msg-artifact-image span was stretching wider than the image (inline-block
in a block context filled the line), so the absolutely-positioned download
button (right:8px of the wrapper) floated ~240px to the right of the image.
Add width:fit-content + max-width so the wrapper hugs the image; the button now
correctly overlays the image's top-right corner. line-height:0 removes inline
descender gap.
Nathan feedback round 2:
- #3220: generated images were rendering at the 120x90 upload-thumbnail size
(too small for the subject of the message). Now render at natural aspect
ratio up to 360px (max-height 360, responsive max-width), lightbox preserved.
- #3223: drop the per-item subtitle/description lines from the session action
menu — show only icon + label (VS Code / browser / ChatGPT pattern). The
description is preserved as a hover tooltip (title=). Makes the menu ~40%
shorter, less crowded, and structurally less prone to viewport clipping.
#3220: redesign generated-image rendering from a permanent bordered card
(filename + Open/Download buttons) to a clean inline image with click-to-zoom
lightbox + a hover/focus-revealed Download overlay, matching ChatGPT/Claude/
Gemini. Drops redundant Open (lightbox already covers it). Tests updated.
#3337: fix two-tone code background — Prism's prism-tomorrow theme styled the
parent <pre> gray while the <code> was navy var(--code-bg), so dark theme showed
a gray frame around navy code. Override BOTH .preview-code[class*=language-] and
its <code> to var(--code-bg) (mirrors the chat code-block fix at .msg-body pre).
#3223: fix action-menu clipping — the new Regenerate-title row made the 9-item
menu tall enough to overflow the viewport bottom when opened on a top-anchored
row at short viewports (e.g. 1280x720). _positionSessionActionMenu now clamps
the menu within both viewport edges and caps max-height with scroll when the
menu is taller than the viewport.
Per maintainer UX direction: drop the text badge pill ("APPROVAL" /
"N QUESTIONS") in favor of color-coding the existing right-side status dot
plus the colored left rail. Red dot (--error) for pending approvals, amber
dot (--warning) for pending clarifies; theme-driven, visible even when the
session is not streaming/unread. Approval dot gently pulses (disabled under
prefers-reduced-motion). Quieter and consistent with the existing pin/unread
dot family — the row no longer needs to truncate its title to fit a pill.
Updated test_session_attention_badges to assert the dot classes
(is-attention-approval/clarify) instead of the removed badge element/styles.
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
Cyberpunk-inspired dark-first skin with neon purple (#B347FF) accent
and cyan (#00DDFF) highlights. Full palette for both light and dark
modes with component-level overrides (glow effects, purple scrollbars,
tool cards, tree viewer, session badges, MCP status indicators).
- static/style.css: 37 CSS rules (light + dark palette + components)
- static/boot.js: Neon entry in _SKINS array
- static/index.html: neon:1 in skin allowlist
Follows the existing skin pattern (modeled after Nous).
- Rename _escHandler to _keyHandler (now handles nav keys too)
- Store counter reference (lb._counterEl) to avoid DOM query on every nav
- Remove dead 'let counter = null' and 'hasNav' closure variable
- Use lb._navImages directly in keyboard handler for consistency
- Add null guard on lb.querySelector('img') in _navigateLightbox
- Inline _updateLightboxCounter one-liner
- Fix CSS section comment 'Image lightbox close' → 'Image lightbox'
- Fix CHANGELOG placeholder (#PR → #2967)
When multiple images appear in the same message, clicking any image
now opens a lightbox with prev/next navigation buttons (‹ / ›) and
keyboard support (← / →). An image counter (e.g. '3 / 5') is shown at
the bottom of the overlay.
- _openImgLightbox now receives the clicked <img> element to find
sibling images within the same message container
- New _openImgLightboxWithNav, _navigateLightbox, _updateLightboxCounter
- CSS: .img-lightbox-nav (prev/next buttons), .img-lightbox-counter
- Close button (×), Escape key, and click-outside-to-close preserved
The tool-card border-subtle was so faint that the cards visually melted
into the surrounding prose once the cursor left the conversation. Bumps
the resting border to --border-muted and adds a 2px left edge so a tool
output row reads as metadata at a glance, even on light skins where
border-subtle is barely visible. Hover still escalates to --border2.
Verified by loading a session with mixed tool calls and assistant prose
on the light theme and confirming the tool cards are now identifiable
without mousing over them.
Refs #2867
Cherry-picked via 3-way apply onto stage HEAD.
Resolved workspace.js conflict: kept master's #2716 sessionId-capture
stale-session guard (closure-scoped sessionId check after await), AND
added PR's renderSessionArtifacts() call to refresh the new Artifacts
tab when the file tree updates. Wrapped in typeof check for defense.
Co-authored-by: AJV20 <abdielvc@me.com>
Cherry-picked via 3-way apply of net delta against stage HEAD. All 8 files
applied cleanly including the new static/pwa-startup.js.
Co-authored-by: AJV20 <abdielvc@me.com>
Add `PATCH /api/mcp/servers/{name}` endpoint that accepts `{"enabled": bool}`,
updates `mcp_servers.<name>.enabled` in config.yaml, and calls `reload_config()`.
Mirrors the existing DELETE pattern.
Also wire the previously-defined-but-unrouted `_handle_mcp_server_delete` into
`handle_delete`, and `_handle_mcp_server_update` into a new `handle_put` +
`do_PUT` in server.py — fixing a pre-existing bug where those handlers existed
but were never reachable over HTTP.
UI: add a toggle button in each MCP server row in the system settings panel
(panels.js). Clicking it calls PATCH and reloads the list. Toggle button is
styled with `.mcp-toggle-enabled` / `.mcp-toggle-disabled` CSS classes. The
`toggle_supported` flag in the list response is now `True`.
i18n: add 5 new keys (`mcp_enable_server`, `mcp_disable_server`,
`mcp_enabled_toast`, `mcp_disabled_toast`, `mcp_toggle_failed`) to all 9
non-English locales (English values as placeholder translations).
Tests: add `TestMcpToggle` class with 7 tests covering disable, enable,
404-not-found, empty name, missing field, response payload, and URL-encoded name.
Update `test_empty_config` and visibility panel assertions to reflect
`toggle_supported: True` and the new toggle button in panels.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three small fixes from Copilot's review:
1. static/style.css:1354 - removed spaces inside `clamp(...)` args to
match the file's existing compact style (no spaces after commas in
neighboring declarations like `transition:border-color .2s,box-shadow .2s`).
2. CHANGELOG.md - wrapped the long single-line entry across multiple lines
with standard Markdown continuation indentation for cleaner diffs.
3. CHANGELOG.md - normalized `~1300 px` to `~1300px` for unit-formatting
consistency.
No behavior change. Same one-line CSS rule, just tightened formatting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`.composer-box` had a hardcoded `max-width: 780px` since the early
v0.50.x layout pass. On wide displays (1440p+, 2880px ultrawides)
this leaves significant unused horizontal space AND squeezes the
composer-footer chips (workspace, model, reasoning, context %)
against each other inside the 780px box.
When the context-percentage ring appears (active token usage), the
workspace chip truncates to "Fou..." instead of showing the full
workspace name. Model + reasoning chips also lose room. The chip
strip horizontally-scrolls inside .composer-left, so the rightmost
chips effectively hide behind context %.
The constraint isn't "Reading flow looks better at 780px" — the
textarea is min-height:64px, max-height:200px and wraps naturally,
so users on wide displays get the SAME readable text wrap regardless
of box width. Only the footer chips suffer.
Fix: clamp(780px, 60vw, 1100px). Preserves the 780px floor (no
regression on viewports < 780px since clamp's first arg is the
minimum) while letting wider viewports use up to 1100px (60% of
viewport width, capped). 1100px gives ~40% more horizontal room for
the footer chips without filling the entire screen at extreme widths.
Per-viewport behavior:
<= 780 px → 780 px (hard floor) — zero change vs current
1280 px → 60vw = 768 → floored to 780 — zero change
1440 px → 60vw = 864 — +84 px room
1920 px → 60vw = 1152 → capped at 1100 — +320 px room
2880 px → 60vw = 1728 → capped at 1100 — +320 px room
One line in static/style.css. CHANGELOG entry. No JS. No new deps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Squashed from 2 author commits:
- d2237e23 feat: surface live activity timeline
- eee57ec0 fix: satisfy activity timeline CI guards
Frontend-only telemetry from existing stream events. Replaces empty
Thinking… placeholder with observable run status (Waiting on model /
Waiting on tool result / Working for …). New CSS, new test file.
- Rewrote _kanbanRenderMarkdown() from basic paragraph wrapper to a
line-by-line block processor supporting headings, code blocks, lists,
task lists, tables, blockquotes, horizontal rules, and strikethrough.
- Added CSS for all new elements (table borders, code blocks, checkboxes,
blockquote accent, heading sizing, etc.).
- Dropped white-space: pre-wrap from .kanban-task-preview-body and
.kanban-detail-row-main since markdown now handles layout.
- Applied _kanbanRenderMarkdown() to task description (was esc()) and
comment body (was esc()) in the task detail view.
Squashed from 2 author commits:
- a1017d02 initial fix: flex:0 0 auto on all 5 chip wraps
- bf54ba50 Copilot review fix-up: consolidate into single rule
Closes#2740. CSS-only, no JS changes. Default-width layout unchanged,
only affects narrow-viewport overflow regime via composer-left's existing
overflow-x:auto.
Add Hepburn skin with full light/dark palette derived from the
Hepburn TUI theme. Brand color #c6246a with pink-magenta accents.
- Light: soft pink surfaces (#fff3f7 / #fbe4ed)
- Dark: deep aubergine (#110a0f / #1e0f19)
- Accent: #d44a7a (light) / #f278ad (dark)
- Styled: send button, new chat button, tool cards, session indicator
Also fix settings panel skin picker to prioritize localStorage
over server defaults, so newly selected skins reflect correctly
in the dropdown.
Original PR: #2676 by @lucasrc
Adds POST /api/skills/toggle endpoint that flips skills.disabled in
config.yaml, and a UI toggle in the Skills panel that shows all skills
(including disabled ones) with a per-skill on/off control.
- Backend: new endpoint validates skill exists in filesystem before
toggling. Read-modify-write wrapped in _cfg_lock for thread safety.
Writes through to platform_disabled.webui when present.
- Frontend: each skill-item now has a toggle switch; disabled skills
appear muted but still listed (previously they were filtered out).
- i18n: new toggle keys translated across all 9 non-English locales.
- Tests: round-trip test for disabled list normalization + toggle
endpoint behavior.
Squash-merged from contributor's branch (19 commits + 1 merge commit)
onto current master via the cherry-pick-stale-contributor-prs procedure.
- Replace text 'Collapse'/'Expand' button labels with Lucide chevron SVG
icons (chevron-down expanded → click to collapse, chevron-up collapsed
→ click to expand). Matches the iconographic design language of the
rest of the chrome (composer buttons, sidebar controls).
ARIA label + title attributes carry the same semantics for assistive
tech, so no accessibility regression vs. the text labels.
- Fix collapsed-card edge clipping at viewport bottom. Original
.clarify-card { bottom: -24px } was sized for the expanded card
(300-420px tall); adding a 72px collapsed variant pushed the header
below the parent's visible region. Override bottom to 8px and reduce
inner padding for the collapsed state so the entire header sits cleanly
inside the viewport at both desktop and mobile sizes (verified card
fits with ~115px margin desktop / ~125px margin mobile).
Per Nathan's 2026-05-22 UX feedback on the screenshot package.
Light-theme review revealed white text on gold chips (color: var(--bg-page)) was
washed out and hard to read. Switched to fixed dark text #1a1a1a with font-weight
600 so the on-state reads clearly on the gold accent in both light and dark
themes. Off-state unchanged (muted text on transparent).
Per deep-review verdict SHIP-WITH-FIXES on PR #2636:
1. Profile-switch reconciliation: _refreshProfileSwitchBackground now re-fetches
/api/settings and re-applies hidden_tabs for the new profile. Without this,
Profile A's hidden-tabs choice stayed in effect under Profile B until the
user opened Settings → Appearance.
2. A11y: switched chips from role=button + aria-pressed to role=switch +
aria-checked. The pressed/not-pressed wording confused screen-reader users
because chip-off looks like the off state. Added role=group +
aria-labelledby on the container, and a :focus-visible style on the chips.
3. Server-side belt-and-suspenders: api/config.py now strips 'chat' and
'settings' from hidden_tabs at validation time, matching the client's apply-
time filter. A tampered POST can no longer persist the forbidden values.
3 new regression tests added (chat/settings rejection, profile-switch wiring,
chip a11y attributes).
Co-authored-by: FrancescoFarinola <francesco.farinola@example.com>
Adds a reload button to the app titlebar visible only in PWA standalone
or fullscreen mode, and a pull-to-refresh gesture on the messages container
that smooth-scrolls to the top before activating.
The reload button sits next to the message count label and provides a
one-tap refresh for users who installed the WebUI as a home-screen app
where browser navigation controls are unavailable.
The pull-to-refresh gesture detects downward drag at the top of the
message list, shows a visual indicator ('Pull to refresh' / 'Release to refresh'),
and reloads on release past the 80px threshold. When triggered mid-conversation,
it smooth-scrolls to the top first.
Opus advisor on stage-371 caught three issues during pre-release review:
1. RTL salvage missed KaTeX math (display equations + inline LaTeX), diff
blocks, CSV tables (column order must read left-to-right regardless of
chat direction), and .skill-file-path. The first salvage commit only
covered pre/code/kbd/samp/tt and tool-call bodies. Added a second
force-LTR block covering: .katex, .katex-block, .katex-display,
.katex-html, .katex-inline, .diff-block (+children), .csv-table-wrap,
.csv-table (+children), .skill-file-path. Severity: KaTeX is the most
user-visible gap — any user rendering math under RTL would see flipped
equations.
2. Quota chip @media (max-width:1400px) hide rule conflicted at exactly
1400px with the existing @media (min-width:1400px) .messages-inner
rule — chip was hidden AT the wide-desktop boundary where it should
first appear. Changed to (max-width:1399.98px). Visually verified at
1400px: chip now correctly visible there.
3. Dead .icon-btn.provider-quota-chip selector — chip never has icon-btn
class. Removed.
Test added: test_rtl_math_and_tables_stay_ltr (pins the 4 new LTR
surfaces). Also removed dead code in test_rtl_code_blocks_stay_ltr
(unused code_block variable).
Per stage-fix protocol: SHIP-with-followup applied on the stage rather
than the source PR, since #2409 is already merged-into-stage and
nesquena-approved. Stage-371 review-bypass batch path still holds.
- Remove duplicate mobile-close-btn from HTML
- Remove dead .mobile-close-btn CSS rules; unhide .close-preview at all viewports
- Change btnClearPreview tooltip from 'Hide workspace panel' to 'Close'
- Update tests across test_sprint41.py, test_sprint44.py, test_issue781.py,
and test_mobile_layout.py to match new single-button model
Keep distinct generated summary categories, route update-summary generation through the configured auxiliary model first, disclose capped large-range summary input, and constrain long summary panels.
When busy_input_mode is 'steer' and the steer is accepted by the server,
show a transient indicator in the chat area (not in S.messages).
This mirrors the CLI/Gateway approach: steer text is never stored in the
message array. The done event's S.messages=d.session.messages replacement
therefore doesn't cause a flash where all SSE content vanishes and re-appears.
The indicator is an independent DOM element (.steer-indicator) appended to
msgInner. It naturally disappears when renderMessages rebuilds msgInner on
turn completion (done/cancel/error).
`_isDesktopWidth()` in boot.js gates every collapse path on
`matchMedia('(min-width:641px)')` — matching where the rail itself becomes
visible. The CSS rules driving the actual visual collapse were nested inside
the workspace-panel block at `@media(min-width:901px)` — a threshold copied
from the right-panel collapse but with no functional reason to apply here.
Behavioural consequence in the 641–900 px band (tablet portrait + small
laptop windows):
- Rail is visible, user clicks the active icon
- JS adds `.layout.sidebar-collapsed` and writes localStorage='1'
- JS sets aria-expanded='false' on the active rail button
- CSS at min-width:901px does NOT apply → sidebar stays at 300 px width
- User sees no visual change; screen reader announces collapsed state for
a sidebar that is still visible; localStorage silently persists
- Resize to ≥901 px later → sidebar suddenly collapses (surprise state)
Fix: hoist the three `.sidebar-collapsed` / flash-prevention rules out of
the workspace-panel @media block and into their own `@media(min-width:641px)`
block. The rail visibility breakpoint, the JS gate, and the CSS gate now
all agree.
`:not(.mobile-open)` is preserved on both selectors so the mobile slide-in
overlay (handled in the `max-width:640px` block) is never targeted — the
new @641 boundary doesn't change that contract.
Verified breakpoint matrix end-to-end (Node harness over real boot.js +
style.css):
Width | JS desktop | CSS applies | Effect
------|------------|-------------|------------
640 | no | no | no-op (mobile overlay)
641 | yes | yes | collapses ✓
700 | yes | yes | collapses ✓
768 | yes | yes | collapses ✓
900 | yes | yes | collapses ✓
1024 | yes | yes | collapses ✓
Regression test added: `test_css_breakpoint_matches_js_isdesktopwidth`
parses boot.js for the `_isDesktopWidth` matchMedia query, walks CSS to
find the @media block enclosing `.layout.sidebar-collapsed`, and asserts
the thresholds match. Locks the invariant so a future refactor can't
re-introduce the asymmetric-band silent-state-leak.
Test counts:
- tests/test_sidebar_collapse_toggle.py: 35/35 pass (was 34, +1 regression)
- Full suite (Python 3.14, local): 5040 passed, 0 failed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets desktop users collapse the session-list sidebar to maximise the chat
area, without adding any visible UI affordance. Default appearance is
identical to master — only users who actively try to toggle (or know the
keyboard shortcut) ever see a difference.
## Behaviour (desktop only, ≥641px)
| State | Action | Result |
|------------------------------------|-----------------------|-----------------------------------------|
| Sidebar open, click active rail | Toggle | Sidebar collapses to width:0 |
| Sidebar open, click different rail | Normal switch | **Sidebar stays open** (no surprise) |
| Sidebar collapsed, click any rail | Expand + switch | Sidebar expands, then panel switches |
| Anywhere, Cmd/Ctrl+B | Toggle | Same as same-active-rail click |
| Mobile (<641px), any of the above | No-op | Mobile overlay behaviour unchanged |
Two discoverability paths, both opt-in. **No new visible buttons.** Users
who never click the active rail icon see zero UI change vs. master.
## Surface-minimal design
The behaviour is contained behind one extra arg on the rail/sidebar-nav
onclick: `switchPanel('chat',{fromRailClick:true})`. Without that flag the
function preserves master's behaviour exactly — every programmatic
`switchPanel(name)` callsite (commands, deeplinks, internal state changes)
is unaffected. The guard chain inside `switchPanel`:
opts.fromRailClick && _isDesktopWidth() && (
_isSidebarCollapsed() ? expandSidebar() :
prevPanel === nextPanel ? (toggleSidebar(true); return false))
is the ONLY new code path that can cause a collapse. Cross-panel clicks
fall through to the existing switch logic untouched.
## Polish from both source PRs
- **Click-active gesture** as the primary toggle (#1884 @jasonjcwu — the
genuine UX innovation; no extra button needed)
- **Cmd/Ctrl+B keyboard shortcut** (#1924 @spektro33; VS Code convention).
Guarded against firing when typing in INPUT / TEXTAREA / contenteditable
so the shortcut never steals from in-progress text editing.
- **Inline flash-prevention `<script>`** in `<head>` (#1924) sets
`data-sidebar-collapsed='1'` on `<html>` BEFORE the stylesheet loads,
so cold loads with a persisted-collapsed state paint correctly from
frame 0 with no flicker. Cleared by JS once the class system takes over.
- **Smooth slide animation** via `.24s cubic-bezier(.22,1,.36,1)`
(#1924, mirrors the existing workspace-panel collapse on the right)
- **`aria-expanded` mirrored** on the active rail button (#1884) so
screen readers announce open/collapsed transitions.
- **`body.resizing` transition-suppression** (#1884) keeps the drag-resize
cursor instant — no animation during a width-resize gesture.
- **bfcache `pageshow` re-sync** (#1884) — if another tab toggled the
sidebar while this page was frozen, bring it in line on restore.
## Drops vs. #1924
- No persistent rail "toggle sidebar" button (Nathan: keep the UI stealth)
- No close-X button in chat panel head (same reason)
- No i18n keys for the dropped buttons
## What did NOT change
- 22 rail/sidebar-nav `onclick` handlers gained the `{fromRailClick:true}`
arg — function-call shape, invisible to users
- 1 inline `<script>` in `<head>` (flash prevention) — invisible
- 5 lines of CSS — invisible unless someone collapses
That's the entire visible-UI delta. **23 ins / 22 del on `index.html`,
all string-replace.**
## Verification
- 5,151 pytest passing including a new 34-test structural suite covering
every contract (CSS rules, JS functions, fromRailClick guard, legacy
proxy forwarding, flash-prevention `<script>` ordering, mobile
exclusion via :not(.mobile-open) selector, aria-expanded sync).
- Live browser walkthrough at 1280px verified:
- Default boot state identical to master (sidebar open, width 300px)
- Click active rail → collapse (width 1, opacity 0, translateX -14px,
localStorage='1', aria-expanded=false). Panel unchanged.
- Click active rail again → expand back to width 300, aria=true
- Click DIFFERENT rail → normal switch, sidebar stays open (legacy-
preserving case, verified explicitly)
- Click rail while collapsed → expand + switch in one gesture
- Cmd+B toggles correctly
- Cmd+B inside `<textarea>` → suppressed (defaultPrevented=false)
- Reload with collapsed state persisted → restores without flash
- Mobile simulation (matchMedia returns false for min-width:641px):
same-active-rail click is no-op, Cmd+B is no-op, sidebar stays at 300px
Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
Co-authored-by: spektro33 <spektro33@users.noreply.github.com>
Closes#1884Closes#1924
Three connected gaps in the Kanban UX, fixed together because they're
load-bearing for the actual work-queue lifecycle:
1. Edit task — the detail view had only status-transition buttons (Triage/
Todo/Ready/Blocked/Done/Archived) plus Block/Unblock and Add comment.
No way to edit title, body, assignee, tenant, or priority once the task
was created. Backend already supported it via PATCH /api/kanban/tasks/<id>
(api/kanban_bridge.py::_patch_task) — purely a UI gap.
Now: an Edit button on the task-detail header opens the existing modal
pre-filled with current values, switches the modal title to 'Edit task'
and the submit button to 'Save', PATCHes instead of POSTing on submit.
2. Run dispatcher — the existing 'Preview dispatcher' button always passed
?dry_run=1 (nudgeKanbanDispatcher), so it was preview-only. There was
literally no UI button anywhere in the WebUI that actually ran the
dispatcher to claim Ready tasks and spawn workers. Users had to drop
to the CLI.
Now: new runKanbanDispatcher() entry point hits /api/kanban/dispatch
without dry_run=1, after a showConfirmDialog confirmation because it
spawns subprocess workers. Two UI surfaces: a lightning-bolt button in
the board header (visually distinct from the dry-run preview ▶), and
a primary 'Run dispatcher' button in the sidebar bulk bar next to a
relabeled 'Preview' button. Toast result shows concrete numbers from
dispatch_once(): 'Dispatched: 1 spawned, 2 skipped (no assignee)' —
not just a generic 'OK'.
3. Assignee dropdown — the previous create modal accepted free-text
assignee with no validation. The dispatcher (kanban_db.py:3567) only
spawns workers when row['assignee'] is a real Hermes profile name; any
typo or blank value made the task sit in Ready forever.
Now: <select> populated from /api/profiles (Hermes profile names) with
historical board assignees grouped under 'Other (CLI lanes / removed
profiles)', plus an explicit '— Unassigned (won't auto-run) —' option.
Default selection is the first profile, not Unassigned. Custom SVG
chevron so the field reads visually as a dropdown. Helper text under
the field explains the dispatcher claim contract. Soft warning if user
explicitly picks Unassigned + Ready ('You picked Unassigned + Ready.
The dispatcher will skip this task. Submit again to confirm, or pick
a profile.'); proceeds on second submit.
Side effect: default new-task status changed from triage to ready, since
'ready' is what users want for tasks they intend to actually run. Triage
is still in the dropdown for tasks that need staging review.
i18n: 19 new keys translated across all 8 supported locales.
Tests: 3 new regression tests in tests/test_kanban_ui_static.py:
- test_kanban_task_detail_has_edit_button_and_modal_supports_edit_mode
- test_kanban_assignee_dropdown_uses_select_not_freetext
- test_kanban_run_dispatcher_button_exists_and_is_distinct_from_preview
Verified end-to-end in browser: created board → opened modal with profile
dropdown → created task with assignee=archivist → clicked Edit → changed
all 5 fields → saved → verified persistence → clicked Run dispatcher →
confirm dialog → confirmed → toast 'Dispatched: 1 spawned' → task moved
Ready → Running.
Test suite: 5042 passed, 11 skipped, 3 xpassed, 0 regressions in 151s.
The Kanban sidebar panel's header '+' button (#kanbanNewTaskBtn) was
wired straight to createKanbanTask(), which reads the inline
#kanbanNewTaskTitle input and silently returns when empty. The inline
input lives below five rows of filters (search, assignee, tenant,
archived/mine toggles, stats, bulk-action bar) and is typically off-screen
on first panel open, so the header button looked dead — clicking it with
no title typed did nothing visible (no modal, no scroll, no focus shift,
no toast).
Now the header '+' opens #kanbanTaskModal — a centered overlay with the
same .kanban-modal-overlay shell the existing create-board modal uses,
so the two flows look and behave identically (centered card, dim
backdrop, ESC closes, click-on-backdrop closes). The modal exposes the
fields the backend already accepts at /api/kanban/tasks: Title, Description,
Status (Triage/Todo/Ready), Priority, Assignee (datalist suggestions from
the active board), Tenant (datalist).
UX details:
- Title is required; submit-with-empty shows a properly styled red error
- Title field auto-focuses on open
- ESC closes the modal; backdrop click closes; Enter on simple inputs
submits, Enter in the description textarea inserts a newline
- Submit POSTs only the fields the user filled in (no forced empty strings)
and auto-opens the new task's detail view
- Submit button disables while posting to prevent double-submit
- Inline quick-add (Enter on #kanbanNewTaskTitle) is preserved as a
power-user shortcut
Side effect: .kanban-modal-error styling improved (proper red alert with
border + tinted background) so the existing create-board modal benefits
from the same polish for free.
i18n: 11 new keys added across all 8 supported locales (en, ja, ru, es,
de, zh, pt, ko).
Tests: tests/test_kanban_ui_static.py::test_kanban_new_task_header_button_opens_modal
covers the modal markup, button wiring, ESC/Enter handling, datalist
population, submit behavior, and inline-quick-add fallthrough.
Verified end-to-end in the browser on an isolated test env (port 8789):
created a board from scratch, opened the modal via header '+',
submitted with title/description/status/priority/assignee/tenant filled in,
moved the task through statuses (Triage → Todo → Ready → Blocked → Archived),
added a comment, verified Cancel + ESC + backdrop-click all close cleanly,
verified validation error rendering, verified inline quick-add still works.
Closes#1964
Earlier in this branch I'd reduced .panel-header > span:first-child to
flex-shrink:1 thinking it would let heading + chip fit better at the
default 300px panel width. That broke
test_workspace_label_shrinks_with_ellipsis which pins the
git-badge:3 > label:2 > icons:0 shrink hierarchy as load-bearing
(git badge collapses first, label second, icons never).
The chip-on-narrow-panel concern is now addressed by the @container
query that hides the chip entirely below 420px container width — the
heading no longer competes with the chip for horizontal space, so
flex-shrink:2 is fine again.
At the default 300px panel width, even the icon-only chip + 'Workspace'
heading + 5 action buttons overflowed and triggered ellipsis on the
heading ('WORKSP...'). Cleaner: hide the chip below 420px container
width and rely on the kebab's accent dot as the non-default-state
signal. The dot costs zero horizontal space (absolute-positioned over
the kebab icon) and the kebab's tooltip still labels what's happening.
On wider panels (user-resized, or future layouts), the full chip with
text appears.
Vision review of v1 flagged the chip's accent-yellow as 'loud and ugly'.
Switched to muted hover-bg + 1px border for a subtler badge look. Also
addressed heading truncation: at the default 300px panel width, heading
(95px) + 5 action buttons (154px) + chip text (110px) overflows, so the
heading was ellipsing to 'W...'. Added a container query on the existing
.rightpanel container that drops the chip text below 360px container
width, leaving just the eye icon (tooltip still labels it).
Replaces the always-visible inline toggle row that ate ~32px below the
breadcrumb on every panel view (root, subdir, file preview). The toggle
is a set-once preference — most users flip it once or never — so the
control hides behind a kebab dropdown in the panel-actions row instead.
A small 'hidden visible' indicator next to the WORKSPACE heading flags
the non-default state so users don't forget the pref is on. Click the
indicator to reopen the menu and uncheck.
The localStorage key, filtering behavior, and the canonical
\`workspaceShowHiddenFiles\` checkbox id are unchanged — the checkbox
is rebuilt inside the dropdown each time it opens. All 11 existing
regression tests for #1793 stay green; 7 new tests pin the kebab
affordance shape.
Four small UX bugs Nathan caught while dogfooding the v0.51.17 release on
desktop. All independently reproduced with browser_console + browser_vision
on a fresh worktree before fixing.
(1) **Left-rail icon tooltips never appeared.** The rail was migrated to the
new `.has-tooltip` system in #1782, but the legacy suppression rule
`.rail .nav-tab:hover::after { content: none }` survived the migration.
Its specificity (0,3,1) outweighs `.has-tooltip:hover::after` (0,2,1),
and `content: none` removes the pseudo-element entirely on hover — so the
new tooltip system silently no-op'd on every rail icon. Fix: drop the
suppression rule and scope the legacy `data-label` tooltip to
`.sidebar-nav .nav-tab` (mobile) only, so it doesn't fire on rail buttons
that carry no `data-label` (which would render an empty styled box).
(2) **`+ New conversation` tooltip clipped at panel right edge.** The button
sits flush with the chat panel's right edge but used `--bottom` which
centers the tooltip on `left:50%` — half the label overflowed past the
panel edge ("New convers..."). New `.has-tooltip--bottom-right` variant
anchors the tooltip's RIGHT edge to the trigger so the label extends
inward. Reusable for any future right-edge panel-head button.
(3) **Workspace right-click menu items had no hover state.** The five sites
in `_showFileContextMenu` (Rename / Reveal / Copy path / Delete) and two
in `_showProjectContextMenu` set `style.background = 'var(--hover)'`. The
custom property `--hover` is undefined anywhere in the codebase. An
undefined `var()` falls back to the property's initial value
(`transparent` for `background`) → no visible hover feedback. The defined
variable is `--hover-bg` (`rgba(255,255,255,.06)`), already used by every
other hover state in the app. One-letter typo, seven sites.
(4) **Rename dialog didn't pre-fill the current filename.** The caller
(`_inlineRenameFileItem`) passed `defaultValue: item.name` to
`showPromptDialog`, but the dialog's input setter reads `opts.value`
only — the param name was silently dropped, leaving only the placeholder
visible (Nathan called it the "ghost name"). Fixed two ways for
defense-in-depth:
- Caller switched to canonical `value: item.name`.
- Dialog now also accepts `defaultValue` as an alias for `value`, so
future typos using the standard `HTMLInputElement.defaultValue` param
name don't repeat the bug.
Plus: added `selectStem:true` opt that selects the stem before the last
`.` on focus (Finder-style: `report.txt` → selects `report`, extension
preserved). Edge cases verified live: directories full-select,
`.gitignore` full-selects (dot at index 0), `noextension` full-selects,
`a.b.c.d` selects `a.b.c`.
## Tests
+12 new regression tests, +5 net (existing test_css_tooltips suite gained 5
class-based tests; new tests/test_workspace_context_menu_and_rename.py file
adds 7 more). Total: 4728 passed (was 4723 in v0.51.17), 4 skipped, 3
xpassed, 0 failed in 141s.
- `RailTooltipCascadeTests` — pins the killer rule's absence (with comment
stripping so the explanatory note doesn't false-positive), pins the
scoped `.sidebar-nav .nav-tab` form, walks every rail button to confirm
`has-tooltip` + non-empty `data-tooltip`.
- `BottomRightTooltipVariantTests` — pins variant existence, mechanics
(`right:0`, `left:auto`, `transform:none`), and `#btnNewChat` adoption
(with mutual-exclusion check that it doesn't carry both `--bottom` and
`--bottom-right`).
- `ContextMenuHoverBackgroundTests` — `var(--hover)` may not appear in
ui.js or sessions.js (the bug shape); affirmative pin that
`_showFileContextMenu` sets ≥4 items to `var(--hover-bg)` and
`_showProjectContextMenu` ≥2.
- `ShowPromptDialogPrefillTests` — pins both `opts.value` and
`opts.defaultValue` references; pins the `selectStem` mechanic
(`lastIndexOf('.')` + `setSelectionRange(0, dot)`); pins the caller's
use of `value:item.name` and `selectStem`.
## Verification
Live in browser at port 8789 (worktree-served):
- Rail Tasks tooltip renders 8px right of the icon at the same vertical
level (math: btn at y=87-123, tooltip at left=44px = 36px width + 8px gap).
- New-conversation tooltip renders below + button with right edge aligned
to button's right edge, extending leftward, fully visible.
- Right-click → Reveal in File Manager shows `rgba(255, 255, 255, 0.035)`
background on hover (the `--hover-bg` value); was `rgba(0, 0, 0, 0)`
(transparent) before.
- Right-click → Rename on `report.txt`: input shows `report.txt`,
selectionStart=0, selectionEnd=6, selected text = "report". Edge cases:
directory `docs` → full-select; `.gitignore` → full-select;
`noextension` → full-select; `a.b.c.d` → selects `a.b.c`.
`node -c` syntax check passes on both modified JS files.
Reported by: Nathan via screenshots (rail tooltips missing, + button
clipped tooltip, Workspace right-click no hover, rename dialog blank).
(1) Send-button tooltip clipping fix:
The send button (btnSend) sits at the right edge of the composer area.
Its side-positioned tooltip extended 'Send message' (~95px wide) past
the viewport edge, leaving only 'Se' visible in some viewports —
confirmed by maintainer screenshot review.
Added a new `.has-tooltip--left` variant that flips the tooltip to
the LEFT side of the trigger via `right: calc(100% + 8px)` instead
of `left: calc(100% + 8px)`. Applied to btnSend in index.html.
Browser-verified: full 'Send message' text now readable to the left
of the gold Send button, no clipping.
(2) Test compatibility for the tooltip coverage expansion:
5 pre-existing tests hardcoded specific class strings or 'title='
attributes that no longer apply after we added has-tooltip + replaced
title= with data-tooltip= on 11 high-traffic icon buttons.
- tests/test_issue1488_composer_voice_buttons.py:
- test_dictation_button_has_dictate_i18n_key: accept either
title='Dictate' or data-tooltip='Dictate' as the static fallback.
- test_buttons_have_distinct_static_titles: extracted helper
_static_tooltip() that prefers data-tooltip over title.
- tests/test_sprint20.py::test_mic_button_has_mic_btn_class:
regex tolerant to additional utility classes between icon-btn and
mic-btn (now 'icon-btn mic-btn has-tooltip').
- tests/test_sprint20b.py::test_send_button_has_title_attribute:
accept title= OR data-tooltip= per #1775.
- tests/test_sprint20b.py::test_send_button_still_has_send_btn_class:
regex tolerant to additional utility classes.
- tests/test_workspace_panel_session_list.py::TestWorkspacePanelCollapsePriority::test_panel_header_no_longer_uses_space_between:
panel-header was changed from overflow:hidden to overflow:visible
so its tooltips can escape the header bar. The title-text ellipsis
moved to the inner span (.panel-header > span:first-child) which
already had its own overflow:hidden + text-overflow:ellipsis.
Test now accepts either parent-level or inner-span overflow handling.
All 192 of the previously-failing or impacted tests now pass.
Browser-verified two issues with stage-311 tooltip rendering:
(1) Workspace panel header tooltips (NewFile, NewFolder, Refresh, etc.)
were being clipped because .panel-header had overflow:hidden. The
title span at `.panel-header > span:first-child` already has its own
overflow:hidden + text-overflow:ellipsis for the workspace name
truncation, so the parent doesn't need it. Changed .panel-header to
overflow:visible — verified tooltip now floats correctly below the
icon row, ellipsis on the title still works because the inner span
handles it locally.
(2) Strengthened tooltip body styling per browser screenshot review:
- Border: var(--border) (#2A2A45 dark slate) → var(--accent-bg-strong)
(gold-tinted at 15% alpha). Subtle brand-tied edge that's slightly
more visible against the very dark page background.
- Shadow: 6px/20px / 0.55 alpha + 1px ring at 0.25 → 8px/24px / 0.65
alpha + 1px ring at 0.35 + 1px inset highlight at 0.04 alpha. Gives
the tooltip more elevation against the dark theme so it reads as a
floating element rather than painted onto the background.
All 19 tooltip pytest checks still pass. Browser-verified on rail
(Tasks, Settings), composer (Attach files, Send message), and workspace
panel header (New folder) — screenshots delivered to maintainer for
visual sign-off.
Browser verification of the rail tooltip showed the 5px arrow ::before
pseudo-element was rendering as a tiny rectangle slice (not a triangle)
because the global `*, ::before, ::after { box-sizing: border-box }`
reset makes the colored border eat inward from a 10×10 box rather than
projecting outward from a 0×0 box. Adding `box-sizing: content-box`
inline to the pseudo fixes the geometry but at 11px text size and 5px
border-width the resulting triangle reads as visual noise rather than
a clear connector — multiple AI vision passes consistently couldn't
identify the arrow even when it was rendering correctly.
VS Code, Slack, and Linear's rail/icon-button tooltips all skip the
arrow for the same reason: spatial proximity at small sizes (an 8px gap
between trigger and tooltip body) is sufficient association without
the visual clutter of a tiny triangle.
Removes both ::before pseudo-rules. Tooltip body unchanged. Side
tooltip moved 12px → 8px gap (closer to trigger now that the arrow is
gone), bottom tooltip 10px → 8px for the same reason.
Browser-verified: rail Tasks tooltip rendering at 8/10 polish per
vision-AI assessment of the standalone tooltip body (solid surface bg,
solid border, warm-white text, 6px shadow + 1px ring, z-index 1500).
Co-authored-by: Jason Wu <jasonjcwu@users.noreply.github.com>
Stage 311 maintainer-side enhancements on top of @jasonjcwu's PR #1782,
addressing browser-verified issues + extending coverage to high-traffic
icon buttons:
(1) Clear native title when custom data-tooltip is present (the core bug fix):
- static/i18n.js: when data-i18n-title runs against an element that has
data-tooltip, sync data-tooltip AND removeAttribute('title'). Without
this, the slow ~1.5s native browser tooltip co-fires alongside the
fast custom CSS tooltip — exactly the bug #1775 reports.
- static/ui.js _applyDashboardStatus: same treatment for the dashboard
rail/mobile buttons (was setting btn.title=warning unconditionally).
- static/boot.js: added _setButtonTooltip() helper, replaced 6 direct
.title assignments (workspace toggle/collapse/clear, voice dictate,
voice mode active/inactive) with calls through the helper.
(2) Extend coverage to high-traffic icon buttons in static/index.html:
- Composer area (side tooltip): btnAttach, btnMic, btnVoiceMode,
btnWorkspacePanelToggle, btnSend.
- Workspace panel header (bottom tooltip): btnCollapseWorkspacePanel,
btnUpDir, btnNewFile, btnNewFolder, btnRefreshPanel, btnClearPreview.
- All 11 buttons gain has-tooltip[--bottom] class and data-tooltip,
lose their native title=. Total covered surfaces: rail (12), sidebar
nav-tabs (12), panel-head (31), composer/workspace icons (11) = 66.
(3) CSS polish (browser-verified visible improvement):
- z-index 60 → 1500/1501 so the tooltip clears all sidebar/panel
stacking contexts. Earlier verification showed the tooltip overlapping
the Filter conversations search input.
- background: var(--bg-strong, ...) → var(--surface) (solid #1A1A2E
instead of falling back via undefined cascade).
- color: var(--text, var(--accent-text)) → var(--text) (solid warm white
#FFF8DC instead of gold which clashed at body-text size).
- border: var(--accent-bg-strong) → var(--border) (#2A2A45 solid
instead of gold at 0.15 alpha — the old border was barely visible
and the arrow ::before triangle was invisible).
- shadow: 4px/0.45 alpha → 6px/0.55 alpha + 0 0 0 1px ring fallback.
- Added 150ms hover-onset delay (matches Cygnus's spec in #1775); 0s
dismissal-delay so quick mouse-aways don't leave the tooltip behind.
- Fixed has-tooltip--bottom arrow direction: was pointing down (wrong),
now points up at the trigger (border-color order corrected).
- Bumped offsets: side tooltip 10px → 12px (clearance from icon edge),
bottom tooltip 8px → 10px.
(4) Test fixes (the 2 CI failures):
- tests/test_cron_refresh_button_835.py: assertion accepts either
title= or data-tooltip= per #1775 (was hardcoded title=).
- tests/test_mobile_layout.py::test_profiles_sidebar_tab_present:
regex tolerant to additional utility classes (has-tooltip).
(5) Regression tests added to tests/test_css_tooltips.py:
- test_native_title_cleared_when_custom_tooltip_present: pins the
removeAttribute('title') call so we don't regress to dual tooltips.
- test_native_title_path_preserved_for_non_tooltip_elements: pins the
el.title fallback for elements without data-tooltip.
Browser-verified: all 72 has-tooltip elements have zero native title at
runtime (was 94 with native, 2 stuck via dashboard JS path).
Co-authored-by: Jason Wu <jasonjcwu@users.noreply.github.com>
- Add .has-tooltip CSS utility class with 300ms delay (vs ~1500ms native)
- Position-aware: right side for rail buttons, bottom for nav/panel buttons
- Arrow indicator pointing back at trigger element
- :focus-visible support for keyboard accessibility
- prefers-reduced-motion: no animation for users who opt out
- Replace native title="" with data-tooltip="" on all rail-btn, sidebar
nav-tab, and panel-head-btn elements in index.html
- Sync data-tooltip via data-i18n-title handler for locale switching
- 17 tests covering HTML coverage, CSS class definitions, and i18n sync
Closes#1775
When pasting screenshots into the composer (especially multiple in
sequence, now possible end-to-end with hermes-webui/hermes-swift-mac
PR #74) the user has no way to verify the right image attached. The
56x56 thumbnail in the chip is fine as a UI affordance but offers no
detail at all. Quote from the request:
When I hit Cmd+C and save an image to the clipboard and then paste
the clipboard out, I want to be able to click on any one of those
uploaded images that's inside the composer bar and have it zoom up
like a lightbox so I can see the image in full once it's been
pasted in to the composer input.
The lightbox infrastructure already exists for message-attached
images (static/ui.js:269 _openImgLightbox + the doc-level click
delegate at :298 for .msg-media-img). This PR extends the same
delegate to also fire on .attach-thumb composer chips:
- Clicking the thumbnail opens the existing image lightbox with the
blob URL as src and the file name as alt text.
- Audio/video chips are excluded (they have their own native
<audio> / <video> controls and don't render an .attach-thumb
img).
- SVG thumbnails (.attach-thumb attach-thumb--svg) qualify — they
are images visually.
- The chip's x remove button is a sibling, not an ancestor, of the
thumb — closest('.attach-thumb') from the button returns null,
so removing still works without lightbox interference.
Also updates static/style.css:
- cursor: zoom-in on .attach-thumb (was cursor: default — actively
misleading).
- Subtle :hover emphasis (brightness 1.05 + scale 1.04, 120ms ease)
so users discover the affordance before clicking.
5 regression tests in tests/test_composer_chip_lightbox.py pinning:
- delegate handles .attach-thumb on IMG elements
- delegate still handles .msg-media-img (no regression)
- audio/video chips do NOT render an .attach-thumb img
- cursor:zoom-in declared on the .attach-thumb selector
- hover emphasis rule present
Browser-verified live on port 8789:
- addFiles three distinct screenshot files (mimicking three Mac
sequential pastes) -> 3 chips, 3 thumbs, all distinct.
- Click thumb #2 -> lightbox opens with the right image, alt text
matches filename.
- Click x on chip #2 -> removes that chip, no lightbox.
- Escape key closes lightbox.
Companion PR on the Mac side:
hermes-webui/hermes-swift-mac#74 (unique filename per paste so
sequential pastes actually appear as distinct chips).
Refs nesquena/hermes-webui#1733.
- Add .model-opt-provider chip (right-aligned, muted) on every model row
that belongs to a provider group, making same-name models across
providers visually distinguishable at a glance.
- Add per-group model count to group headings: 'OpenRouter (47)'.
- Add subtle border-top divider between provider groups for visual
separation during scroll.
Scope: Shape A from #1425 — smallest change, ~15 LOC, no API churn.
Note: Settings model picker is a native <select> and already has optgroup
labels; this targets the custom dropdown used in the composer.
Closes#1425