release: 7.18.0 — release candidate (merges #282 #283) - #284
release: 7.18.0 — release candidate (merges #282 #283)#284vakovalskii wants to merge 16 commits into
Conversation
The missing-folder-detection refactor added the launcher-card-actions wrapper only for the missing-folder branch, but left the pre-existing closing </div> in the normal (folder-exists) branch without ever opening it. The unbalanced markup made every action button/select render as a direct flex-column child of .launcher-card, stretching each one to full card width on its own row instead of a compact action row. Also brings the <select> terminal-panes control in line with the button sizing in that row, and pushes the rarely-used Remove control to the end of the row with quieter styling so it doesn't compete visually with New/Last.
The tree only ever showed agents in external native terminals — an agent running inside codbash's own Workspace pane silently disappeared from it, so a project with both an in-app Claude session and an iTerm one only showed half the picture. That asymmetry (plus dimmed "idle" rows reading as ambiguous "maybe gone") is what read as messy/hard-to-trust grouping. - _wsRunningGroups(mode) replaces _wsRunningByProject(): still built from activeSessions (a live ps-scan re-polled every 5s, so an exited process is simply absent next tick — no separate "ghost" filtering needed), but now includes local (in-codbash) agents alongside external ones instead of dropping them. - Grouping is now a per-browser toggle (project → agent, or agent → project), persisted to localStorage['codedash-running-group'], exposed as a compact 2-button segmented control in the tree header. - Rows are color-coded by where they run: blue dot = inside codbash, orange dot = external native terminal (iTerm/Terminal.app/Warp/cmux). Idle still dims to muted gray, on top of either color. - jumpToRunningAgent now dispatches on the `local` tag: local agents jump straight to their Workspace tab/pane (found by matching cwd against the live pane list); external agents keep the existing /api/focus path. Updates the design doc and CLAUDE.md (which documented "external only" as intentional) and the existing test suite for the new function names/shape.
The first pass grouped project/agent as a flat 2-level list — one header,
then every session underneath as a same-labeled row ("Claude", "Claude",
…). That read as alternating noise (session, agent, session, agent) rather
than a real hierarchy, especially once a project had 2+ live sessions of
the same agent.
_wsRunningTree(mode) replaces _wsRunningGroups(mode): a real 3-level tree
— outer group -> inner group -> individual sessions — built via a small
generic _wsGroupBy() applied twice. mode:'project' nests project -> agent
-> sessions; mode:'agent' nests the mirror, agent -> project -> sessions
(re-parenting the whole tree, not just relabeling a flat list).
A project+agent pair with exactly one live session collapses its leaf row
into the subgroup row itself (.ws-run-leaf) — no redundant single-child
row. Once it holds 2+ sessions, the subgroup renders as a real subheader
and each session gets its own leaf row, labeled via _wsSessionLeafLabel
(a live pane's name if local and matched, else the session-id prefix —
same convention as the session cards' "Resume last session (id12345)" —
else a bare pid) so same-project/same-agent sessions read as genuinely
distinct instead of the same label repeated.
Top-level (L1) groups — project in project-mode, agent kind in agent-mode — now start collapsed. Clicking a header expands it in place to reveal its agent/project subgroups and individual sessions; the same accordion logic applies to both grouping directions. - _wsRunExpanded (in-memory, keyed `mode|groupKey`) tracks open/closed per group so switching the Project/Agent toggle doesn't share expand state across the two different hierarchies. - _wsToggleRunGroup flips a `collapsed` class directly on the group's wrapper DOM node instead of forcing a full tree rebuild — cheap, and a later rebuild (triggered by a real activeSessions change) re-reads _wsRunExpanded so the user's open/closed choices survive it. - The L1 header's click now means "toggle" rather than "jump to the first session" — with a header disclosing multiple children there's no single unambiguous default target, so the jump action lives entirely on leaf rows (.ws-run-l2.ws-run-leaf / .ws-run-l3), unchanged from before. - Basic keyboard support: role="button", aria-expanded, Enter/Space to toggle.
…out fix Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the gaps found in a UX audit across Overview, Projects, Sessions, and Workspace: real keyboard focus and screen-reader semantics were inconsistent — some controls had them, most didn't. - Overview: .ov-card gets a visible :focus-visible outline (it was already a real <button>, just missing the ring). - Calendar: day cells are now real <button>s (were unfocusable divs) with aria-pressed reflecting selection; the popup is a proper role="dialog" aria-modal with Escape-to-close and focus returned to the date button on close (toggleCalendar/closeCalendar/onCalendarPopupKeydown). - Add Project modal: addProjectSwitchTab() now updates aria-selected and roving tabindex when switching tabs (previously only toggled the visual .active class, so a screen reader kept announcing the first tab as selected forever); added Left/Right/Home/End arrow-key navigation matching the existing Projects/History tablist pattern. - Session cards (.card / .list-row / .qa-item): were plain divs with an onclick and nested buttons — unreachable by Tab entirely. Added tabindex, a descriptive aria-label, :focus-visible styling, and a shared onCardKeydown handler (Enter/Space triggers the card's own click, guarded so it doesn't also fire for a nested button/checkbox's own key handling). - Detail panel: role="dialog" aria-modal, wired into the existing _installModalFocusTrap/_uninstallModalFocusTrap helper (same one Add Project / Projects Settings use) for Tab-trapping and focus-return on close. Hardened _uninstallModalFocusTrap to skip focus-return when the captured element has since been detached from the DOM (a background poll/re-render can replace it while the modal is open) rather than silently stranding focus on the modal's own now-hidden close button. - Workspace: aria-label on the pane close button, tab close button, and the three launch/layout <select> menus that only had a title before. Split-pane resize handles (.ws-resizer) are now real ARIA separators — tabindex, role="separator", aria-orientation, and arrow-key resize (mirrors the pointer-drag math without triggering a full handle rebuild mid-interaction, which would destroy and unfocus the handle the user is actively adjusting).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Добавил в этот RC ещё один PR — #285 (accessibility pass, @NovakPAai): сливается автомержем, тесты 263 passed / 0 failed, Итоговый состав 7.18.0: #282 (вёрстка карточек Projects), #283 (единое дерево Running agents), #285 (клавиатурная доступность и семантика диалогов). |
GET /api/llm-config returned the stored API key verbatim on every
Settings load — it sat unmasked in each network response/devtools log
and the frontend dropped it straight into the password input's value.
The GitHub endpoints already follow the right rule ("Never vend raw
tokens to the browser", /api/github/profile strips token/repoToken);
the LLM endpoint was the one exception.
- GET /api/llm-config now returns { model, url, hasKey, keyHint } —
keyHint is a ••••-masked last-4 so the user can tell which key is
stored without the secret ever reaching the DOM.
- Because the key no longer round-trips, POST with an empty apiKey
field means "keep the stored key" (otherwise every URL/model tweak
would silently wipe it); an explicit { clearApiKey: true } removes it.
- saveLLMConfig writes ~/.claude/codedash-llm.json with mode 0600 and
chmods a pre-existing world-readable file from older versions.
- Frontend shows the stored-key state via the input placeholder
("••••1234 (saved — type to replace)"), never pre-fills the value;
autocomplete="new-password" keeps browser autofill managers out.
saveLLMSettings now also surfaces save failures instead of toasting
"saved" unconditionally.
- leaderboard.js: rel="noopener noreferrer" on all target=_blank links
(reverse-tabnabbing).
Verified live: GET carries no apiKey field, hint shows ••••1234, file
lands -rw-------, an empty-key save preserves the key, clearApiKey
clears it. Source-contract tests added (test/llm-config-redaction.test.js).
buildSearchIndex re-parsed every session with detail — a findSessionFile lookup plus a full detail load (sync fs read + JSON.parse per line) each — in one synchronous tick, on whichever request happened to miss the 60s cache. Measured on a 900-session history: a 3.6s hard stall of the event loop, which also stalls every other API call and the terminal WebSocket data pump. The analytics job already solved this exact problem with chunk+yield (_scheduleAnalyticsRecompute); search never got the same treatment. Chunking per session alone wasn't enough: real histories have a heavy tail (median session ~0.1MB here, but codex transcripts up to 78MB), and readLines slurps the whole file into a string before splitting — one such session blocks for seconds regardless of the outer chunk size. - buildSearchIndex is async, processes sessions in small chunks (8 — each item is far heavier than the computeSessionCost calls analytics batches 80-at-a-time) and yields via setImmediate between them. - JSONL sessions over SEARCH_STREAM_THRESHOLD (4MB) are read line-by-line off a stream that yields every 2000 lines, so one huge transcript can't block either. Nothing is truncated — this only changes *when* the work happens, not what gets indexed. - getSearchIndex is stale-while-revalidate (mirroring getCostAnalytics): a >60s-old index is still overwhelmingly accurate, so it's served instantly while the refresh runs in the background. Only a genuine cold start awaits. - Concurrent rebuilds dedupe into one in-flight job, so a burst of searches during a rebuild no longer queues N full-history scans. - Collapsed six near-identical per-format if/else branches into a SEARCH_DETAIL_LOADERS lookup table, and the two JSONL readers now share one per-line parser so they can't drift. Snippet cap is a named constant instead of a repeated 500. - searchFullText is now async; /api/search and the `codbash search` CLI command updated accordingly. Measured before/after on the same 900-session history: worst event-loop stall 3622ms -> 60ms cold build 3.6s -> 3.0s warm search ~14ms (unchanged) Search results verified byte-identical to origin/main across 6 queries (496 result rows) via a git-worktree differential run.
Clears the last two items from the UX/server audit backlog.
GitHub device-code modal (leaderboard):
- Was a bare div with no dialog semantics. Now role="dialog" +
aria-modal + aria-labelledby, focus moved inside on open, Tab trapped
within it, Escape closes, and focus returns to whatever opened it
(guarded against the node having been replaced by render()).
- The status line is aria-live so "Code expired" / "Connection error" is
announced rather than silently repainted.
- The poll loop had a bare `catch {}`: every network failure was
swallowed and the user watched "Waiting for authorization..." until
the tries ran out, with nothing ever indicating a problem. It now
counts consecutive failures (3 → surface and stop), reports a
server-sent `error` field, and honours `slow_down` per RFC 8628 §3.5
— matching pollRepoScopeOnce() in app.js, which already did all this.
- Cancel is a real handler instead of an inline
this.parentElement.parentElement DOM walk.
Shell-string exec → argv form:
- data.js findQwenSessionByPid interpolated a pid into an `lsof …` shell
string; the sibling lsof call in getActiveSessions already used
execFileSync, so this was the outlier. stderr now suppressed via stdio
rather than a `2>/dev/null` that required a shell.
- migrate.js built `find "$path" … | wc -l` from a home path; now argv
form with the count done in JS (which also drops the pipe).
- Removed the adjacent `du -sb … || du -sk …` call entirely: its result
(totalSize) was computed and never printed. It was also wrong — BSD du
has no -b, so on macOS it always fell through to -sk and added
KILOBYTES to a byte total. Dead code hiding a unit bug behind a shell
`||`.
Verified: modal semantics/Tab-trap/Escape/focus-return driven in a real
browser; lsof argv vs shell forms confirmed to find the same session on
a process holding a matching file open; find argv form matches the old
pipe count on paths containing quotes, $ and backticks; `codbash export`
end-to-end reports 934 files against 934 actually in the archive.
Conflict in leaderboard.js: #288 replaces the inline GitHub device-code modal markup with _lbBuildAuthModal() (focus trap + Escape + focus return). Took #288 for that hunk only — resolving the whole file to either side would have dropped the other PR's work; #286's rel="noopener noreferrer" on the remaining three target="_blank" links is kept (its test asserts all four).
|
Добавил в RC ещё три PR: #286 (маскирование ключа LLM), #287 (индекс поиска не блокирует event loop), #288 (доступный GitHub-диалог + argv-форма exec). Итого 7.18.0 = #282, #283, #285, #286, #287, #288. 289 тестов / 0 падений, синтаксис всех затронутых файлов чист. Отмечу разрешение конфликта: #286 и #288 оба правят Отдельно: #287 закрывает часть той проблемы с блокировкой event loop, которую мы разбирали — индекс поиска давал многосекундный стопор. Оставшиеся два источника ( |
Релиз-кандидат 7.18.0 — один merge вместо двух.
Верификация
mainавтомержемnode --test test/*.test.js→ 263 passed / 0 failed / 2 skipped (было 256)node --checkнаapp.jsиworkspace.js— сборки нет, синтаксическая ошибка сломала бы дашбордПо #283 отдельно
Он разворачивает решение из #275, где дерево намеренно показывало только внешние терминалы. Смена осознанная,
CLAUDE.mdиdocs/design/running-agents-external.mdобновлены в самом PR.Главное — критичный инвариант сохранён и явно закреплён в коде: клик по строке никогда не спавнит пустой in-app терминал. Локальный агент → прыжок к своей панели, внешний →
POST /api/focusи подъём реального окна. Иначеclaude --continueпородил бы второй инстанс живого агента.Заморозки терминала, которые мы разбирали. Замеры на моих данных:
execSync lsofна PID вgetActiveSessionsloadSessions()(TTL кеша совпадает с периодом опроса)getLeaderboardStats()Всё синхронно и в одном event loop с WebSocket терминала. Это отдельная работа, идёт следующим PR.
После мержа
Собираю подписанный + нотаризованный desktop (dmg и zip, arm64 + x64), staple,
refresh-update-feed, релизv7.18.0.Важно: v7.17.0 так и не был выпущен (сборка падала на троттлинге Apple TSA), поэтому пользователи прыгнут с 7.16.0 сразу на 7.18.0 и получат заодно всё из 7.17.0 — имена панелей терминала и фикс контраста dim-текста.
🤖 Generated with Claude Code