From d3805b41e777ea713fd61c12607ee6e119bde7ee Mon Sep 17 00:00:00 2001 From: Marcel Hild Date: Fri, 4 Sep 2026 18:53:15 +0200 Subject: [PATCH] perf: keep the TUI off the status line render path Claude Code re-runs this binary every couple of seconds for as long as a session is open, so module load time is paid continuously rather than once. src/ccstatusline.ts imports runTUI statically, which pulls ink, React and yoga-layout into the render path even though rendering never touches them. Making that import dynamic is not enough on its own: --outfile cannot split, so the whole graph still gets parsed. Building with --splitting moves the TUI into its own chunk and shrinks the entry from 3.34 MB to 21 KB. Measured on an M-series Mac, 20 runs per data point, real payload piped in: before 238, 237, 238 ms/render after 226, 222, 220 ms/render That is roughly 7%, and the rendered output is byte-identical. The interactive TUI was smoke-tested under a pty and behaves the same. scripts/replace-version.ts had to change too: with splitting the __PACKAGE_VERSION__ placeholder lands in a chunk rather than in dist/ccstatusline.js, so patching only the entry would have silently shipped an unreplaced placeholder. It now patches every emitted script and fails loudly when it finds none. Packaging is unaffected: files is already dist/, and bin/main/exports keep pointing at dist/ccstatusline.js. Verified the built output runs under node as well as bun. Assisted-by: claude:claude-opus-5 --- package.json | 2 +- scripts/replace-version.ts | 30 +++++++++++++++++++++--------- src/ccstatusline.ts | 6 +++++- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 118994d06..7f8a16e40 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ ], "scripts": { "start": "bun run src/ccstatusline.ts", - "build": "rm -rf dist/* ; bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --target-version=14", + "build": "rm -rf dist/* ; bun build src/ccstatusline.ts --target=node --splitting --format=esm --outdir=dist --target-version=14", "postbuild": "bun run scripts/replace-version.ts", "example": "cat scripts/payload.example.json | bun start", "video:studio": "remotion studio remotion/index.ts", diff --git a/scripts/replace-version.ts b/scripts/replace-version.ts index 852da26d3..181581761 100755 --- a/scripts/replace-version.ts +++ b/scripts/replace-version.ts @@ -2,6 +2,7 @@ import { readFileSync, + readdirSync, writeFileSync } from 'fs'; import { join } from 'path'; @@ -15,14 +16,25 @@ interface PackageJson { const packageJson = JSON.parse(readFileSync('package.json', 'utf-8')) as PackageJson; const version = packageJson.version; -// Read the bundled file -const bundledFilePath = join('dist', 'ccstatusline.js'); -let bundledContent = readFileSync(bundledFilePath, 'utf-8'); - -// Replace the placeholder with the actual version -bundledContent = bundledContent.replace(/__PACKAGE_VERSION__/g, version); +// The build emits the entry plus code-split chunks, and the placeholder can +// land in any of them, so patch every emitted script rather than just the entry. +let patched = 0; +for (const file of readdirSync('dist')) { + if (!file.endsWith('.js')) { + continue; + } + const path = join('dist', file); + const content = readFileSync(path, 'utf-8'); + if (!content.includes('__PACKAGE_VERSION__')) { + continue; + } + writeFileSync(path, content.replace(/__PACKAGE_VERSION__/g, version)); + patched++; +} -// Write back the modified content -writeFileSync(bundledFilePath, bundledContent); +if (patched === 0) { + console.error('✗ No __PACKAGE_VERSION__ placeholder found in dist/'); + process.exit(1); +} -console.log(`✓ Replaced version placeholder with ${version}`); +console.log(`✓ Replaced version placeholder with ${version} in ${patched} file(s)`); diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index 0a5bc1799..89bc296b1 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -1,7 +1,6 @@ #!/usr/bin/env node import chalk from 'chalk'; -import { runTUI } from './tui'; import type { SkillsMetrics } from './types'; import type { RenderContext } from './types/RenderContext'; import type { StatusJSON } from './types/StatusJSON'; @@ -351,6 +350,11 @@ async function main() { void updatemessage; await saveSettings(newSettings); } + // Imported lazily: the TUI pulls in ink/React/yoga-layout, which the + // status line render path never touches. Claude Code re-runs this + // binary every couple of seconds, so keeping that graph off the + // render path is worth the dynamic import here. + const { runTUI } = await import('./tui'); runTUI(); } }