Skip to content

release: 7.18.0 — release candidate (merges #282 #283) - #284

Open
vakovalskii wants to merge 16 commits into
mainfrom
rc/7.18.0
Open

release: 7.18.0 — release candidate (merges #282 #283)#284
vakovalskii wants to merge 16 commits into
mainfrom
rc/7.18.0

Conversation

@vakovalskii

Copy link
Copy Markdown
Owner

Релиз-кандидат 7.18.0 — один merge вместо двух.

PR Что Автор
#283 Running agents показывает и локальных, и внешних агентов в одном дереве @NovakPAai
#282 Починена вёрстка карточек в Projects (несбалансированный тег) @NovakPAai

Верификация

  • Обе ветки сливаются в main автомержем
  • node --test test/*.test.js263 passed / 0 failed / 2 skipped (было 256)
  • node --check на app.js и workspace.js — сборки нет, синтаксическая ошибка сломала бы дашборд
  • CI зелёный на обоих PR

По #283 отдельно

Он разворачивает решение из #275, где дерево намеренно показывало только внешние терминалы. Смена осознанная, CLAUDE.md и docs/design/running-agents-external.md обновлены в самом PR.

Главное — критичный инвариант сохранён и явно закреплён в коде: клик по строке никогда не спавнит пустой in-app терминал. Локальный агент → прыжок к своей панели, внешний → POST /api/focus и подъём реального окна. Иначе claude --continue породил бы второй инстанс живого агента.

⚠️ Что этот релиз НЕ чинит

Заморозки терминала, которые мы разбирали. Замеры на моих данных:

Источник Периодичность Блокировка
execSync lsof на PID в getActiveSessions 5 с до 2 с на процесс
loadSessions() (TTL кеша совпадает с периодом опроса) 60 с 648 мс … 11.9 с
getLeaderboardStats() по клику ~40 с

Всё синхронно и в одном 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

NovakPAai and others added 9 commits August 2, 2026 10:00
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>
@vakovalskii

Copy link
Copy Markdown
Owner Author

Добавил в этот RC ещё один PR — #285 (accessibility pass, @NovakPAai): сливается автомержем, тесты 263 passed / 0 failed, node --check по всем затронутым frontend-файлам чист. Запись в changelog дополнена.

Итоговый состав 7.18.0: #282 (вёрстка карточек Projects), #283 (единое дерево Running agents), #285 (клавиатурная доступность и семантика диалогов).

NovakPAai and others added 7 commits August 2, 2026 22:54
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).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vakovalskii

Copy link
Copy Markdown
Owner Author

Добавил в RC ещё три PR: #286 (маскирование ключа LLM), #287 (индекс поиска не блокирует event loop), #288 (доступный GitHub-диалог + argv-форма exec). Итого 7.18.0 = #282, #283, #285, #286, #287, #288.

289 тестов / 0 падений, синтаксис всех затронутых файлов чист.

Отмечу разрешение конфликта: #286 и #288 оба правят leaderboard.js. #288 заменяет инлайновую разметку модалки на _lbBuildAuthModal(), поэтому для этого участка взята его сторона. Разрешать файл целиком в любую сторону было нельзя — потерялась бы работа другого PR: тест из #286 как раз это и поймал, когда я сначала так и сделал (он требует rel="noopener noreferrer" у всех четырёх target="_blank", а на main их не было ни у одной). Сейчас в файле и _lbBuildAuthModal, и все четыре noopener.

Отдельно: #287 закрывает часть той проблемы с блокировкой event loop, которую мы разбирали — индекс поиска давал многосекундный стопор. Оставшиеся два источника (execSync lsof каждые 5 с в getActiveSessions и loadSessions на 648 мс…12 с) всё ещё в силе.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants