Commit Graph
649 Commits
Author SHA1 Message Date
allenliang2022 955e31b9a1 fix(chat): do not collapse a pure-prose turn into a worklog (stream-done jump)
A turn that streamed only prose (a long plain-text answer, or a degeneration
burst that floods the body with repeated tokens) still projected an anchor
activity scene whose activity_rows were all prose/terminal — zero tool/thinking
rows. The settle path promoted it to a collapsed worklog anyway (the gate only
checked activity_rows.length), hiding the whole answer and shrinking the
transcript by the full streamed height at STREAM_DONE, so the browser clamped a
bottom-pinned viewport back to the top (the 'jump back' report).

Add a worklog-worthiness predicate at both gates: the generation gate
(_anchorSceneHasWorklogWorthyRows in messages.js, decides whether to attach a
scene at all) and the render gate (_anchorSceneSceneHasWorklogWorthyRows in
ui.js, defense-in-depth for already-persisted all-prose scenes). A scene is
worklog-worthy only if it has >=1 tool/thinking row or a compression lifecycle
card; pure prose is not, so the turn renders as normal visible prose and the
viewport stays pinned at the bottom.

Behavioral + structural tests in tests/test_pure_prose_turn_not_worklog.py.
2026-06-29 17:06:45 +00:00
allenliang2022 2aa1d168b0 fix(chat): scope STREAM_DONE keep-open to just-settled turn via one-shot token
Address maintainer must-fix: _shouldKeepSettledWorklogOpenForPinnedFollow()
keyed only on global pin state, so every historical settled worklog re-expanded
on any pinned re-render — defeating the compact default. Gate the keep-open
exception on a one-shot token = the just-settled stream id (armed at STREAM_DONE
renderMessages, disarmed right after), threaded as _shouldKeep...(streamId).
Unpinned path unchanged. Replace source-string test with a behavioral Node test
that drives a second pinned turn and asserts it collapses.
2026-06-29 17:06:44 +00:00
allenliang2022 0d198d677d test(chat): document keep-open precedence over live collapse state
Follow-up to maintainer review on #5058: make the pinned-follower helper comment
explicit that avoiding the visible STREAM_DONE shrink jump intentionally takes
precedence over a transient user-collapsed live worklog while the reader remains
pinned. Add a source lock so that intent stays documented.

Focused scroll locks remain green (22 passed), node --check clean.
2026-06-29 17:06:44 +00:00
allenliang2022 5fd3b1c9d7 fix(chat): prevent stream-end worklog collapse jump for pinned readers
A new post-#4970 scroll-jump class surfaced after Release YH: when a streamed
assistant turn with a large live worklog/tool trace settles, the settled compact
worklog collapses hundreds of pixels at STREAM_DONE. The reader is still pinned
and scroll state is correct, but the net scrollHeight shrink makes the browser
clamp scrollTop down by the same amount, which is visible as a large backward
jump.

Fix by keeping the just-settled activity worklog open for pinned followers
(_scrollPinned && !_messageUserUnpinned), so the live->settled DOM swap is
height-stable. Unpinned readers still get compact settled worklogs and keep their
viewport normally. Also make _anchorSceneWorklogGroup respect opts.collapsed;
previously it hard-coded collapsed: !live so the caller could not request an open
settled group.

Regression locks assert the pinned-follow helper, that settled rendering passes
collapsed:!keepSettledWorklogOpen, and that _anchorSceneWorklogGroup honors an
explicit opts.collapsed. Verified locally with a Playwright repro: before the
fix a large tool-worklog answer produced scrollHeight -367/-422px and scrollTop
-367/-422px at STREAM_DONE; after the fix nShrinks=0 and nBack=0. Local focused
scroll locks pass: 22 passed.
2026-06-29 17:06:44 +00:00
santastabber 178db13512 fix: authorize session html media artifacts 2026-06-29 05:17:33 +00:00
justanotherAIcontributorandgreptile-apps[bot] 99555d592c Update static/ui.js
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-29 04:44:16 +00:00
Stacey2911 dd658d7a04 fix(workspace): align Workspace Todos rendering with sidebar 2026-06-29 04:44:16 +00:00
Rod Boev 6d80dd41d6 fix(updates): restart gateway after in-app agent update (#5156) 2026-06-29 03:44:06 +00:00
nesquena-hermesandPaladin173 6d770f1c05 fix(composer): refit on .composer-left resize, not just footer (Codex gate #4657)
Codex flagged that the fit ResizeObserver only watched .composer-footer, whose
outer width may not change when right-side controls (status/context chips)
appear — leaving the left control group's fit stale. Also observe .composer-left.

Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 01:33:26 +00:00
nesquena-hermes c6370235be feat(composer): fit footer controls by overflow (#4657, rebased onto master)
Rebased Paladin173's #4657 onto current master. The per-control visibility
toggles half already shipped via his own #4598, so this rebase carries only the
net-new JS fit-by-overflow engine (_fitComposerFooter: ResizeObserver +
MutationObserver measure .composer-left overflow, step full -> cf-icons ->
cf-burger). Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 01:19:02 +00:00
nesquena-hermes fc087c067f Merge branch 'pr-5118-head' into stage/5118-scroll 2026-06-28 21:22:53 +00:00
b3nwandb3nw bf1f8339b8 fix(ui): use absolute keep_count for edit and regenerate truncate
Refs nesquena/hermes-webui#5096

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 18:01:54 +00:00
santastabber b6b7f1df1d Fix pinned stream scroll restoration 2026-06-29 00:52:36 +10:00
Stacey2911 b39c147b31 fix(workspace): live-sync active Workspace Todos tab 2026-06-28 07:28:30 +00:00
nesquena-hermes 031b5b342a fix(extensions): define TTS registry at module scope + route auto-read through registered engines
Codex gate found 2 functional bugs:
1. registerHermesTtsEngine was inside the voice-mode IIFE, which returns early
   on browsers without SpeechRecognition/speechSynthesis — so the documented API
   didn't exist there. Moved the registry (registerHermesTtsEngine / _hermesTtsSynth
   / option helpers) to module scope so it runs unconditionally.
2. autoReadLastAssistant() ignored a selected registered extension engine (only
   handled elevenlabs/edge, fell through to browser TTS). Added the registered-engine
   branch mirroring speakMessage(), with graceful fallback when the saved engine
   is no longer registered.
2026-06-28 06:03:08 +00:00
nesquena-hermes cd71403be9 feat(extensions): TTS-engine registration capability (registerHermesTtsEngine)
Lets a trusted local extension contribute a TTS engine that appears in the
Settings -> TTS Engine dropdown and is used by BOTH playback paths (voice-mode
auto-read in boot.js _speakResponse + the per-message Listen button in ui.js
speakMessage). Mirrors window.registerHermesSkin.

  window.registerHermesTtsEngine({ id, label, synthesize(text, opts) })
    -> Promise<ArrayBuffer|Blob> of audio; core coerces + plays through the same
       <audio> lifecycle as the Edge engine (stop/rearm in voice mode).

Core owns selection, the dropdown <option> (label via textContent, never HTML),
and playback. Built-in ids (browser/edge/elevenlabs) are reserved; invalid
descriptors are rejected; empty/failed synth degrades gracefully. The settings
panel re-adds registered options on render (the markup hardcodes only built-ins).

Unblocks TTS-engine extensions (e.g. a local VOICEVOX engine). Docs in
docs/EXTENSIONS.md; CHANGELOG [Unreleased]. New regression test drives the real
registry via a Node harness (register/validation/reserved-guard/synth coercion).

Full suite: 10893 passed, 0 failed. Verified end-to-end in a live browser with a
companion VOICEVOX extension: engine registers, appears in the dropdown
(browser/edge/elevenlabs/voicevox), and the Listen button routes through the
registered synthesize (both VOICEVOX steps called).
2026-06-28 05:43:02 +00:00
nesquena-hermesandrodboev 5bb03a6322 feat(settings): sync a Hermes Dashboard chip with Appearance dropdown (#4936)
Adds a "Hermes Dashboard" chip to the Settings > Appearance "Sidebar tabs"
grid, bidirectionally synced with the existing dashboard-mode dropdown
(never/always/auto). Chip off -> dropdown "never"; chip on -> restore last
non-never mode; dropdown change -> chip re-render.

The chip is a proxy for the dashboard config via saveDashboardSettings ->
POST /api/dashboard/config; it NEVER mutates hidden_tabs/tab_order (separate
toggle fn from _toggleTabVisibilityChip). loadDashboardSettings guards a stale
config GET from clobbering a newer save (_dashboardSettingsLoadSeq /
_dashboardSettingsWriteSeq), and a failed chip save rolls the dropdown/chip
back. Chip label via textContent (XSS-safe).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-27 20:33:51 +00:00
Rod Boev 88911448ff fix(#4706): keep onboarding picker state aligned after custom clear 2026-06-27 04:25:45 +00:00
Rod Boev d0eb9b57df fix(#4706): reuse the searchable model picker in onboarding 2026-06-27 04:25:45 +00:00
Rod Boev 1bff768c74 fix(#5021): preserve extra-model label contract 2026-06-27 02:52:33 +00:00
Rod Boev 79a92f0e3b fix(#5021): bound boot model 401 recovery 2026-06-27 02:52:33 +00:00
allenliang2022 e2ea0789b3 fix(chat): do not treat Space on transcript controls as scroll intent (#4970 review)
Maintainer/Codex found one narrow edge in the keyboard intent stamp: because the
listener runs capture-phase and accepts any focused descendant of #messages,
Space/Spacebar on an in-transcript control (tool-card toggles, copy buttons,
role buttons, links/tabs) stamped _lastMessageKeyScrollIntentMs before the
control handler could preventDefault/stopPropagation. That made a button
activation look like a scroll intent and could mask a legitimate post-render
artifact.

Fix by excluding Space/Spacebar when the event target or active element is an
interactive transcript control: button, a[href], select, summary, role=button,
role=tab, role=menuitem, or contenteditable. Keep PageUp/PageDown/arrows/Home/End
stamping unchanged, and keep the existing INPUT/TEXTAREA/contenteditable composer
exclusion.

Add a behavioral node-harness test: Space on a focused transcript button leaves
the key-scroll stamp at -Infinity (serialized null), while PageUp on the pane
still stamps 1234. test_4856 + test_4295 green (20 passed), node --check clean.
2026-06-26 13:29:14 +00:00
allenliang2022 6825bc03b4 fix(chat): record keyboard message-pane scroll as intent (#4970 greptile P1)
Keyboard scrolling of the message pane (PageUp/PageDown, Arrow keys, Space,
Home/End) fires a native scroll event with no wheel/touch/scrollbar/non-message
intent. Inside the 1400ms post-render artifact window the suppression branch
then returned before movedUp could unpin, so a keyboard scroll-up was swallowed
and live-follow snapped the reader back to the bottom.

- Add _lastMessageKeyScrollIntentMs + _recentMessageKeyScrollIntent(), stamped
  by a capture-phase keydown listener on the scroll keys, gated to when the
  message pane is the scroll target (focused/contains focus/hovered) and not an
  editable field (composer/input/contenteditable).
- Gate the post-render suppression on !_recentMessageKeyScrollIntent() (typeof
  guard keeps the #4295 node harness inert).
- Clear the stamp in both _resetScrollDirectionTracker() and
  _resetStreamScrollFollow() (same stale-state hygiene as the wheel stamp).

Tests: keyboard-scroll-inside-window-still-unpins behavioral regression (harness
extended with injected _recentMessageKeyScrollIntent), plus a source lock for the
helper/keydown-stamp/suppression-gate/both-resets. test_4856 + test_4295 green
(19 passed), node --check clean.
2026-06-26 13:29:14 +00:00
allenliang2022 67d9580825 fix(chat): reset wheel intent on session/stream resets + gate suppression on scrollbar drag (#4970 review)
Maintainer dual-gate found two stale-state leaks in the new
_lastMessageWheelIntentMs and one adjacent pre-existing gap; all three fixed:

MUST-FIX 1 — _resetScrollDirectionTracker() (session switch) did not clear
_lastMessageWheelIntentMs, so a gentle wheel in chat A left
_recentMessageWheelIntent() true into chat B's first post-render window,
under-suppressing the artifact and falsely unpinning. Now reset to -Infinity.

MUST-FIX 2 — _resetStreamScrollFollow() (fresh stream) had the same leak: a
gentle upward wheel within 1200ms of a new stream could silently disable live
follow. Now reset to -Infinity.

SHOULD-FIX 3 — the suppression branch ignored _scrollbarDragActive, so a manual
scrollbar-drag upward scroll inside the 1400ms window was swallowed. Gate the
branch on (typeof _scrollbarDragActive==='undefined' || !_scrollbarDragActive);
typeof guard keeps the #4295 node harness inert.

Tests: scrollbar-drag-inside-window-still-unpins behavioral regression (harness
extended with injected _scrollbarDragActive), plus source locks for both resets
and the scrollbar-drag gate. test_4856 + test_4295 green (17 passed),
node --check clean.
2026-06-26 13:29:14 +00:00
allenliang2022 2ce0e731ec fix(chat): scope post-render suppression to true no-intent artifacts (#4970 review)
Maintainer dual-gate (Codex) found the suppression could swallow a genuine
low-delta trackpad wheel scroll-up for ~1.4s after a render:
_recordNonMessageScrollIntent() only recorded message-pane wheel intent at
deltaY<-30, so a gentle deltaY:-5 left both intent helpers false and the
post-render branch returned before movedUp set _messageUserUnpinned.

- Track recent low-delta upward message-pane wheel intent separately
  (_lastMessageWheelIntentMs / _recentMessageWheelIntent), recorded for any
  upward wheel (deltaY<0). The decisive deltaY<-30 sticky-unpin is unchanged.
- Require !_recentMessageWheelIntent() before suppressing the artifact, so a
  real gentle scroll-up inside the window still unpins.
- Add behavioral node-harness regressions: gentle wheel inside the window
  unpins; no-intent artifact inside the window stays suppressed; outside the
  window unpins. Plus a source lock that low-delta intent is tracked.
2026-06-26 13:29:14 +00:00
allenliang2022 e102f4e0d9 fix(chat): guard post-render artifact suppression so #4295 harness passes
The new movedUp suppression branch called _recentMessageRenderArtifactWindow()
and the intent helpers in an unguarded position. The #4295 unit harness injects
the scroll-listener body via new Function() without those helpers, so the branch
threw ReferenceError on a movedUp sample and crashed the node subprocess.

Reorder the && chain with typeof-function guards first; production evaluates the
real helpers, the harness short-circuits before any call. Behavior unchanged.
2026-06-26 13:29:14 +00:00
allenliang2022 f9a952f085 fix(chat): suppress post-render scroll artifacts 2026-06-26 13:29:14 +00:00
Frank SongandPaperclip 2d21eff780 Fix pending user dedupe current-turn scope
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-26 11:12:50 +00:00
nesquena-hermes c4b9edc84e fix(#4932): wire ordered transparent path to #4927 durable snippet fallback + gate scans to transparent mode
Codex+Opus gate (both reproduced): the new mixed-content ordered render path
resolved tool snippets only from the S.messages join (_collectToolResultSnippetsByTid)
and never consulted persistedSnippetByTid (S.session.tool_calls) — so on a
cold/paginated load where the tool_result join misses (#4927 scenario) the inline
card rendered an EMPTY body, AND its tid was added to transparentOrderedToolIds,
suppressing the post-loop derived card that WOULD have recovered via #4927. Build
a transparentPersistedSnippetByTid map from S.session.tool_calls and thread it as a
5th arg to _transparentOrderedToolCall, falling back in both the live and non-live
branches. Also gate the transparent-only scans (S.toolCalls + _collectToolResultSnippetsByTid)
behind isTransparentStream() so non-transparent renders skip the O(messages×parts)
work (Opus perf finding). Redaction (#4926/#4928) was already safe (shared buildToolCard).
+ cold-load persisted-fallback regression test.
2026-06-25 21:21:28 +00:00
Rod Boev 5f1a2a07de Keep settled transparent turns aligned with anchor-scene ownership 2026-06-25 21:11:56 +00:00
Rod Boev bef89969ed fix(chat): preserve recycle harness anchors after prose split (#3397) 2026-06-25 21:11:56 +00:00
Rod Boev b590518cf4 fix(chat): preserve transparent stream prose order for mixed turns (#3397) 2026-06-25 21:11:55 +00:00
allenliang2022 ef6901c947 fix(chat): ignore DOM-wipe scroll clamp during render 2026-06-25 20:19:00 +00:00
nesquena-hermes 0caa57d8b7 fix(#4927): cover the Anthropic tool_use derived push with the persisted fallback
Codex gate: the third derived-push site (Anthropic tool_use content-array branch,
ui.js) still read resultsByTid[tid]||'' without the persisted fallback, so
Anthropic-format sessions could still cold-reload with an empty tool body. Add
the persistedSnippetByTid fallback there too; now all derived result-snippet
reads (OpenAI top-level, Anthropic tool_use, partial) chain to the durable
persisted snippet. Added a guard test asserting EVERY derived resultSnippet read
includes the fallback.
2026-06-25 19:23:31 +00:00
nesquena-hermes dad0a3554d fix(#4927): durable fallback for tool output/diff body on cold reload
In the transparent stream, terminal output and patch diffs intermittently
vanished after a cold/paginated reload. A settled tool row renders its body from
`snippet`, but the raw-assistant-envelope 'derived' rebuild only joined the
result by tool id via resultsByTid (role:tool messages); when that join missed
(id mismatch, recovery-rebuilt turn) the snippet was empty, and #4622's live
enrichment only helps when S.toolCalls still matches (empty on cold load).

Build a persistedSnippetByTid lookup from the durable session.tool_calls compact
summary (which carries a bounded result snippet per tid) and use it as a
fallback in all three derived-push sites, so the persisted record is the
reliable source and the live join becomes a fast-path, not the only path that
shows the body. Part of #4925.
2026-06-25 19:14:23 +00:00
nesquena-hermes f79c975842 fix(#4928): redact tool args at all render/copy sites now that content args are full-length
Codex gate: raising content args from 120->4000 chars newly surfaces secrets on
non-first lines / past char 120 in render paths that print tc.args directly and
bypass the #4926 label redaction. Apply _redactToolTargetLabel to arg values at
every such site: the transparent Full-tab args render (_transparentToolDetailHtml),
the non-transparent tool-card args block (buildToolCard visibleArgs), the
clipboard copy path (_copyEventToClipboard), and the _toolArgsSnapshot itself.
+ node regression proving a secret past char 120 in a long command is masked in
the Full tab while the non-secret command structure is retained.
2026-06-25 18:28:06 +00:00
nesquena-hermes bd6e57fb42 fix(#4928): don't cap tool-call content args to 120 chars
Tool-call args were truncated to 120 chars at three sites (_truncate_tool_args,
the live _tool_args_snapshot in api/streaming.py, and _toolArgsSnapshot in
static/ui.js). For incidental args that's fine, but content/diff-bearing keys
(command/cmd/script/code/patch/diff/old_string/new_string/content/path/
file_path) feed the rendered tool card AND the diff reconstruction in
recovery-rebuilt sessions, so the cap silently corrupted long commands/paths and
produced broken/empty diffs. Exempt those keys with a much larger cap
(_TOOL_ARG_CONTENT_CAP = _TOOL_RESULT_SNIPPET_MAX = 4000) at every site, keyed by
key name; incidental args keep the 120 cap; storage stays bounded. Part of #4925.
2026-06-25 18:18:17 +00:00
nesquena-hermes 64cb4cef4e fix(#4926): env-assignment redaction must not eat URL query tails
Codex re-gate: the env/AUTH '=' redaction regex matched 'token=' INSIDE a URL
(?token=...&mode=debug&signature=...) before the dedicated URL-query redactor,
and its \S+ value swallowed the rest of the URL incl later params + closing
quote. Anchor the env-assignment match to a shell-token boundary (^|[\s;|(]) so
it can't match keys preceded by ? or & — URL query params are left to the
dedicated query-param redactor, which masks the secret-looking ones while
preserving the command tail. + regression tests (URL-tail-preserved,
benign-PATH-not-over-redacted).
2026-06-25 17:58:12 +00:00
nesquena-hermes ed21bd20cd fix(#4926): broaden _redactToolTargetLabel to mask env/flag/header secrets in the full command
Codex gate: exposing the full multi-line command in the expanded shell card
newly surfaces secrets on non-first lines (the collapsed header only ever showed
line 1). The old redactor masked only password-shaped fields. Broaden it to mask
common secret forms across the WHOLE command: env/colon assignments for
*TOKEN/*API_KEY/*SECRET/*ACCESS_KEY/*PRIVATE_KEY/*CREDENTIAL/*CLIENT_SECRET/
*SESSION_KEY and *AUTH*=, --token/--api-key/--secret style flags,
Authorization: Bearer/Bot/Token headers, and secret-looking URL query params.
Test now drives the REAL redactor (not an identity stub) and asserts non-first-line
secrets are masked while the collapsed header stays first-line.
2026-06-25 17:52:41 +00:00
nesquena-hermes 99b6a5e30b fix(#4926): expanded shell tool card shows the full multi-line command
In the transparent activity stream, a shell tool card's collapsed header
correctly shows only the first line of the command, but the EXPANDED detail
lead (`$ <command>`) reused that same first-line-only value via
_toolTargetLabel, so a multi-line script still rendered as line 1 when expanded.
The full command is available frontend-side; the truncation was purely in the
label derivation.

Adds _toolFullCommandLabel(tc) (full multi-line, redacted, no first-line
collapse) and uses it in _toolDetailLeadText for the shell kind, while the
collapsed header keeps the first-line _toolTargetLabel. Part of #4925.
2026-06-25 17:39:05 +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
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 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-hermesandRod Boev 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-hermesandRod Boev 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