Commit graph

364 commits

Author SHA1 Message Date
nesquena-hermes
12d1cec90a fix(#4930): don't render a fabricated turn duration from a stale pending_started_at
A settled turn that recorded no live duration fell back to
(Date.now()/1000) - session.pending_started_at. But pending_started_at is the
start of an IN-FLIGHT turn, so for a settled turn it is either stale (a session
that sat idle) or about a different pending turn — rendering a bogus
'Processed 15h 32m' on fresh conversations (#4930). Gate the fallback on a turn
actually being in flight (active_stream_id || pending_user_message); otherwise
return undefined (no duration shown) rather than a fabricated one. The recorded
live duration (_turnDuration / base.turn_duration) still takes priority, so this
only removes bogus durations, never a correct one.
2026-06-25 19:53:19 +00:00
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
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
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
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
Brett Yelverton
2a40ca4240 fix(streaming): restore mobile scroll-jank guard during live renders
A later streaming parse-cache change dropped the #MOBILESCROLL guard from
the live-stream render tick while leaving the helper and CSS in place.
On iOS PWA, Safari can paint a transient scrollTop=0 frame during streamed
DOM rebuilds, forcing the reader to the first/oldest message.

Re-add _fixMobileScrollJank() before streaming DOM work begins and pin it
with a regression test so the third layer of the mobile scroll fix cannot
be silently dropped again.
2026-06-24 17:20:40 +00:00
nesquena-hermes
d68afd7862 fix(#4800): key the streaming parse-cache on liveReasoningText too (Codex gate)
The parse-result cache keyed only on assistantText, but _parseStreamState() is a
function of BOTH assistantText AND liveReasoningText. A 'reasoning' SSE event (which
mutates liveReasoningText without calling _scheduleRender) landing between a token's
_scheduleRender(parsed) and the rAF firing made _doRender serve a parsed computed from
the older reasoning -> _renderLiveThinking could show stale thinking text. Keying the
cache on liveReasoningText too forces a fresh _parseStreamState when reasoning changed,
and also makes a canceled-then-rescheduled render miss the stale entry. Co-authored-by: wlknight
2026-06-23 22:52:51 +00:00
nesquena-hermes
1dbff4e708 Merge branch 'peek-4800' into gate/4800-stream-perf 2026-06-23 22:46:14 +00:00
nesquena-hermes
e26a6e47c0 Merge branch 'peek-4797' into stage/rodboev-4797-4796 2026-06-23 22:19:12 +00:00
wlknight
20a900be60 fix(perf): eliminate O(DOM) streaming render freeze on long agent answers
Introduce _safeSmdRenderer — a renderer wrapper that hooks smd's set_attr
to validate URL schemes inline as each DOM node is created, eliminating
the per-token _sanitizeSmdLinks(assistantBody) full-DOM scan that caused
progressive freezing on answers of 30,000+ chars (5,000+ DOM nodes).

Also add parse-result caching to _scheduleRender to avoid redundant
_extractInlineThinkingFromContent scans between the token event handler
and the rAF render callback.

Changes:
- New _safeSmdRenderer(el): URL scheme safety inline via set_attr hook
  (identical algorithm to proven _streamFadeRenderer.set_attr, no animation)
- _smdNewParser: non-fade path uses _safeSmdRenderer instead of bare
  default_renderer
- _smdWrite: removed _sanitizeSmdLinks(assistantBody) call — safety now
  handled by renderer's set_attr at node-creation time
- _scheduleRender(parsed): accepts optional cached parse result from
  token handler to avoid duplicate O(n) text scans
- Token handler: passes _parseStreamState() result to _scheduleRender

_sanitizeSmdLinks is preserved at _smdEndParser (stream-end safety net)
and in fallback paths where smd is not available.

All 166 existing tests pass.
2026-06-24 06:09:44 +08:00
Rod Boev
94e25ad494 fix(#4753): notify background desktop tabs without hiding streams 2026-06-23 17:10:03 -04:00
Frank Song
4c8e88ab2d fix: dedupe live compact worklog process prose 2026-06-23 23:03:04 +08:00
Frank Song
a4470bd30c Deduplicate live progress reasoning echoes 2026-06-23 12:51:16 +08:00
nesquena-hermes
20c70c3505 fix(#4730): clear assistantBody before smd re-init to prevent duplicate live text
Codex gate found: when window.smd becomes available after a segment already
rendered through the renderMd() fallback, _smdNewParser resets _smdWrittenText
to '' but does NOT clear the element, so the following _smdWrite(displayText)
appends the full accumulated segment on top of the existing fallback render —
duplicating the live assistant text. Clear assistantBody.innerHTML before re-init.

Co-authored-by: akrhin <akrhin@users.noreply.github.com>
2026-06-22 23:27:23 +00:00
sintez
f2abe50b7c perf: skip gateway poll when hidden/streaming, re-init smd parser before innerHTML fallback 2026-06-23 02:13:34 +03:00
Nathan Esquenazi
66ee1d6c81 fix(review): unbreak test_session_rotate_url_sync (PR shard-2 CI was red)
The #4720 done-handler comment (9 lines) pushed the tab-anchor localStorage
sync (`hermes-webui-session` setItem + _setActiveSessionUrl) past the 800-char
proximity window that test_session_rotate_url_sync asserts the #3018 session-id
sync lives within — so the PR's `test (3.x, 2)` shards were failing on master's
own regression guard. The full pytest suite caught it; the PR's gate slice did
not run it.

Fix (behavior unchanged — the _oldestIdx reset stays in the same place, before
the re-filter, guarded):
- Trim the inline comment to one line (the full rationale is in the #4720 commit
  + issue), so the completion block stops overflowing the guard window.
- The reset statement legitimately grew the completion block even bare, so widen
  test_session_rotate_url_sync's proximity window 800 -> 1000 (still well within
  the single done/settled handler; the guard's intent is preserved).

test_session_rotate_url_sync + test_issue4720_* both pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:44:18 -07:00
nesquena-hermes
ab71f4b906 fix: reset _oldestIdx on stream done so transcript stops jumping to first message (#4720)
After a turn completed on a long conversation that loaded truncated, the
viewport jumped to the first message. The done SSE handler replaced S.messages
with the full transcript payload and updated _messagesTruncated, but did NOT
reset _oldestIdx the way the canonical full-load paths do (sessions.js
_ensureMessagesLoaded, ui.js loadSession).

The read-position rework (#4613) keys the scroll anchor on an absolute index
(sessionIdx = _oldestIdx + rawIdx; rows tagged data-session-msg-idx). On done
the handler also expands the render window from the tail window (50) to all
messages, shifting every rawIdx. With _oldestIdx left stale, the captured
anchor resolved to an earlier row and the fallback used the raw scrollTop
against the now-taller full transcript, landing near the first message.

Reset _oldestIdx from d.session._messages_offset||0 alongside _messagesTruncated.
The done payload (_session_payload_with_full_messages) embeds the full transcript
and carries no offset, so this resets to 0 and keeps the absolute anchor aligned
with the rebuilt rows. No-op for non-truncated sessions; pinned-at-bottom readers
still follow to bottom (shouldFollowOnDone path unchanged).

Both gates clean (Codex SAFE TO SHIP, Opus ship-it) — verified the done payload
is always a full transcript with no offset, and every other _oldestIdx consumer
becomes more consistent.
2026-06-22 19:26:07 +00:00
nesquena-hermes
12ff6e807d
stage #4681 (franksong2702): stop legacy interim toggle leaking into anchor Worklog (#4687)
When data-anchor-scene-live-owner=1, the #2403 legacy interim_assistant collapse toggle
('Show N earlier updates') is now cleared in renderLiveAnchorActivityScene cleanup AND the
interim_assistant SSE handler skips creating it. Guards both directions; regression test
locks the invariant. messages.js + ui.js. 289 interim/anchor/worklog tests pass.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-22 01:48:07 -07:00
nesquena-hermes
0456003c84
stage #4577 (dso2ng): keep failed steer from cancelling active runs (#4670)
A failed /steer (or busy-mode steer) no longer silently falls back to interrupt+queue
(which cancelled the active stream). _trySteer now returns delivered:bool, restores the
draft + leaves the stream running on failure; send() only clears staged files when
_steerDelivered===true. Backend hunk comment-only (failure paths already returned
{accepted:False}, never touched CANCEL_FLAGS). i18n value-only on pre-existing keys
(locale-parity preserved, 173 locale tests pass). 48 own tests pass.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: dso2ng <dso2ng@users.noreply.github.com>
2026-06-21 23:22:51 -07:00
nesquena-hermes
1f4e70a5d4
stage #4652 (nesquena-hermes): stop /api/reasoning storm + memoize YAML config parse (fixes #4650) (#4659)
Two-layer perf fix for the /api/reasoning storm dropping streaming to ~2 tok/s:
- ui.js: syncReasoningChip() serves the cached chip on routine topbar syncs, only
  re-fetching when the model/provider key (_lastReasoningFetchKey) changes — restores
  pre-a9ce2889 behavior while keeping its model-switch-refresh intent.
- config.py: memoize _load_yaml_config_file keyed on (path, st_mtime_ns, st_size) under
  a lock; cache the RAW parsed dict and re-run cheap _expand_env_vars per call (cache stays
  correct vs os.environ and read-modify-save callers get a fresh structure; on-disk edits
  picked up via mtime/size). Code byte-faithful from PR head f868f10a1ad7. 9 own tests pass.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
2026-06-21 21:45:13 -07:00
nesquena-hermes
0dba26d06e
Release v0.51.562 — selected-context quote cards (#4380) (#4629)
* stage #4380 (santastabber): render selected context as quote cards + CHANGELOG

Evolves the shipped #2543 named-context-blocks: composer chips -> quote cards
(accent rail, editable label w/ rename, remove, clipped excerpt) + thread render
of labeled selections as <figure> cards. Label+quote esc()'d on every path
(adversarial harness 0 survivors). 3 new i18n keys in all 13 locales (parity green).
Code applied from PR head 2eefc11c02a7 (own 3-dot contribution, CHANGELOG re-stamped).

Co-authored-by: santastabber <santastabber@users.noreply.github.com>

* fix #4380 Codex gate finding: selection-only reply must enable the primary Send button

Codex CORE: #4380 moved selected context out of the textarea into _pendingSelections,
but _composerHasContent() only checked textarea+files -> getComposerPrimaryAction()
returned disabled for a selection-only reply (desktop Enter calls send() directly so
it worked, but click/tap/mobile users were blocked). Fix: expose
window._hasPendingSelections() from messages.js, fold it into _composerHasContent(),
and call updateSendBtn() from _renderSelectionChips() so add/remove/clear refreshes
the button. + regression test.

* Release v0.51.562 — Release TU (selected-context quote cards; #4380)

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
2026-06-21 12:24:42 -07:00
nesquena-hermes
236b165b7a harden(#4587): align scene-builder final-answer filters to the renderer's >=0.9 ratio
Codex gate (SILENT): the scene-builder/hydrator _anchor_scene_row_looks_like_final_answer
(messages.js + routes.py) used a loose '>=80 + prefix' rule while the renderer
_anchorSceneProseMatchesFinalAnswer uses '>=80 + ratio>=0.9'. A short intermediate-prose row that
was a prefix of the final answer could be dropped from the persisted scene by the looser builder,
leaving the stricter renderer no row to show -> silent prose loss. Aligned both filters to the
renderer's rule (drop fewer -> preserve more intermediate prose). Matrix already GREEN; this closes
the edge the fixture didn't exercise.
2026-06-21 06:03:44 +00:00
nesquena-hermes
6951d10ae5 rebase #4411 onto master (v0.51.542) for deep-review — Stable Assistant Turn Anchors capstone
franksong2702's activity_scene_v1 anchor-scene model: live-stream/settled/refresh/re-entry
show the same ordered turn. Rebased clean (no conflicts) onto current master.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-20 21:48:55 +00:00
jja881
53affd6936 fix: address review feedback — remove redundant clause, add guard and tests
- Remove the redundant 4th OR clause from _catalog_has_provider() — the
  existing C1–C3 already handle compound provider IDs correctly (C2 IS
  a normalized-vs-raw comparison; the 4th clause was a strict subset).
- Add && _activeProvider guard to _isCrossProviderPick in messages.js
  to prevent spurious True when the profile has no configured default
  provider (reviewer's optional hardening suggestion).
- Add test_catalog_has_provider_compound_ids.py verifying that compound
  provider IDs like 'custom:glm-free-relay' are correctly recognized by
  the existing C1–C3 clauses.
2026-06-20 18:44:25 +00:00
jja881
96265f6ae4 fix: preserve cross-provider model selection across conversation turns
Two-part fix for the model selector jumping back to the profile default
after the first message when a cross-provider model (e.g. a custom relay
like custom:glm-free-relay) is selected.

Backend (api/routes.py):
- _catalog_has_provider() now matches compound provider IDs (e.g.
  custom:zenmux-relay) whose normalized form (custom) is a raw
  catalog provider_id.  Previously the three existing checks missed
  normalized-vs-raw comparison for compound hints, causing the catalog
  lookup to fail and the fallback branch to silently revert the model
  to the profile default on every non-explicit turn.

Frontend (static/messages.js):
- The explicit_model_pick marker was consumed after the first send(),
  so subsequent sends lost the signal and the server repaired the
  model back to the profile default (#3737 regression).
- Now, when the session has a non-default model from a different
  provider than the profile's active provider, every send() sends
  explicit_model_pick=true so the server honors the user's choice
  across the entire conversation.
- The pending onchange marker is still consumed after the first send
  (unchanged behavior); only the explicit_pick inference is broadened.
2026-06-20 18:44:25 +00:00
nesquena-hermes
15fe6add30 stage #4524: clarify 409-terminal (id-guarded, loading-cleared) + SSE-notify on expiry (#4504) + v0.51.534 CHANGELOG 2026-06-20 05:36:56 +00:00
nesquena-hermes
ca19574b75 fix(notifications): make hidden-tracker stream-owned + clear on terminal paths (#4416 Codex gate)
Codex SHIP-WITH-FIXES: non-done terminal paths (apperror/cancel/stream-error
finalize) didn't clear _STREAM_WAS_HIDDEN, so a stale wasHidden=true could be
inherited by a later same-sid reconnect -> spurious notification. Make each
entry stream-owned ({streamId, wasHidden}) so a stale entry from a prior stream
is never trusted (reconnect keeps state only when streamId matches; done reads
via the owned entry), and add _clearStreamHidden(activeSid, streamId) on the
apperror/cancel/finalize-fallback terminal paths (belt-and-suspenders).
2026-06-19 23:54:23 +00:00
nesquena-hermes
d473f6c5a6 fix(notifications): notify on completion if tab was hidden during the stream (#4416)
A background tab's SSE is throttled by Chromium, so the stream 'done' event
arrives late (after the user returns, document.hidden=false) and the
response-complete notification was suppressed by the live visibility check.
Track per-stream whether the tab was hidden at ANY point during the stream
(_STREAM_WAS_HIDDEN, set at attach, OR'd by one idempotent visibilitychange
listener, read+cleared at done) and pass forceHidden accordingly.
sendBrowserNotification bypasses ONLY the live visibility gate on forceHidden,
still honoring the user's notifications-enabled setting — so a watched stream
stays silent (matches Slack/Discord/Gmail/Claude) but a backgrounded one
notifies. Correct fix for the #4416 root cause; supersedes #4421's always-fire
approach (which would also fire when the tab was never hidden).

Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>
2026-06-19 23:48:42 +00:00
Frank Song
36e992415b Fix post-start attach resilience for #4481 2026-06-19 17:51:36 +08:00
nesquena-hermes
46f234f4fa release: v0.51.509 — notice when connected gateway can't do approvals (#4456, #4300)
Includes a showToast arg-order fix (4000ms,'warning') caught at gate.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>

Closes #4456
2026-06-19 03:14:08 +00:00
nesquena-hermes
f94508994f release: v0.51.505 — allow macOS Mail message: links in chat markdown (#4420, #4319)
Co-authored-by: bergeouss <bergeouss@users.noreply.github.com>

Closes #4420
Closes #4319
2026-06-19 01:58:28 +00:00
bergeouss
d812f70116 fix(#4313): guard EventSource.close() calls to suppress ERR_ABORTED noise
Add readyState !== CLOSED (2) guards to all EventSource.close() calls across
messages.js, sessions.js, panels.js, and terminal.js. Chromium reports
net::ERR_ABORTED when close() is called on an already-closed EventSource,
polluting the console during normal lifecycle transitions (session switch,
panel close, stream teardown).

Key hotspots:
- closeLiveStream() double-close via onerror → _closeSource chain
- _closeSessionEventsSSE, _gatewaySSE.onerror missing try/catch
- _kanbanStopPolling, terminal close handlers

20 close() calls guarded across 4 files, no structural changes to stream
lifecycle.
2026-06-18 17:25:18 +00:00
nesquena-hermes
8246b4aad4 feat(#2543): collect selected chat text as named context blocks
"Reply with selection" no longer appends the selected text straight into
the composer. It now adds a named chip ("Context N") to a row above the
message box (composerSelectionChips). Chips can be renamed (double-click,
esc()-escaped) or removed (x); multiple stack. On send(),
_flushSelectionBlocksToComposer() materializes pending blocks into the
composer as **Name:** + markdown-quoted text, then the normal send path
runs unchanged on composer.value.

Regression-safety:
- _flushSelectionBlocksToComposer early-returns when no blocks are pending,
  so send() is byte-identical for anyone who never uses the feature.
- loadSession() AND newSession() both clear pending blocks before replacing
  S.session, so chips never leak across a sidebar switch OR a New Chat
  (the New Chat clear + its regression test were added per Codex review).
- chip name/text rendered via esc(); XSS-safe.

Verified live (headless CDP drive): add/render/remove/rename, flush
produces correct markdown, empty-flush no-op, session-switch clears,
injected <img onerror=...> chip-name escaped (not executed).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-17 20:15:02 +00:00
nesquena-hermes
4817e3a6ab
Release PB: honest notification-permission controls (#4118) (#4265)
ai... rodboev's #4133, notification-feature ONLY (dropped the unrelated
'invalidate sidebar cache on WAL' commit — that's a separate good fix to split
out). Button now reflects live Notification.permission instead of silent no-op.

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-15 13:42:46 -07:00
nesquena-hermes
5812df5f7b Merge #4112 (skill bundles in WebUI slash commands, #4087) onto master 2026-06-13 23:13:28 +00:00
nesquena-hermes
f7047d4d19 Merge #4072 (wire /credits through WebUI command dispatch, #4071) onto master 2026-06-13 21:15:22 +00:00
nesquena-hermes
5a33a51cec Merge #4131 (badge pending prompts in document title, #1694) onto master
# Conflicts:
#	CHANGELOG.md
2026-06-13 19:19:24 +00:00
Rod Boev
e5640a42b9 Keep prompt-attention title badges from breaking title ownership 2026-06-13 15:05:03 -04:00
Rod Boev
4329768288 feat(topbar): badge pending prompts in document title (#4121) 2026-06-13 15:04:20 -04:00