Skip to content

feat(tui): rework the header box to the reference element allocation - #764

Merged
ericleepi314 merged 1 commit into
mainfrom
feat/header-box-allocation
Jul 29, 2026
Merged

feat(tui): rework the header box to the reference element allocation#764
ericleepi314 merged 1 commit into
mainfrom
feat/header-box-allocation

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

What

Reworks the startup header box (SessionPanel) so its element allocation matches the reference Claude Code header. Section content is unchanged — Tools/Skills/System Prompt/MCP collapsibles stay exactly as they were. Only the layout changed.

Before — left column hard-coded to min(mascotWidth + 4, cols × 0.4) = 20, so the cwd truncated while ~65 columns sat empty and the title floated inside the box:

╭────────────────────────────────────────────────────────────────╮
│  (\/)        (\/)         clawcodex v1.0.0 (2026-07-28)        │
│   \\__      __//                                               │
│  claude-opus-5 ·       ▾ Available Tools                       │
│  Claude Max            file: Edit, Glob, Grep, Read, Write     │
│  /Users/ericlee2/wor…  ▸ Available Skills (8) in 2 categories  │
╰────────────────────────────────────────────────────────────────╯

After — title in the border, identity column sized to its own content and centered, real divider, feed column filling the remainder:

╭─── clawcodex v1.0.0 ───────────────────────────────────────────────────────────╮
│                                                                                │
│            Welcome back, ericlee2            │ ▾ Available Tools               │
│                                              │ file: Edit, Glob, Grep, Read, … │
│               (\/)        (\/)               │ shell: Bash, BashOutput, …      │
│                \\__      __//                │ web: WebFetch, WebSearch        │
│                  \( o  o )/                  │                                 │
│                   |======|                   │ ▸ Available Skills (8) in 2 …   │
│                  /|======|\                  │                                 │
│                  \\______//                  │ ▸ System Prompt — 34 chars      │
│                                              │                                 │
│                 ────────────                 │ ▸ MCP Servers (1) connected     │
│       Model claude-opus-5 · Claude Max       │                                 │
│   Path /Users/ericlee2/workspace/clawcodex   │ ─────────────────────────────── │
│               Session a1b2c3d4               │ 10 tools · 8 skills · 1 MCP · … │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯

Allocation math ported from the in-repo reference — calculateOptimalLeftWidth / calculateLayoutDimensions / formatWelcomeMessage / truncatePath in typescript/src/utils/logoV2Utils.ts. Below ~90 columns the layout stacks rather than splitting.

Defects found and fixed

Each is pinned by a test that fails when the fix is reverted (mutation-tested, counts below).

Defect Effect Tests that catch it
Left column sized to the mascot, not its content cwd truncated to /Users/ericlee2/wor… with ~65 columns empty 2
Split forced when it couldn't fit flex shrank both columns → ▸Available (8) in 2 / Skills categories 2
borderText past cols - 2 ink drops the corners (render-border.ts:43-44) → box with no top edge. brand.name is operator-configurable, so branded installs hit it at ordinary widths 4
Path clipped by code unit CJK cwd overshot, re-truncated at the far end, eliding both ends — killing the segment middle-truncation exists to preserve. Also split surrogate pairs 3
Clipping by code point keycap 1️⃣ undercounts (cluster is 2 columns) → overflow; regional-indicator flags split into reversed letter pairs 2

Clipping now walks grapheme clusters via Intl.Segmenter — the same segmenter stringWidth uses internally.

Supporting changes

  • applyColor exported from the vendored ink fork (+1 line each in entry-exports.ts / index.d.ts) so the border title is tinted through the fork's terminal-aware downgrade instead of a hand-rolled truecolor escape, which would bypass the 8-bit path for legacy Apple Terminal.
  • artWidth measures columns, now that it feeds left-column sizing — a custom bannerHero skin with wide glyphs would otherwise under-size the column it has to fit inside.
  • Dropped the dead release_date field. Its only consumer was the old title; no producer exists (grep -rn release_date --include=*.py returns nothing).
  • Added a pretest hook. The vendored ink dist/ is gitignored and npm test resolves through it, so a stale build fails every ink-importing test with an ESM link error. Verified by deleting dist/entry-exports.js and running npm test.

Testing

  • npm run typecheck clean; eslint clean on all 8 files.
  • Full suite 8 failed / 1545 passed. Baseline on 11c3bedd with these changes stashed is 8 failed / 1493 passed — same five files (createGatewayEventHandler, cursorDriftRegression, statusRule, useConfigSync, virtualHeights). Zero regressions; +52 tests are new here.
  • Rendered and checked at 34/40/60/72/88/96/100/130/140/200 columns, plus deep-cwd, no-version, branded-long-title, and CJK/emoji/keycap/flag cwd cases.
  • Reviewed by the critic subagent across two passes; all raised defects fixed and re-verified.

A trap worth knowing

brandingRenderWidth.test.ts documents it: useStdout() returns process.stdout, not the stream passed to renderSync. The usual harness (Object.assign(passThrough, {columns}), as in brandingMcpCount.test.ts) therefore leaves columns undefined and the component falls back to ?? 100 — so every "render at width N" silently produces the same 100-column layout clipped to N. It looks responsive and tests nothing. Both the critic and I hit this independently.

Relatedly, two of my own tests were initially vacuous: row-width assertions can't see content corruption at identical width, and an ANSI strip that leaves the ESC byte filters out the very rows being asserted on. Both now carry explicit non-vacuity guards.

🤖 Generated with Claude Code

The startup header sized its left column as `min(mascotWidth + 4, cols * 0.4)`,
which never looked at the content it had to hold. At 140 columns that pinned it
to 20 and truncated the cwd to `/Users/ericlee2/wor...` while ~65 columns sat
empty to the right, and the panel title floated inside the box over the right
column only.

Port the allocation math from the in-repo reference
(`calculateOptimalLeftWidth` / `calculateLayoutDimensions` in
typescript/src/utils/logoV2Utils.ts): size the left column to its own widest
line, book the divider and padding explicitly, and hand the remainder to the
feed column. The title moves into the top border, the identity block gains a
welcome line and labeled Model/Path rows, and a rule separates the columns.
Section content is unchanged -- only the allocation.

Four defects surfaced while building it, each pinned by a test that fails when
the fix is reverted:

- Collapse toggles were sibling <Text>es in a flex row, so an overflowing
  toggle wrapped each span into its own column ("|Available  (8) in 2" /
  " Skills      categories") instead of truncating. Now one nested run.

- Splitting was gated on terminal width alone. Because the left column is
  sized to the cwd, a deep path wants 45+ columns at a terminal width of 72;
  forcing the split there overflowed the box and flexbox resolved it by
  shrinking both children. `fitsHorizontal` now also requires the columns to
  fit, and stacks otherwise.

- ink stops embedding `borderText` once the title reaches `cols - 2` and
  instead returns the raw string with the corners dropped
  (render-border.ts:43-44), leaving a box with no top edge. `brand.name` is
  operator-configurable, so a branded install hit this at ordinary widths.
  `borderTitleParts` degrades name+version -> name -> none.

- Path truncation clipped by code unit, so a CJK cwd overshot its budget and
  got re-truncated at the far end -- eliding both ends and destroying the
  trailing segment middle-truncation exists to preserve. It also split
  surrogate pairs. Clipping now walks grapheme clusters via Intl.Segmenter,
  the same segmenter stringWidth uses internally.

Supporting changes: export `applyColor` from the vendored ink fork so the
border title is tinted through the fork's terminal-aware downgrade rather than
a hand-rolled truecolor escape; `artWidth` measures columns now that it feeds
layout sizing; drop the dead `release_date` field, whose only consumer was the
old title; add a `pretest` hook so a stale gitignored ink `dist/` cannot fail
the suite with an ESM link error.

brandingRenderWidth.test.ts documents a trap worth knowing: `useStdout()`
returns `process.stdout`, not the stream passed to `renderSync`, so the usual
`Object.assign(passThrough, {columns})` harness leaves the component on its
`?? 100` fallback and every "render at width N" silently produces the same
100-column layout.

Suite: 8 failed / 1545 passed -- the same 8 pre-existing failures as baseline
on 11c3bed (8 failed / 1493 passed), +52 from the two new files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericleepi314
ericleepi314 merged commit 5775956 into main Jul 29, 2026
3 checks passed
@ericleepi314
ericleepi314 deleted the feat/header-box-allocation branch July 29, 2026 06:10
@github-actions

Copy link
Copy Markdown

Test Results

    1 files      1 suites   8m 5s ⏱️
9 010 tests 9 004 ✅ 6 💤 0 ❌
9 256 runs  9 250 ✅ 6 💤 0 ❌

Results for commit dea83bd.

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.

1 participant