Skip to content

refactor(render): request-scoped render orchestration with byte parity (#15) - #25

Merged
axisrow merged 2 commits into
mainfrom
feat/request-scoped-render
Sep 27, 2026
Merged

axisrow merged 2 commits into
mainfrom
feat/request-scoped-render

Conversation

@axisrow

@axisrow axisrow commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #15 (delivery step 1 of the daemon epic #14). Extracts the render orchestration out of the one-shot entry point into a request-scoped function so a future shared-process runner can serve renders without process-global state.

  • src/render.ts (new): renderStatusLines(data, loaded, invocation) — accepts validated input, the config loaded for this request, and an explicit invocation snapshot; returns text. stdout/process.exit stay in the entry point (src/ccstatusline.ts, now thin).
  • RenderInvocation (src/types/RenderContext.ts): per-request snapshot — configPath, cwd, env snapshot, terminalWidth — resolved once by the entry point.
  • src/utils/config.ts: loadSettingsFrom(configPath) / saveSettingsTo(configPath, settings) resolve paths per call; module-level settingsPath/lastLoadError are not mutated by the render path anymore (TUI/CLI keep the old wrappers, untouched).
  • src/utils/usage-fetch.ts: the single in-memory usage result became an explicit UsageMemoryCache, created per request by usage-prefetch (createUsageMemoryCache()).
  • Terminal width: probed once per request by the entry point (memoized probe + L2 session cache unchanged) and passed in explicitly; the render path no longer reads the process memo.
  • Styling state: chalk.level + updateColorMap() moved to the top of the final synchronous formatting section (no await inside) — the serialization option from the issue; concurrent requests cannot interleave inside the formatting pass.
  • Config validation, atomic migrations, warning badges, malformed-file recovery: preserved verbatim.

Parity proof

8 CLI scenarios captured from the pre-refactor binary, then replayed after the refactor — diff -r of raw stdout bytes: identical, repeats stable:

scenario covers
s1-basic multiline layouts, tokens/context widgets, width
s2-theme-truecolor theme in regular mode, truecolor
s2b-gradient-truecolor foreground gradient override
s3-ansi16-narrow ansi16, bold/dim/parens-dim, merge, truncation at width 60
s4-powerline powerline themes across lines, flex separators, caps
s5-custom-command custom command widget (real exec)
s6-malformed-json warning badge + malformed-file recovery
s7-updatemessage updatemessage print + atomic settings write (remaining 5→4)

Fixed transcript fixture, fixed env, fixed CCSTATUSLINE_WIDTH. Golden bytes are checked in: src/__tests__/render-parity.test.ts replays them against renderStatusLines and covers per-request config isolation (a rejected load reports its own error without touching module state).

Resource-regression gate

scripts/benchmark-render.py, 80 renders/run, warm caches, alternating base/refactored runs (transcript 13 MB, 4 parallel workers):

run pair base 74ba67f refactored Δ
r2 214.3 209.0 −2.5%
r3 222.8 228.4 +2.5%
r4 215.3 218.5 +1.5%
r5 226.4 216.6 −4.3%

Pair average +2.6% — within the ≤ +5% gate. Peak RSS median 102.7 MB → 86.3 MB (5×/usr/bin/time runs each), no growth. Output hash identical across every run (454f8606…), including the cold initial baseline at 427.0 ms CPU/render (system noise dominated the first pair; the warm alternating pairs above are the honest comparison).

Tests

bun test: 2504 pass, 0 fail (156 files) — single final run on the committed tree.
bun run lint: clean (tsc + ESLint, no suppressions). Includes lint fixes for pre-existing errors from the #12/#13 commits (separate style commit).

Known follow-ups

  • cwd/env are carried in RenderInvocation but not yet consumed by deep providers (custom-command exec env, usage-fetch proxy/credential env reads) — that inversion lands with the shared-process sub-issues.
  • 🤖 Generated with Claude Code

#15)

Extract the orchestration out of the one-shot entry point into a
request-scoped function so a shared-process runner (#14) can serve
renders without process-global state:

- src/render.ts (new): renderStatusLines(data, loaded, invocation)
  returns the output text; stdout/process.exit stay in the entry point.
  Per-request styling (chalk.level + COLOR_MAP) is set at the top of the
  final synchronous formatting section, serializing it per request.
- src/types/RenderContext.ts: RenderInvocation snapshot (configPath,
  cwd, env, terminalWidth) resolved once per request by the entry point.
- src/utils/config.ts: loadSettingsFrom(configPath)/saveSettingsTo()
  resolve paths per call; module-level settingsPath/lastLoadError are no
  longer mutated by the render path (TUI/CLI keep the old wrappers).
- src/utils/usage-fetch.ts: the in-memory usage result moved into an
  explicit UsageMemoryCache created per request; usage-prefetch creates
  one per prefetch call.
- Terminal width: probed once by the entry point (memoized probe + L2
  session cache unchanged) and passed in via the invocation; the render
  path no longer reads the process memo itself.

Config validation, atomic migrations, warning badges and malformed-file
recovery are preserved verbatim.

Parity proof: 8 CLI scenarios (multiline layouts, ansi16/ansi256/
truecolor + theme, gradient override, powerline with themes across
lines, custom command, malformed-config badge, updatemessage write)
captured from the pre-refactor binary are byte-identical after the
refactor; fixed transcript fixture, fixed env, fixed CCSTATUSLINE_WIDTH.
src/__tests__/render-parity.test.ts replays the golden bytes against
renderStatusLines and covers per-request config isolation.

Resource gate (scripts/benchmark-render.py, 80 renders/run, warm cache,
alternating runs): paired base(74ba67f) vs refactored runs measured
-4.3%..+2.5% CPU/render (pair average +2.6%, within the <=+5% gate);
peak RSS median 102.7 MB -> 86.3 MB (no growth). Output hash identical
across every run.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@axisrow
axisrow force-pushed the feat/request-scoped-render branch from a91b085 to 6df95fc Compare September 27, 2026 02:36

@axisrow axisrow left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вердикт: Approved — один неблокирующий инлайн-комментарий, изменений не запрашиваю.

Примечание: head в задании (a91b085) устарел — это коммит до rebase; ревьюю текущий head PR 6df95fc, он совпадает с чистым чекаутом; дифф против origin/main.

  • Оркестрация сохраняет поведение: в render.ts между chalk.level/updateColorMap и форматированием строк нет await (единственные await ниже — записи updatemessage после сборки outputLines); COLOR_MAP читается на месте вызова, модули префетча её не трогают, поэтому перенос chalk-настройки за Promise.all безопасен.
  • Точка входа: initConfigPath по-прежнему предшествует getConfigPath в main; пустой text ничего не печатает; защитный config-badge кладётся в outputLines и не теряется; типы RenderInvocation.terminalWidth и RenderContext.terminalWidth согласованы (number и null).
  • config.ts: loadSettingsFrom/saveSettingsTo не мутируют модульное состояние; обёртки loadSettings/saveSettings сохраняют lastLoadError для TUI (App.tsx) и CLI (cli.ts); тест изоляции ассертит, что getConfigLoadError() остаётся null после запросных загрузок.
  • usage-fetch: cache протянут через все пути (быстрый путь памяти, файловый кэш, getStaleUsageOrError, cacheUsageData), семантика errorMaxAge сохранена; внешних вызывающих fetchUsageData нет — ставший обязательным options ловится компиляцией.
  • Парити-тесты: golden replay по сценариям s1-s7, badge на malformed, запись updatemessage строго в invocation.configPath (remaining 5→4 ассертом). Чувствительность к ubuntu-CI проверена чтением: git-виджеты дают одинаковый fallback и на отсутствующем cwd (обход вверх по дереву ловит ошибку), и на not-a-repo; custom-command наследует реальный cwd процесса, а не замоканный; время-зависимых виджетов в сценариях нет.
  • Follow-up из описания (cwd/env пока не потребляются глубокими провайдерами) — заявленная область, не блокирует.
  • Неблокирующее: src/render.ts, строки 188 и 207 — невидимый литеральный NBSP в строке замены вместо прежнего escape-а; предлагаю вернуть escape (обратный слэш + u00A0) для читаемости, детали в инлайн-комментарии.

Comment thread src/render.ts Outdated
Review follow-up on #25: the two .replace(/ /g, ...) calls carried a
literal U+00A0 in the replacement string (invisible in editors) instead
of the original ' ' escape. Byte-identical output, but one editor
normalization or copy-paste would silently break the VSCode trim
protection. Back to the explicit escape.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@axisrow
axisrow merged commit 8eb8c02 into main Sep 27, 2026
6 checks passed
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.

Daemon: extract request-scoped render orchestration with output parity

1 participant