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.
- 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>
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>
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>
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.
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>
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>
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>
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>
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.
Codex found the guard had a first-event gap: programmatic seeds of _lastScrollTop
(_setMessageScrollToBottom, scrollToBottom, snapshot restore paths) didn't seed
_lastMessageClientHeight, so the FIRST native toolbar-collapse scroll saw a null
prior height -> grew=false -> the false-unpin still fired. Now every programmatic
_lastScrollTop=el.scrollTop is paired with _lastMessageClientHeight=el.clientHeight.
iOS Safari portrait resolves its dynamic toolbar height after first paint; when the
toolbar collapses the #messages scroller grows (clientHeight up), firing a scroll
event with decreased scrollTop that was misread as an upward user scroll -> falsely
set _messageUserUnpinned, and the ResizeObserver settle then self-cancelled, leaving
the reader stranded at the TOP of a freshly-opened conversation (landscape unaffected
since it doesn't resize late). Sibling of #4701.
Fix (touch-safe, no-op on desktop/landscape where clientHeight is stable):
- Track _lastMessageClientHeight; gate movedUp on !grew so a container-height
INCREASE can't be read as an upward scroll. Reset on session switch.
- For explicit (open) settles, also observe the scroller itself so a late portrait
toolbar collapse re-anchors the bottom.
Independent of #4818 (overflow-anchor paint-gap) — different mechanism, coexists.
Diagnosis credit: investigation. Co-authored-by: reinocheong <reporter>