Skip to content

feat(cli): add the boot chrome the dev TUI draws before the session starts - #15

Merged
amondnet merged 7 commits into
mainfrom
amondnet/dev
Aug 28, 2026
Merged

feat(cli): add the boot chrome the dev TUI draws before the session starts#15
amondnet merged 7 commits into
mainfrom
amondnet/dev

Conversation

@amondnet

@amondnet amondnet commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new private workspace package @pleaseai/cli (packages/cli) holding the terminal "boot chrome" that a future please dev command will draw before handing the session to @ai-sdk/tui.

The code is ported from vercel/eve's packages/eve/src/cli/ui (Apache-2.0, the same license as this repo; attributed in NOTICE and in per-file headers), trimmed to what the boot needs and simplified where the runtime has caught up — Intl.Segmenter and Unicode property escapes replace eve's hand-written tables.

Modules:

Module What it does
sanitize.ts Strips control input, skipping escape sequences as whole units rather than just the ESC byte
text.ts Grapheme-cluster width via Intl.Segmenter — a Hangul glyph measures two cells
theme.ts Palette, banner, aligned sections, tagged lines
live-region.ts In-place repaint, plus committing finished phases to scrollback
progress-pulse.ts + boot-row.ts One pulsing line naming the boot phase currently running

Also included: docs/dev-tui.md (the design record), the NOTICE attribution, sonar-project.properties registration for the new package, and README / README.ko layout and docs-list updates.

57 tests; mise run ci passes.

Why the split exists

@ai-sdk/tui@1.0.84 renders the session in the alternate screen buffer — its dist contains the ?1049h / ?1049l pair — so everything it draws is gone on exit. eve's live region is built on the opposite premise: native scrollback, transcript survives.

The two cannot be merged, and do not need to be. @ai-sdk/tui owns the session; this chrome owns the main screen on either side of it. A run that never reaches the interactive UI has nothing to say from inside a buffer it never entered.

Why the dev command is not in this PR

runAgentTUI structurally takes an Agent, but the shipped implementation reads exactly two members — agent.stream and agent.tools. That was verified by grepping the published bundle, not taken from the docs.

The in-progress defineAgent surface exposes neither: its AgentSession has only prompt(), which wraps generate() and returns a finished turn — and incremental rendering is the whole point of the interactive UI. docs/dev-tui.md records three ways out and deliberately picks none, because that is an API decision rather than an omission.

@pleaseai/cli is therefore private: true, and registered in sonar but not yet in release-please: publishing an empty please binary would be worse than publishing none.

Related issue

None — this work has no tracking issue.

Checklist

  • PR title follows Conventional Commits
  • Tests added or updated, and the suite passes (bun run test)
  • Lint and type-check pass (bun run lint, bun run type-check)
  • Documentation updated if behavior changed
  • No breaking change, or a BREAKING CHANGE: note is included

Summary by cubic

Adds the boot chrome the future please dev command draws before handing its session to @ai-sdk/tui — palette, banner, grapheme-aware width fitting, an inline repaint region, and a pulsing boot row — as a new private package @pleasedev/cli (packages/cli). The code is ported from vercel/eve's cli/ui (Apache-2.0, attributed in NOTICE), owns the main screen while @ai-sdk/tui takes the alternate buffer, and ships with docs/dev-tui.md and a passing test suite.

Fixes from review

  • The banner rule and tagged-line indentation now measure in terminal cells, so a wide CJK title no longer gets a rule visibly shorter than the text.
  • CI now normalizes and uploads coverage for every package — not just packages/core — so the new package's files are no longer reported at 0%.
  • Colour detection follows picocolors, so NO_COLOR, FORCE_COLOR, and CI behave as intended; sanitization consumes ESC # 8 and stops at malformed input; the pulse sequence rejects all-unlit; and a reused phase name prints again after a commit.
  • The package follows the @pleasedev scope rename from main, and the lockfile was regenerated so CI's frozen install resolves it.

Why please dev is absent

  • @ai-sdk/tui takes the alternate screen buffer for the session; this chrome owns the main screen on either side of it, and the two cannot merge.
  • runAgentTUI consumes exactly agent.stream and agent.tools; the merged defineAgent still exposes neither, so the interactive command waits on an API decision documented in docs/dev-tui.md.
  • The package is private: true and out of release-please until it has a command, because publishing an empty please binary is worse than none.

Written for commit 5cf709e. Summary will update on new commits.

…tarts

`please dev` will hand its session to `@ai-sdk/tui`, which takes the alternate
screen buffer and gives it back untouched on exit. Everything either side of
that hand-off belongs to the main screen, and has to survive the buffer being
torn down — a run that never reaches the interactive UI has nothing to say from
inside a buffer it never entered.

So this is the main screen's half: a palette, a banner, aligned sections,
grapheme-aware width measurement, an inline repaint engine, and a one-line
pulsing boot row that names the phase a container start is currently in. Ported
from vercel/eve's `cli/ui` (Apache-2.0, attributed in NOTICE), trimmed to what
the boot needs and simplified where the runtime has caught up — `Intl.Segmenter`
and Unicode property escapes replace eve's hand-written tables.

The `dev` command itself is not here. It needs `stream` and `tools` off the
agent, and `defineAgent` exposes neither; `docs/dev-tui.md` states the three
ways out rather than picking one silently.

`@pleaseai/cli` is private until it has a command — an empty `please` binary is
worse than none — so it is registered in sonar but not yet in release-please.
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.61702% with 30 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/cli/src/ui/boot-row.ts 85.18% 16 Missing ⚠️
packages/cli/src/ui/sanitize.ts 95.41% 5 Missing ⚠️
packages/cli/src/ui/theme.ts 93.05% 5 Missing ⚠️
packages/cli/src/ui/live-region.ts 93.33% 2 Missing ⚠️
packages/cli/src/ui/text.ts 98.23% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

…ckage's coverage

Three fixes from review of the boot chrome.

`renderCliBanner` ruled its title with `String.length` and
`renderCliTaggedLine` indented continuations by the same measure — the exact
mistake `text.ts` exists to prevent. A 13-cell Korean title got a 7-character
rule under it. Both now use `visibleLength`, pinned by tests that fail against
the old measure.

CI normalized and uploaded coverage for `packages/core` by hardcoded path, so
the `packages/cli` lcov added in the previous commit was referenced by Sonar
but never rewritten or uploaded: bun emits package-relative `SF:` paths, which
Sonar resolves from the repo root and cannot match, landing every new file in
new code at 0%. The normalize step now loops over `packages/*/coverage`,
deriving each prefix from the path so the next package needs no change here,
and both Codecov steps upload core's and cli's reports.

Also: `commit()` repeated the row render already in `paint()`, and a
`lastIndex` reset before `String.prototype.search` did nothing, since `search`
runs from 0 and restores it.
main renamed the npm scope from `@pleaseai` to `@pleasedev` (#14) and pinned
bun 1.4.0 (#17) after this branch was pushed. Only the two READMEs and the
lockfile conflicted textually, but the rename reaches further than any merge
could show: `packages/cli` is a directory main has never seen, so its
`@pleaseai/cli` name and every `@pleaseai/*` reference in `docs/dev-tui.md`
would have survived a clean-looking resolution. Both now follow the new scope.

The lockfile was taken from main and regenerated rather than hand-merged.

Left alone deliberately: `@pleaseai/sandbox-contract`, `@pleaseai/sandbox-e2b`
and `@pleaseai/harness-cf-transport` in the sandbox sources name upstream
packages in another repository, not this workspace, which is why #14 did not
touch them either. The `pleaseai.dev` contact addresses are a domain, not a
scope.
The scope rename landed after `bun install` had already written the workspace
entry, so bun.lock still carried `@pleaseai/cli` and `bun install
--frozen-lockfile` — what CI runs — refused it. Regenerated under the pinned
bun 1.4.0 and verified frozen.
@amondnet
amondnet marked this pull request as ready for review August 28, 2026 08:43

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 23 files

Architecture diagram
sequenceDiagram
    participant UI as Boot Chrome (packages/cli/src/ui)
    participant Render as LiveRegion
    participant Text as text.ts (Intl.Segmenter)
    participant Sanitize as sanitize.ts
    participant Main as Main Screen
    participant Alternate as @ai-sdk/tui (Alternate Buffer)
    participant Scroll as Native Scrollback

    Note over UI,Alternate: TERMINAL SPLIT: Boot chrome owns main screen<br/>@ai-sdk/tui owns alternate buffer for session

    UI->>UI: Start boot row
    UI->>Render: Create LiveRegion

    loop Each boot phase (image pull, container start)
        UI->>Text: Measure phase text width
        UI->>Sanitize: Sanitize external values (container id, docker output)
        Sanitize-->>UI: Clean text
        UI->>UI: Fit detail to column width (ellipsize if needed)
        UI->>Render: update() with single row
        Render->>Main: Paint row (sync markers, cursor controls)
        Main-->>Render: Row rendered
    
        alt Phase completes
            UI->>Render: flush(committedLine, currentRow)
            Render->>Scroll: Commit phase line to scrollback
            Render->>Main: Repaint live row above scrollback
        end
    end

    Note over UI,Alternate: HAND-OFF TO SESSION
    UI->>UI: Stop boot row (erase live region)
    UI->>Alternate: Enter alternate screen buffer (goes dark)
    Alternate-->>Main: Session ends, buffer torn down
    Main-->>UI: Main screen restored with committed scrollback

    Note over UI,Alternate: Initial boot error path
    alt Boot phase fails before TUI
        UI->>Render: commit() error line
        Render->>Scroll: Write error to scrollback
        UI-->>UI: Exit without entering alternate buffer
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/cli/src/ui/theme.ts Outdated
Comment thread packages/cli/src/ui/progress-pulse.ts
Comment thread packages/cli/src/ui/live-region.ts
Comment thread packages/cli/src/ui/sanitize.ts Outdated
Comment thread packages/cli/src/ui/boot-row.ts
cubic raised five findings on the boot chrome; four are fixed here and the
fifth is tracked instead.

- `theme.ts` passed a bare `process.stdout.isTTY` to picocolors, overriding the
  detection that honours `NO_COLOR`, `--no-color` and `TERM=dumb` — a terminal
  that asked for no colour got escapes anyway. Now delegated outright. The first
  attempt ANDed the two, which the local ocr pass caught as an over-correction:
  it re-breaks `FORCE_COLOR` and `CI`, the branches that exist to enable colour
  without a TTY. picocolors reads the environment once at import, so the tests
  pin this from child processes with controlled environments.
- `assertPulseSequence` accepted an all-unlit sequence, which animates without
  ever drawing the glyph the row exists to show.
- `sanitize.ts` matched only charset-designation introducers, so `ESC # 8` left
  its `8` behind as visible text. Generalised to the ECMA-48 shape, which covers
  the charset case as one instance. The local gpt pass then caught a regression
  in that generalisation: the scan ran past a byte that cannot legally continue
  a sequence, looking for a terminator that was not coming, and ate the text on
  the way — `ESC ( <emoji> visible` came out as `isible`. It now stops at the
  malformed byte and leaves the rest as text.
- `boot-row.ts` kept its non-TTY dedupe state across `commit()`, so a phase name
  reused after a commit printed nothing at all.

Left open: the live region miscounts its rows when the terminal is narrowed
mid-paint. Real, but the fix needs a `SIGWINCH` listener and a teardown owner —
the same design decision the cursor hide/restore gap is waiting on. Filed rather
than guessed at.
…eAgent

`defineAgent` landed on main (#13) — the API this note said the `dev` command
was waiting for. Verified the two gaps against what actually shipped rather
than assuming they carried over: `AgentSession` exposes `prompt` and no
`stream`, and `Agent` exposes `createSession` and no `tools`, which is exactly
what `runAgentTUI` reads. So the analysis holds and only the status framing was
stale — "waiting for it to land" is now "the shape that landed does not expose
these".

The README layout conflict is resolved in main's favour, with `packages/cli`
and `docs/dev-tui.md` re-added to the restructured tree. main also renamed
project-layout.md from "the first argument" to "the argument", which left the
dev-tui sentence calling itself "the second" without an antecedent; reworded in
both READMEs.

Lockfile taken from main and regenerated under the pinned bun, verified frozen.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 8 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/cli/src/ui/theme.ts
…alsified

The module header still claimed a CI log "prints the same text without escapes".
Delegating to picocolors made that false — `CI` is one of the branches that
enables colour without a terminal — and it contradicted a test in the same
package that pins exactly that. Rewritten to say who decides and what each
environment does.
@amondnet
amondnet added this pull request to the merge queue Aug 28, 2026
Merged via the queue into main with commit 7094dc5 Aug 28, 2026
7 checks passed
@amondnet
amondnet deleted the amondnet/dev branch August 28, 2026 09:09
amondnet added a commit that referenced this pull request Aug 28, 2026
Takes in `defineAgent` / `defineSandbox` (#13) and the dev-TUI boot chrome
(#15). Both READMEs conflicted where main rewrote the Status section around the
new public API while this branch added three backend rows and a note on where
each backend's suite actually runs; the resolution keeps main's narrative and
appends the coverage note, and folds the three new backend directories into
main's deeper `src/agent` + `src/sandbox` layout tree.

The three backends this branch adds ship no `SandboxBackendFactory` — the
`docker({ image })` shape main introduced — because the adaptation is not
mechanical for any of them: microsandbox takes ports as a guest→host map rather
than a list, and just-bash publishes no ports at all, which `resolveSandbox`
requires. Filed as a follow-up rather than decided in a merge.
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