Commit graph

2043 commits

Author SHA1 Message Date
Rod Boev
7d617d91f7 fix(#4771): block duplicate approval submits 2026-06-25 13:09:42 +00:00
Rod Boev
0b12b3b1fb test(#4771): cover both approval rejection paths 2026-06-25 13:09:42 +00:00
Rod Boev
528fa72390 fix(#4771): keep gateway approval failures actionable 2026-06-25 13:09:42 +00:00
luandnh
91af4de845 fix: protect pipes inside <code> tags, not backticks
Inline code pass converts `code` to <code>...</code> BEFORE the table
pass runs, so the _protectPipes backtick regex never matched. Changed
to protect pipes inside <code>...</code> tag pairs instead.
2026-06-25 08:25:58 +00:00
luandnh
c1c039ff80 fix: protect pipes inside backtick code spans in renderMd tables
The _protectPipes function protected | inside brackets but not inside
backtick inline-code spans, so || in `code || here` inside a table
cell was split into extra columns. Added a second replace for matched
backtick pairs in the same iterative loop.
2026-06-25 08:25:58 +00:00
luandnh
f82e786c99 fix: table pipe | inside inline code breaks markdown table rendering
The smd parser treated | characters as table column separators even
when inside backtick inline code (token 11). Added e.token!==m (code
span) guards in both table | handlers so pipes inside code are
output as literal text instead of creating table cells.
2026-06-25 08:25:58 +00:00
nesquena-hermes
89d7fae347 fix(#4622): restore full body for CAPPED-PREVIEW settled snippets, not just empty
Codex gate caught that the enrich only restored when the settled snippet was
fully EMPTY — but the backend persists a 4000-char capped preview
(_TOOL_RESULT_SNIPPET_MAX), so long terminal/tool output settles to a truncated
4000-char prefix, not empty (the actual #4622 symptom). Now also restore when
the settled snippet is a bounded preview of the live one (live longer + settled
is a prefix + settled length >= the 4000 cap), without clobbering a genuinely
short non-preview value. Adds capped-preview restore + short-prefix-not-clobbered
tests (proven non-vacuous).
2026-06-25 06:51:33 +00:00
nesquena-hermes
8d2d8b1f98 fix(#4622): trim dead enrichment branches per Opus gate
Opus + Codex both SAFE TO SHIP. Opus flagged two cosmetic redundancies (not
blockers): the snippet guard checked payload.result/payload.output which
_anchorSceneToolRowFromCall never sets, and the tool.result/tool.output writes
are never read by the consumer _anchorSceneToolCallFromRow (it reads
tool.snippet||payload.snippet||payload.result||payload.output). Removed both —
tool.snippet/payload.snippet is what actually restores the visible body.
Behavior-identical; 18 settled/reconnect tests still green.
2026-06-25 06:40:27 +00:00
nesquena-hermes
ef3092434f fix(#4622): restore tool-card result body on the settled rebuild path
In the Transparent Stream / Worklog view, terminal tool cards lost their stdout
and patch/edit cards lost their diff AFTER a turn settled (live cards were fine).

Root cause (proven via a node harness driving the real settled chain
_anchorSceneRowsByMessageIndex -> _anchorSceneToolRowFromCall ->
_anchorSceneToolCallFromRow): the settled rebuild builds each tool row from the
persisted messages[].tool_calls (state.db/sidecar), which can carry only a short
preview — or, on a cold/paginated load, nothing — for the result body. The full
output lives on the live S.toolCalls entry at settle time, but the dedup loop in
_anchorSceneRowsByMessageIndex SKIPPED the matching live entry (continue) instead
of restoring the missing body onto the surviving settled row. So buildToolCard
got tc.snippet='' -> empty output region, and the patch/edit diff (detected from
the snippet) never rendered. This also blanks the transparent 'Output' tab and
the 'Full' (detailed-input) tab.

Fix: new _enrichSettledToolRowBodyFromLive() restores snippet/result/output,
command, and args onto a settled row from the matched live call, ONLY when the
settled value is empty (never clobbers a genuinely persisted body). Wired into
the dedup loop: on a tid match that would have been skipped, enrich the existing
pooled row instead of dropping the live data.

The prior #4625 guard only exercised the LIVE buildToolCard path, which is why it
stayed green while the settled path regressed. New
tests/test_issue4622_settled_toolcard_body.py drives the full SETTLED chain and
asserts: terminal stdout restored + full output in data-full + transparent Output
tab non-empty; patch diff body restored + renders as a diff + Full-tab args kept;
genuine persisted body not clobbered; args restored when dropped; no-live-match
leaves the row untouched; no duplicate row after enrich. Verified to FAIL on clean
master (assertions) and pass with the fix. Also registered the new helper in the
and now references the new symbol).

Reusable proof harness saved to ~/WebUI/qa/harnesses/.

Closes #4622.
2026-06-25 06:40:27 +00:00
nesquena-hermes
856aa1d48d fix(#4891): show header in read-only Memory sections + Profiles help view
Gate (Codex) caught two SILENT regressions in the original fix: explicitly
hiding the header in the 'else'/'empty' branches also hid populated read views.
- _setMemoryHeaderButtons('read') now shows the header for ALL read views
  (incl. read-only Project Context / External Notes), gating only the Edit btn.
- _renderProfileConceptHelp() now uses a new 'help' header mode (shows header,
  hides all action buttons) instead of 'empty' (which hid the help title).
Adds tests/test_issue4891_detail_header_visibility.py (proven non-vacuous).

Co-authored-by: luandnh <luandnh@users.noreply.github.com>
2026-06-25 06:20:20 +00:00
luandnh
561369a261 fix: task detail actions hidden on mobile PWA
The main-view header (and its action buttons) used a CSS :has(.main-view-title:empty)
rule to stay hidden until the title populated; on some mobile browsers / PWA
WebViews that pseudo-class doesn't re-evaluate after textContent is set
dynamically, leaving the header permanently hidden. Explicitly set header
display in the read/create/edit/closed header-button setters (tasks, skills,
memory, workspaces) instead of relying on :has() reactivity.

Co-authored-by: luandnh <luandnh@users.noreply.github.com>
2026-06-25 06:08:00 +00:00
nesquena-hermes
3acd802588 fix(#4856): mobile transcript collapses unscrollable — overflow-x:hidden coerces overflow-y:auto
The mobile .messages-inner rule used overflow-x:hidden, which per the CSS
overflow spec coerces overflow-y from visible to auto — turning the inner
into a scroll container. A scroll container's min-height:auto resolves to 0,
so the inner (a flex-shrink:1 item inside the .messages column flexbox)
collapsed to the viewport height instead of growing to its content. The
transcript then overflowed the clipped inner rather than the .messages
scroller, leaving nothing to scroll and pinning scrollTop at 0 — every
open/send/receive stranded mobile users at the top with no way to scroll down.

Fix: overflow-x:clip suppresses horizontal overflow (the original #4583/#4553
intent) WITHOUT coercing overflow-y or creating a scroll container, so
min-height:auto stays min-content and the inner grows to full height.

Verified empirically via headless Chrome mobile emulation (390x844, mobile=true):
scrollableDelta 0 -> 7746; open/send/receive all land at bottom; mid-scroll
position preserved across re-render. Desktop unaffected.

Updated #4553 test (clip satisfies its horizontal-pan goal) + added dedicated
2026-06-25 05:18:32 +00:00
nesquena-hermes
fee11eab49 fix(#4746): make gallery Install button text theme-readable
The .extension-gallery-install-btn used color:#fff on a var(--accent) background;
with the gold accent (#FFD700 dark / #B8860B light) the white text was washed out /
low-contrast. Switched to the readable outlined treatment (accent-tinted background +
var(--accent-text) text + var(--accent) border), matching the uninstall button + the
steer-recovery-retry convention. Verified readable on Gallery + Installed tabs in
both dark and light.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-25 04:27:12 +00:00
Rod Boev
ee7ff6ed33 fix(extensions): harden gallery against live registry and security review findings 2026-06-25 04:21:50 +00:00
Rod Boev
ddd5c8d936 feat(extensions): add gallery browse, install, and uninstall (#4746) 2026-06-25 04:21:50 +00:00
Brett Yelverton
2df199c554 fix(kanban): add missing vi locale keys for kanban filter i18n
The Vietnamese locale block (added upstream after the kanban-unassigned
branch was authored) was missing two i18n keys:
- kanban_tasks_hidden_by_filters
- kanban_clear_filters

These keys are required by the kanban hidden-by-filters UX which shows a
'Clear filters' button when text search hides all tasks. Both keys use
English text matching the pattern of untranslated new keys in all other
locale blocks.
2026-06-25 03:54:32 +00:00
Brett Yelverton
dfcd94a808 fix(kanban): show unassigned tasks as explicit profile lane on mobile
- Add KANBAN_UNASSIGNED_LANE constant and _kanbanLaneKey/_kanbanLaneLabel
  helpers so unassigned tasks map to a stable internal key '__unassigned__'
  rather than a locale-dependent translated string
- _kanbanLaneNames now places 'default' first, other assignees alphabetically,
  and the Unassigned lane last
- _kanbanRenderProfileLanes filters by _kanbanLaneKey, renders label via
  _kanbanLaneLabel, and adds kanban-profile-lane-unassigned CSS class
- _kanbanTaskMeta always emits the unassigned label so sidebar shows it too
- _kanbanRenderBoard checks unfiltered total and shows a 'Tasks hidden by
  filters' state with a Clear filters button when text search hides all tasks
- Add kanban_tasks_hidden_by_filters and kanban_clear_filters i18n keys to
  all 13 locale blocks
- Add .kanban-profile-lane-unassigned header border using warning token

Fixes: on Dynasty board mobile, Ready=3 CLI but default lane showed Ready=0
because those tasks were unassigned and had no visible lane.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 03:54:32 +00:00
nesquena-hermes
c315839836 fix(#4749): make steer-recovery button text theme-readable (explicit color tokens)
The .steer-recovery buttons set background:var(--surface) but no color, so native
<button> text fell back to the UA ButtonText (dark) color -> dark-on-dark unreadable
'Retry'/'Dismiss' in dark mode. Added explicit color:var(--text) on buttons + label,
styled Retry as an accent-colored primary (color+border var(--accent), bold), and
white-space:nowrap so labels don't wrap on mobile. Verified readable across dark,
light, and mobile viewports.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-25 03:37:26 +00:00
Rod Boev
b70949f484 fix(steer): rename typo, catch retry rejection, update JSDoc (#4749) 2026-06-25 03:23:36 +00:00
Rod Boev
d28f39a0b4 fix: add steer_fail/recovery i18n keys to all non-English locales 2026-06-25 03:23:36 +00:00
Rod Boev
5312f96746 fix: move _trySteer JSDoc above its function definition 2026-06-25 03:23:36 +00:00
Rod Boev
c11049646f fix: use real CSS tokens for recovery bar and update stale test assertion 2026-06-25 03:23:36 +00:00
Rod Boev
764d5b8601 fix(#4749): show steer failure reason and offer retry/dismiss recovery 2026-06-25 03:23:36 +00:00
Rod Boev
c109ed9113 fix(plugins): align activation-state sort with badge precedence (#4713) 2026-06-25 02:57:06 +00:00
Rod Boev
e3a1540d32 fix: align sort partition with badge for inactive exclusive providers 2026-06-25 02:57:06 +00:00
Rod Boev
7c1cd99961 fix(#4713): dark-mode disabled badge contrast and active-first plugin sort 2026-06-25 02:57:06 +00:00
nesquena-hermes
0a6947c2d2 fix(#4754): namespace dismissed approvals by session (gate finding)
Codex CORE: the dismissed-set was keyed by bare approval_id, but gateway/run
sources preserve externally-supplied IDs without session namespacing — so two
sessions carrying the same approval_id collide and dismissing in session A hides
session B's still-pending approval. Fix: _approvalDismissKey(sid, approvalId)
builds a session-namespaced composite key, threaded through _isApprovalDismissed,
_markApprovalDismissed, _unmarkApprovalDismissed + all 5 call sites (showApprovalCard,
dismissApprovalCard, respondApproval, no-pending poll cleanup). Updated 4 stale
assertions to the namespaced signatures + added a functional node-driver
cross-session-collision regression (proven non-vacuous).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-25 02:15:47 +00:00
Rod Boev
0258599a46 fix(approval): clear pending attention state on dismiss (#4754) 2026-06-25 02:04:51 +00:00
Rod Boev
820362ca14 fix: scope un-dismiss to session-local pending, not global _approvalCurrentId 2026-06-25 02:04:51 +00:00
Rod Boev
2956804a14 fix(#4754): persist approval card dismissal across tab switches and restarts 2026-06-25 02:04:51 +00:00
Rod Boev
36742abcee fix(#4875): widen continuation-prompt filter to cover output-length and tool-call variants 2026-06-25 01:21:08 +00:00
Rod Boev
f43bf2df6a docs(#4856): update stale call-site comment to match overflow-anchor semantics 2026-06-25 01:06:09 +00:00
Rod Boev
0668cfa77c fix(#4856): suppress Chromium scroll-anchor re-selection during mobile DOM rebuild 2026-06-25 01:06:09 +00:00
Charles Bine
867471c7d3 fix(#484): drop noisy "parse failed" note on JSON/YAML fragments
LLMs routinely emit JSON/YAML *fragments* in fenced blocks — a bare
`"key": "value"` line, snippets with `...`, trailing commas — which
legitimately fail JSON.parse. initTreeViews() stamped an unstyled
`<div class="tree-parse-note">parse failed</div>` below such blocks,
producing bare white text under an otherwise-clean code box.

The note was incidental, not designed: it was bolted onto the YAML
lazy-load commit (29a31a6e), has no CSS, no behavior test, and originally
even fired on *valid* YAML before the CDN script finished loading (later
fixed). The block still renders as syntax-highlighted raw, so the note
added only noise.

Remove the note insertion; fall through silently to raw view. The per-block
Raw/Tree toggle, YAML lazy-load, and the #484 default-view config are all
unchanged. The `parse_failed_note` i18n key is intentionally retained (kept
green by test_chinese_locale.py) to avoid touching all 13 locales.

Verification: ./scripts/test.sh tests/test_issue484_json_tree_viewer.py
tests/test_chinese_locale.py — 29 passed.
2026-06-24 22:14:27 +00:00
Rod Boev
2ca65abefa fix: use JSON.parse for bracket-prefixed lines instead of blanket suppression 2026-06-24 20:35:04 +00:00
Rod Boev
29604e5e7b fix: fall back to tc.snippet for cold-loaded tool results 2026-06-24 20:35:04 +00:00
Rod Boev
c19aaae27f fix(#4752): show tool result instead of call args in collapsed card preview 2026-06-24 20:35:04 +00:00
nesquena-hermes
57a78e9284 fix(#4728): clear stale custom:* base_url + default wins without loaded session (gate findings)
Two gate-found CORE/SILENT edges fixed on-branch:
- api/config.py: switching the main default to a different custom:* provider kept
  the OLD provider's base_url, routing New Chat to the wrong endpoint. The
  base_url-clear on provider-change now covers custom:* too (was exempted).
- static/sessions.js: newSession() skipped the configured default when S.session
  was null but the picker held stale state (e.g. after deleting the last session),
  inheriting the stale model. Default now wins over legacy picker regardless of
  loaded-session state (explicit empty-composer override still takes priority).
- Updated 2 stale source-grep precedence assertions + widened a brittle 6000-char
  newSession() slice window to 7000 (assignment block grew; intent unchanged).
- 2 new non-vacuous regression tests (custom base_url clear; precedence).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 20:17:29 +00:00
Rod Boev
23b3206f83 fix(#4728): guard settings default-model capture when no option is selected 2026-06-24 20:09:30 +00:00
Rod Boev
bede035a5e fix(#4728): persist provider-only default-model changes end to end 2026-06-24 20:09:30 +00:00
Rod Boev
69d202b94a fix(#4728): treat provider-only default changes as settings edits 2026-06-24 20:09:30 +00:00
Rod Boev
eba089fd90 fix(#4728): preserve fallback-order guarantees for default-model routing 2026-06-24 20:09:30 +00:00
Rod Boev
cdb44fb7e0 fix(#4728): sync saved default model provider into new chat state 2026-06-24 20:09:30 +00:00
Rod Boev
dde3845171 fix(#4728): keep default-model requests on the configured provider 2026-06-24 20:09:30 +00:00
Rod Boev
d8b58da018 fix(#4728): use configured default for new conversations 2026-06-24 20:09:30 +00:00
nesquena-hermes
3c8cfdc589 fix(#4811): regenerate row_id/seq from final order index so anonymous tool rows don't collide (gate finding)
Codex gate found a SILENT drop: reconnect scene rows are built with orderIndex=0,
so two tools WITHOUT a tool id at the same message index both got row_id
'...:tool:<idx>:0'; _completeSettledAnchorSceneForTurn dedupes by row_id, silently
dropping the second. Now the emit loop rewrites order_index AND regenerates the
index-derived row_id + seq + identity.seq from the final per-bucket position
(tid-based tool row_ids, already unique, are left untouched). Regression test
added + proven non-vacuous.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 19:48:56 +00:00
Rod Boev
f330fa7f44 perf(reconnect): pre-index toolCalls and remove unreachable guard (#4811) 2026-06-24 19:39:40 +00:00
Rod Boev
057b878136 fix(#4811): sort reconnect scene rows by phase and started_at chronology 2026-06-24 19:39:40 +00:00
nesquena-hermes
c21d30e02b fix(#4757): defer window-drop cleanup a tick so target ondrop runs first (gate finding)
Codex re-gate: the capture-phase window 'drop' listener fired BEFORE the target
el.ondrop, clearing _wsActiveDragPath before _isWorkspaceTreeMoveDrag/_wsDragSrcPath
ran — re-breaking the macOS stripped-MIME move it set out to fix. setTimeout(_clearWsDragData,0)
lets the real drop handler complete, then clears the lingering flag.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 19:13:59 +00:00
nesquena-hermes
63e3f91654 harden(#4757): gate stripped-MIME drop on text/plain===tracked path + robust drag-flag cleanup (gate finding)
Codex gate found a SILENT hazard: if a workspace drag is abandoned without
ondragend firing (cancel/drop-outside/blur), _wsActiveDragPath lingered and a
later FOREIGN text/plain drag could be misread as a workspace move. Hardening:
- _wsDragSrcPath now only trusts the active flag when the drop's own text/plain
  EQUALS _wsActiveDragPath (a foreign/empty text drag resolves to '' -> no move).
- Shared _clearWsDragData() called from ondragend, a drop finally, and
  window-level dragend/drop/pagehide/blur fallbacks (lost-dragend cleanup).
- 2 regression tests (stale-flag foreign-text -> empty; _clearWsDragData resets),
  proven non-vacuous.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-24 19:06:30 +00:00