feat(setup): paint card and log bodies in brand cyan
Adds a `brandBody` helper in setup/lib/theme.ts that wraps prose in brand cyan (#2BB7CE), with the same TTY/NO_COLOR/truecolor gating used by `brand`/`brandBold`/`brandChip`. The helper splits multi-line input and colors each line independently so the SGR sequence doesn't bleed across clack's gutter prefix. Routing: - `note()` (the un-dim card wrapper from #2095) now passes `brandBody` as its `format` callback, so card bodies render cyan line-by-line. - Every prose `p.log.{message,info,success,step,warn}` call in the setup flow wraps its body argument in `brandBody`. Calls whose body is explicitly `k.dim(...)` (failure transcript tails, log paths, claude-assist response previews) are left alone — those are the "preview/debug" cases the dim-policy comment in theme.ts already carves out. - Spinner-finish lines in windowed-runner / claude-assist color only the message portion; the `(5s)` elapsed suffix stays dim. Brand cyan accents (chips, wordmark, inline emphasis) are unchanged. This PR only adds the body color. A follow-up will add OSC 11 dark/light detection so light-mode terminals get a brand blue (#2b6fdc) variant — opt-in upgrade with no regression for the dark-mode default.
This commit is contained in:
@@ -24,7 +24,7 @@ import * as p from '@clack/prompts';
|
||||
import k from 'kleur';
|
||||
|
||||
import { ensureAnswer } from './runner.js';
|
||||
import { fitToWidth, note } from './theme.js';
|
||||
import { brandBody, fitToWidth, note } from './theme.js';
|
||||
|
||||
export interface AssistContext {
|
||||
stepName: string;
|
||||
@@ -106,7 +106,7 @@ export async function offerClaudeAssist(
|
||||
|
||||
const parsed = parseResponse(response);
|
||||
if (!parsed) {
|
||||
p.log.warn("Claude responded but I couldn't parse a command out of it.");
|
||||
p.log.warn(brandBody("Claude responded but I couldn't parse a command out of it."));
|
||||
p.log.message(k.dim(response.trim().slice(0, 500)));
|
||||
return false;
|
||||
}
|
||||
@@ -268,7 +268,7 @@ async function queryClaudeUnderSpinner(
|
||||
const elapsed = Math.round((Date.now() - start) / 1000);
|
||||
const suffix = ` (${elapsed}s)`;
|
||||
if (kind === 'ok') {
|
||||
p.log.success(`${fitToWidth('Claude replied.', suffix)}${k.dim(suffix)}`);
|
||||
p.log.success(`${brandBody(fitToWidth('Claude replied.', suffix))}${k.dim(suffix)}`);
|
||||
resolve(payload);
|
||||
} else {
|
||||
p.log.error(
|
||||
|
||||
@@ -27,7 +27,7 @@ import { execSync, spawn } from 'child_process';
|
||||
import * as p from '@clack/prompts';
|
||||
import k from 'kleur';
|
||||
|
||||
import { note } from './theme.js';
|
||||
import { brandBody, note } from './theme.js';
|
||||
|
||||
export interface HandoffContext {
|
||||
/** Channel this handoff is happening in (e.g., 'teams'). */
|
||||
@@ -64,7 +64,7 @@ export interface HandoffContext {
|
||||
export async function offerClaudeHandoff(ctx: HandoffContext): Promise<boolean> {
|
||||
if (!isClaudeUsable()) {
|
||||
p.log.warn(
|
||||
"Claude isn't installed yet — can't hand you off here. Finish setup first, then retry.",
|
||||
brandBody("Claude isn't installed yet — can't hand you off here. Finish setup first, then retry."),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export async function offerClaudeHandoff(ctx: HandoffContext): Promise<boolean>
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
child.on('close', () => {
|
||||
p.log.success("Back from Claude. Let's continue.");
|
||||
p.log.success(brandBody("Back from Claude. Let's continue."));
|
||||
resolve(true);
|
||||
});
|
||||
child.on('error', () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import k from 'kleur';
|
||||
import * as setupLog from '../logs.js';
|
||||
import { offerClaudeAssist } from './claude-assist.js';
|
||||
import { emit as phEmit } from './diagnostics.js';
|
||||
import { fitToWidth } from './theme.js';
|
||||
import { brandBody, fitToWidth } from './theme.js';
|
||||
|
||||
export type Fields = Record<string, string>;
|
||||
export type Block = { type: string; fields: Fields };
|
||||
@@ -390,7 +390,7 @@ export async function fail(
|
||||
const skipList = [
|
||||
...new Set([...existingSkip, ...setupLog.completedStepNames()]),
|
||||
].join(',');
|
||||
p.log.step(`Retrying from ${stepName}…`);
|
||||
p.log.step(brandBody(`Retrying from ${stepName}…`));
|
||||
const result = spawnSync('pnpm', ['--silent', 'run', 'setup:auto'], {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, NANOCLAW_SKIP: skipList },
|
||||
|
||||
@@ -39,6 +39,29 @@ export function brandChip(s: string): string {
|
||||
return k.bgCyan(k.black(k.bold(s)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Brand body color for setup-flow prose. Used for card bodies (via the
|
||||
* `note()` formatter) and `p.log.*` body arguments — anywhere the
|
||||
* previous "dim" treatment was making prose hard to read or washing
|
||||
* out embedded brand emphasis.
|
||||
*
|
||||
* Multi-line input is colored line-by-line so embedded line breaks
|
||||
* don't bleed the SGR sequence across clack's gutter prefix.
|
||||
*/
|
||||
export function brandBody(s: string): string {
|
||||
if (!USE_ANSI) return s;
|
||||
if (TRUECOLOR) {
|
||||
return s
|
||||
.split('\n')
|
||||
.map((line) => (line.length > 0 ? `\x1b[38;2;43;183;206m${line}\x1b[39m` : line))
|
||||
.join('\n');
|
||||
}
|
||||
return s
|
||||
.split('\n')
|
||||
.map((line) => (line.length > 0 ? k.cyan(line) : line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap text so it fits inside clack's gutter without the terminal's soft
|
||||
* wrap breaking the `│ …` bar on long lines. Works on a single string with
|
||||
@@ -70,16 +93,13 @@ export function dimWrap(text: string, gutter: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap clack's `p.note` with the dim formatter disabled. By default
|
||||
* clack renders note bodies through `styleText("dim", …)`, which the
|
||||
* project's prose-readability stance (see `dimWrap` above) explicitly
|
||||
* rejects. Pass-through formatter keeps body text at the terminal's
|
||||
* regular weight; pre-styled segments (chips, bold, brand color) come
|
||||
* through unfaded.
|
||||
* Wrap clack's `p.note` so card bodies render in the brand body color
|
||||
* (#2b6fdc) instead of clack's default dim. Clack runs the formatter
|
||||
* on each line individually, so `brandBody` colors each line cleanly
|
||||
* without bleeding across the gutter prefix.
|
||||
*/
|
||||
const passthroughFormat = (s: string): string => s;
|
||||
export function note(message: string, title?: string): void {
|
||||
p.note(message, title, { format: passthroughFormat });
|
||||
p.note(message, title, { format: brandBody });
|
||||
}
|
||||
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { emit as phEmit } from './diagnostics.js';
|
||||
import type { StepResult, SpinnerLabels } from './runner.js';
|
||||
import { dumpTranscriptOnFailure, spawnStep, writeStepEntry } from './runner.js';
|
||||
import * as setupLog from '../logs.js';
|
||||
import { fitToWidth } from './theme.js';
|
||||
import { brandBody, fitToWidth } from './theme.js';
|
||||
|
||||
const WINDOW_SIZE = 3;
|
||||
const SPINNER_FRAMES = ['◒', '◐', '◓', '◑'];
|
||||
@@ -169,7 +169,7 @@ async function runUnderWindow(
|
||||
if (result.ok) {
|
||||
const isSkipped = result.terminal?.fields.STATUS === 'skipped';
|
||||
const msg = isSkipped && labels.skipped ? labels.skipped : labels.done;
|
||||
p.log.success(`${fitToWidth(msg, suffix)}${k.dim(suffix)}`);
|
||||
p.log.success(`${brandBody(fitToWidth(msg, suffix))}${k.dim(suffix)}`);
|
||||
} else {
|
||||
const failMsg = labels.failed ?? labels.running.replace(/…$/, ' failed');
|
||||
p.log.error(`${fitToWidth(failMsg, suffix)}${k.dim(suffix)}`);
|
||||
@@ -185,7 +185,7 @@ async function handleStall(
|
||||
): Promise<void> {
|
||||
render.pauseRender();
|
||||
p.log.warn(
|
||||
`This looks stuck — no output from the ${stepName} step for the last 60 seconds.`,
|
||||
brandBody(`This looks stuck — no output from the ${stepName} step for the last 60 seconds.`),
|
||||
);
|
||||
phEmit('step_stalled', { step: stepName });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user