feat(cards): wrap 16 cards in round-border Card primitive — port the demo look to live chat

card-demo had a polished round-border + paddingX=1 wrapper around each
card, but the live chat path (src/cli/ui/cards/*.tsx) never adopted it,
so the demo and the actual chat looked completely different. New
primitive `Card({tone, children})` ports that wrapper to the real cards.

Refactored cards now flow through `<Card tone={...}>` with their content
flush left inside the border (paddingX=1 on the wrapper provides the gap;
removed the per-body `paddingLeft={2}` that would have stacked).

Border tone tracks status:
- ok / running / settled — TONE.ok / brand / accent
- error / warn / abort  — TONE.err / warn
- meta-info             — FG.meta (usage / memory)

LiveCard skipped — it's a one-line transient toast and a 3-row border
would defeat the point. CompactUsageRow path also stays flat.

DEC 2026 sync from 0.24.1 means the border resizing for streaming
content (reasoning/tool/streaming card body growing) lands atomically
per frame; no flicker layered on top of what we already had.
This commit is contained in:
esengine committed 2026-05-04 05:44:00 -07:00
1 parent 51a5f9dd8e
commit e78112bf4f
17 files changed
+159 -183

No files matched your search

+7 -8
View File
@@ -1,6 +1,7 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { BranchCard as BranchCardData } from "../state/cards.js";
import { CARD, FG, TONE } from "../theme/tokens.js";
@@ -12,25 +13,23 @@ export function BranchCard({ card }: { card: BranchCardData }): React.ReactEleme
const filled = Math.max(0, Math.min(BAR_CELLS, Math.round(ratio * BAR_CELLS)));
const tone = card.done ? TONE.ok : CARD.branch.color;
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={tone}>
<CardHeader
glyph="⎇"
tone={tone}
title={card.done ? "branching done" : "branching"}
meta={[`${card.completed} of ${card.total} samples`]}
/>
<Box paddingLeft={2} flexDirection="row" gap={1}>
<Box flexDirection="row" gap={1}>
<Text color={tone}>{"█".repeat(filled)}</Text>
<Text color={FG.faint}>{"░".repeat(BAR_CELLS - filled)}</Text>
<Text color={FG.faint}>{`${(ratio * 100).toFixed(0)}%`}</Text>
</Box>
{!card.done && card.completed > 0 ? (
<Box paddingLeft={2}>
<Text color={FG.faint}>
{`latest · #${card.latestIndex} · T=${card.latestTemperature.toFixed(2)} · ${card.latestUncertainties} unc`}
</Text>
</Box>
<Text color={FG.faint}>
{`latest · #${card.latestIndex} · T=${card.latestTemperature.toFixed(2)} · ${card.latestUncertainties} unc`}
</Text>
) : null}
</Box>
</Card>
);
}
+15 -18
View File
@@ -1,6 +1,8 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { CtxCard as CtxCardData } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
@@ -9,7 +11,7 @@ const BAR_CELLS = 32;
function row(label: string, tokens: number, ratio: number, color: string): React.ReactElement {
const filled = Math.max(0, Math.min(BAR_CELLS, Math.round(ratio * BAR_CELLS)));
return (
<Box paddingLeft={2} flexDirection="row" gap={1}>
<Box flexDirection="row" gap={1}>
<Text color={FG.sub}>{label.padEnd(7)}</Text>
<Text color={color}>{"█".repeat(filled)}</Text>
<Text color={FG.faint}>{"░".repeat(BAR_CELLS - filled)}</Text>
@@ -27,35 +29,30 @@ export function CtxCard({ card }: { card: CtxCardData }): React.ReactElement {
const usedPct = (used / cap) * 100;
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row" gap={1}>
<Text color={TONE.brand}>⌘</Text>
<Text color={TONE.brand} bold>
context
</Text>
<Text color={FG.faint}>
{`· ${used.toLocaleString()} / ${cap.toLocaleString()} (${usedPct.toFixed(1)}%)`}
</Text>
</Box>
<Card tone={TONE.brand}>
<CardHeader
glyph="⌘"
tone={TONE.brand}
title="context"
meta={[`${used.toLocaleString()} / ${cap.toLocaleString()} (${usedPct.toFixed(1)}%)`]}
/>
{row("system", card.systemTokens, card.systemTokens / cap, TONE.brand)}
{row("tools", card.toolsTokens, card.toolsTokens / cap, TONE.warn)}
{row("log", card.logTokens, card.logTokens / cap, TONE.ok)}
{row("input", card.inputTokens, card.inputTokens / cap, TONE.accent)}
{card.topTools.length > 0 ? (
<>
<Box paddingLeft={2}>
<Text color={FG.faint}>
{`top tools · ${card.toolsCount} total · ${card.logMessages} log msgs`}
</Text>
</Box>
<Text color={FG.faint}>
{`top tools · ${card.toolsCount} total · ${card.logMessages} log msgs`}
</Text>
{card.topTools.slice(0, 5).map((t) => (
<Box key={`${t.turn}-${t.name}`} paddingLeft={2} flexDirection="row" gap={1}>
<Box key={`${t.turn}-${t.name}`} flexDirection="row" gap={1}>
<Text color={FG.sub}>{t.name}</Text>
<Text color={FG.faint}>{`· turn ${t.turn} · ${t.tokens.toLocaleString()}`}</Text>
</Box>
))}
</>
) : null}
</Box>
</Card>
);
}
+8 -14
View File
@@ -1,6 +1,7 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { DiffCard as DiffCardData } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
@@ -22,7 +23,7 @@ const LINE_GLYPH = {
export function DiffCard({ card }: { card: DiffCardData }): React.ReactElement {
const showFooter = card.hunks.length > 0;
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={TONE.warn}>
<CardHeader
glyph="±"
tone={TONE.warn}
@@ -35,18 +36,11 @@ export function DiffCard({ card }: { card: DiffCardData }): React.ReactElement {
/>
{card.hunks.map((hunk) => (
<Box key={`${card.id}:${hunk.header}`} flexDirection="column">
<Box paddingLeft={2}>
<Text italic color={FG.faint}>
{hunk.header}
</Text>
</Box>
<Text italic color={FG.faint}>
{hunk.header}
</Text>
{hunk.lines.map((line, li) => (
<Box
key={`${card.id}:${hunk.header}:${li}`}
paddingLeft={2}
flexDirection="row"
gap={1}
>
<Box key={`${card.id}:${hunk.header}:${li}`} flexDirection="row" gap={1}>
<Text color={LINE_COLOR[line.kind]}>{LINE_GLYPH[line.kind]}</Text>
<Text color={LINE_COLOR[line.kind]} dimColor={line.kind === "ctx"}>
{line.text}
@@ -56,7 +50,7 @@ export function DiffCard({ card }: { card: DiffCardData }): React.ReactElement {
</Box>
))}
{showFooter && (
<Box paddingLeft={2} flexDirection="row" gap={2}>
<Box flexDirection="row" gap={2}>
<Text bold color={TONE.ok}>
[a] apply
</Text>
@@ -66,6 +60,6 @@ export function DiffCard({ card }: { card: DiffCardData }): React.ReactElement {
</Text>
</Box>
)}
</Box>
</Card>
);
}
+6 -10
View File
@@ -1,6 +1,8 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { DoctorCard as DoctorCardData, DoctorCheckEntry } from "../state/cards.js";
import { CARD, FG, TONE } from "../theme/tokens.js";
@@ -30,16 +32,10 @@ export function DoctorCard({ card }: { card: DoctorCardData }): React.ReactEleme
const summary = `${card.checks.length} checks · ${ok} passed${warn > 0 ? ` · ${warn} warn` : ""}${fail > 0 ? ` · ${fail} fail` : ""}`;
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row" gap={1}>
<Text color={CARD.tool.color}>⚕</Text>
<Text color={CARD.tool.color} bold>
doctor
</Text>
<Text color={FG.faint}>{`· ${summary}`}</Text>
</Box>
<Card tone={CARD.tool.color}>
<CardHeader glyph="⚕" tone={CARD.tool.color} title="doctor" meta={[summary]} />
{card.checks.map((c) => (
<Box key={c.label} paddingLeft={2} flexDirection="row" gap={1}>
<Box key={c.label} flexDirection="row" gap={1}>
<Text color={LEVEL_COLOR[c.level]}>{LEVEL_GLYPH[c.level]}</Text>
<Text bold color={FG.body}>
{c.label.padEnd(labelWidth + 1)}
@@ -48,6 +44,6 @@ export function DoctorCard({ card }: { card: DoctorCardData }): React.ReactEleme
<Text color={LEVEL_COLOR[c.level]}>{LEVEL_TAG[c.level]}</Text>
</Box>
))}
</Box>
</Card>
);
}
+23 -26
View File
@@ -1,6 +1,8 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { ErrorCard as ErrorCardData } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
@@ -9,7 +11,7 @@ const STACK_TAIL = 5;
export function ErrorCard({ card }: { card: ErrorCardData }): React.ReactElement {
const retryNote =
card.retries !== undefined && card.retries > 0
? `· ${card.retries} retr${card.retries === 1 ? "y" : "ies"}`
? `${card.retries} retr${card.retries === 1 ? "y" : "ies"}`
: null;
const stackLines = card.stack ? card.stack.split("\n") : [];
const stackTrunc = stackLines.length > STACK_TAIL;
@@ -19,38 +21,33 @@ export function ErrorCard({ card }: { card: ErrorCardData }): React.ReactElement
const messageLines = card.message.split("\n");
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row" gap={1}>
<Text color={TONE.err}>✖</Text>
<Text color={TONE.err} bold>
{card.title || "error"}
</Text>
{retryNote ? <Text color={FG.faint}>{retryNote}</Text> : null}
</Box>
<Card tone={TONE.err}>
<CardHeader
glyph="✖"
tone={TONE.err}
title={card.title || "error"}
meta={retryNote ? [retryNote] : undefined}
/>
{messageLines.map((line, i) => (
<Box key={`${card.id}:msg:${i}`} paddingLeft={2}>
<Text color={TONE.err}>{line || " "}</Text>
</Box>
<Text key={`${card.id}:msg:${i}`} color={TONE.err}>
{line || " "}
</Text>
))}
{hasStack ? (
<>
<Box paddingLeft={2} marginTop={1}>
<Text color={FG.meta}>stack trace</Text>
</Box>
<Box flexDirection="column" marginTop={1}>
<Text color={FG.meta}>stack trace</Text>
{stackHidden > 0 ? (
<Box paddingLeft={2}>
<Text color={FG.faint}>
{`⋮ ${stackHidden} earlier stack line${stackHidden === 1 ? "" : "s"} hidden`}
</Text>
</Box>
<Text color={FG.faint}>
{`⋮ ${stackHidden} earlier stack line${stackHidden === 1 ? "" : "s"} hidden`}
</Text>
) : null}
{stackVisible.map((line, i) => (
<Box key={`${card.id}:stk:${stackHidden + i}`} paddingLeft={2}>
<Text color={FG.meta}>{line || " "}</Text>
</Box>
<Text key={`${card.id}:stk:${stackHidden + i}`} color={FG.meta}>
{line || " "}
</Text>
))}
</>
</Box>
) : null}
</Box>
</Card>
);
}
+6 -11
View File
@@ -1,6 +1,7 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { MemoryCard as MemoryCardData, MemoryEntry } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
@@ -41,7 +42,7 @@ export function MemoryCard({ card }: { card: MemoryCardData }): React.ReactEleme
const tokens =
card.tokens > 1024 ? `~${(card.tokens / 1024).toFixed(1)}K tok` : `~${card.tokens} tok`;
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={FG.meta}>
<CardHeader
glyph="⌑"
tone={FG.meta}
@@ -55,24 +56,18 @@ export function MemoryCard({ card }: { card: MemoryCardData }): React.ReactEleme
const remaining = all.length - shown.length;
return (
<Box key={category} flexDirection="column">
<Box paddingLeft={2}>
<Text color={FG.faint}>{`${CATEGORY_LABEL[category]} (${counts[category]})`}</Text>
</Box>
<Text color={FG.faint}>{`${CATEGORY_LABEL[category]} (${counts[category]})`}</Text>
{shown.map((entry) => (
<Box key={`${category}:${entry.summary}`} paddingLeft={2} flexDirection="row" gap={1}>
<Box key={`${category}:${entry.summary}`} flexDirection="row" gap={1}>
<Text color={CATEGORY_GLYPH_COLOR[category]}>{CATEGORY_GLYPH[category]}</Text>
<Text color={FG.sub}>{entry.summary}</Text>
</Box>
))}
{remaining > 0 ? (
<Box paddingLeft={2}>
<Text color={FG.faint}>{`⋮ +${remaining} more`}</Text>
</Box>
) : null}
{remaining > 0 ? <Text color={FG.faint}>{`⋮ +${remaining} more`}</Text> : null}
</Box>
);
})}
</Box>
</Card>
);
}
+4 -3
View File
@@ -1,6 +1,7 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { PlanCard as PlanCardData, PlanStep } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
@@ -30,13 +31,13 @@ export function PlanCard({ card }: { card: PlanCardData }): React.ReactElement {
const progress = `${variantTag}${doneCount}/${card.steps.length} done`;
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={TONE.accent}>
<CardHeader glyph="⊞" tone={TONE.accent} title={card.title} meta={[progress]} />
{card.steps.map((step, i) => {
const isActive = step.status === "running";
const titleColor = isActive ? FG.strong : FG.sub;
return (
<Box key={step.id} paddingLeft={2} flexDirection="row" gap={1}>
<Box key={step.id} flexDirection="row" gap={1}>
<Text color={STATUS_COLOR[step.status]}>{STATUS_GLYPH[step.status]}</Text>
<Text bold={isActive} color={titleColor}>
{`${i + 1}. ${step.title}`}
@@ -45,6 +46,6 @@ export function PlanCard({ card }: { card: PlanCardData }): React.ReactElement {
</Box>
);
})}
</Box>
</Card>
);
}
+7 -10
View File
@@ -2,6 +2,7 @@ import { Box, Text, useStdout } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { clipToCells, wrapToCells } from "../../../frame/width.js";
import { Card } from "../primitives/Card.js";
import { CardHeader, type MetaItem } from "../primitives/CardHeader.js";
import { CursorBlock } from "../primitives/CursorBlock.js";
import { Spinner } from "../primitives/Spinner.js";
@@ -12,7 +13,6 @@ import { FG, TONE } from "../theme/tokens.js";
const STREAMING_PREVIEW_LINES = 4;
/** Once settled, only the conclusion is actionable; the rest is in `/reasoning last`. */
const SETTLED_TAIL_LINES = 2;
const BODY_PAD = 2;
export function ReasoningCard({
card,
@@ -23,13 +23,14 @@ export function ReasoningCard({
}): React.ReactElement {
const { stdout } = useStdout();
const cols = stdout?.columns ?? 80;
const lineCells = Math.max(20, cols - BODY_PAD - 4);
const lineCells = Math.max(20, cols - 4);
const allLines = card.text.length > 0 ? card.text.split("\n") : [];
const showBody = expanded && (allLines.length > 0 || card.streaming);
const tone = card.aborted ? TONE.err : TONE.accent;
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={tone}>
<ReasoningHeader card={card} />
{showBody &&
(card.streaming ? (
@@ -37,7 +38,7 @@ export function ReasoningCard({
) : (
<SettledPreview card={card} allLines={allLines} lineCells={lineCells} />
))}
</Box>
</Card>
);
}
@@ -122,7 +123,7 @@ function BodyLines({
{lines.map((line, i) => {
const isLast = i === lines.length - 1;
return (
<Box key={`${card.id}:b:${indexOffset + i}`} paddingLeft={BODY_PAD} flexDirection="row">
<Box key={`${card.id}:b:${indexOffset + i}`} flexDirection="row">
<Text italic color={FG.meta}>
{clipToCells(line, lineCells)}
</Text>
@@ -148,9 +149,5 @@ function ElisionHint({
parts.push(`${droppedLines} line${droppedLines === 1 ? "" : "s"}`);
}
if (card.tokens > 0) parts.push(`${card.tokens.toLocaleString()} tok`);
return (
<Box paddingLeft={BODY_PAD}>
<Text color={FG.faint}>{`⋯ ${parts.join(" · ")} above · /reasoning last`}</Text>
</Box>
);
return <Text color={FG.faint}>{`⋯ ${parts.join(" · ")} above · /reasoning last`}</Text>;
}
+8 -11
View File
@@ -1,6 +1,7 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { SearchCard as SearchCardData, SearchHit } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
@@ -15,7 +16,7 @@ export function SearchCard({ card }: { card: SearchCardData }): React.ReactEleme
const grouped = groupByFile(card.hits.slice(0, 10));
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={TONE.info}>
<CardHeader
glyph="⊙"
tone={TONE.info}
@@ -25,13 +26,11 @@ export function SearchCard({ card }: { card: SearchCardData }): React.ReactEleme
/>
{grouped.map(([file, hits]) => (
<Box key={file} flexDirection="column">
<Box paddingLeft={2}>
<Text bold color={FG.strong}>
{file}
</Text>
</Box>
<Text bold color={FG.strong}>
{file}
</Text>
{hits.map((h, i) => (
<Box key={`${file}:${h.line}:${i}`} paddingLeft={2} flexDirection="row" gap={1}>
<Box key={`${file}:${h.line}:${i}`} flexDirection="row" gap={1}>
<Text color={FG.faint}>{`${h.line.toString().padStart(4)} │`}</Text>
<HighlightedLine text={h.preview} start={h.matchStart} end={h.matchEnd} />
</Box>
@@ -39,11 +38,9 @@ export function SearchCard({ card }: { card: SearchCardData }): React.ReactEleme
</Box>
))}
{card.hits.length > 10 ? (
<Box paddingLeft={2}>
<Text color={FG.faint}>{`⋮ +${card.hits.length - 10} more hits`}</Text>
</Box>
<Text color={FG.faint}>{`⋮ +${card.hits.length - 10} more hits`}</Text>
) : null}
</Box>
</Card>
);
}
+9 -20
View File
@@ -4,12 +4,12 @@ import React from "react";
import { clipToCells, wrapToCells } from "../../../frame/width.js";
import { useReserveRows } from "../layout/viewport-budget.js";
import { Markdown } from "../markdown.js";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import { Spinner } from "../primitives/Spinner.js";
import type { StreamingCard as StreamingCardData } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
const BODY_PAD = 2;
/** Streaming preview tail length — bounded live region so chunks don't thrash whole-card layout. */
const STREAMING_PREVIEW_LINES = 4;
@@ -23,16 +23,14 @@ export function StreamingCard({ card }: { card: StreamingCardData }): React.Reac
if (card.done && !card.aborted) {
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={TONE.ok}>
<CardHeader glyph="‹" tone={TONE.ok} title="reply" />
<Box paddingLeft={BODY_PAD} flexDirection="column">
<Markdown text={card.text} />
</Box>
</Box>
<Markdown text={card.text} />
</Card>
);
}
const lineCells = Math.max(20, cols - BODY_PAD - 4);
const lineCells = Math.max(20, cols - 4);
const allLines = card.text.length > 0 ? card.text.split("\n") : [""];
const visualLines = allLines.flatMap((l) => wrapToCells(l, lineCells));
const visible = visualLines.slice(-STREAMING_PREVIEW_LINES);
@@ -42,28 +40,19 @@ export function StreamingCard({ card }: { card: StreamingCardData }): React.Reac
const headLabel = aborted ? "aborted" : "writing…";
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={headColor}>
<CardHeader
glyph={glyph}
tone={headColor}
title={headLabel}
right={aborted ? undefined : <Spinner kind="braille" color={TONE.brand} />}
/>
{visible.map((line, i) => (
<Box
key={`${card.id}:${allLines.length - visible.length + i}`}
paddingLeft={BODY_PAD}
flexDirection="row"
>
<Box key={`${card.id}:${allLines.length - visible.length + i}`} flexDirection="row">
<Text color={aborted ? FG.meta : FG.body}>{clipToCells(line, lineCells)}</Text>
</Box>
))}
{aborted ? (
<Box paddingLeft={BODY_PAD}>
<Text color={FG.faint}>[truncated by esc]</Text>
</Box>
) : null}
</Box>
{aborted ? <Text color={FG.faint}>[truncated by esc]</Text> : null}
</Card>
);
}
+6 -9
View File
@@ -1,6 +1,7 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card as CardWrap } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { Card, SubAgentCard as SubAgentCardData } from "../state/cards.js";
import { CARD, FG, TONE } from "../theme/tokens.js";
@@ -15,7 +16,7 @@ export function SubAgentCard({ card }: { card: SubAgentCardData }): React.ReactE
const headColor = STATUS_COLOR[card.status];
const headGlyph = card.status === "failed" ? "✖" : "⌬";
return (
<Box flexDirection="column" marginTop={1}>
<CardWrap tone={headColor}>
<CardHeader
glyph={headGlyph}
tone={headColor}
@@ -24,21 +25,17 @@ export function SubAgentCard({ card }: { card: SubAgentCardData }): React.ReactE
subtitle={card.name}
meta={[{ text: card.status, color: headColor }]}
/>
<Box paddingLeft={2}>
<Text color={FG.sub}>{card.task}</Text>
</Box>
<Text color={FG.sub}>{card.task}</Text>
{card.tools && card.tools.length > 0 && (
<Box paddingLeft={2}>
<Text color={FG.faint}>{`tools · ${card.tools.join(", ")}`}</Text>
</Box>
<Text color={FG.faint}>{`tools · ${card.tools.join(", ")}`}</Text>
)}
{card.children.map((child) => (
<Box key={child.id} paddingLeft={2} flexDirection="row" gap={1}>
<Box key={child.id} flexDirection="row" gap={1}>
<Text color={TONE.violet}>▎</Text>
<ChildSummary card={child} />
</Box>
))}
</Box>
</CardWrap>
);
}
+4 -3
View File
@@ -1,6 +1,7 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { TaskCard as TaskCardData, TaskStep } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
@@ -34,7 +35,7 @@ const TASK_GLYPH: Record<TaskCardData["status"], string> = {
export function TaskCard({ card }: { card: TaskCardData }): React.ReactElement {
const elapsed = `${(card.elapsedMs / 1000).toFixed(1)}s`;
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={TASK_COLOR[card.status]}>
<CardHeader
glyph={TASK_GLYPH[card.status]}
tone={TASK_COLOR[card.status]}
@@ -43,7 +44,7 @@ export function TaskCard({ card }: { card: TaskCardData }): React.ReactElement {
meta={[elapsed, card.status]}
/>
{card.steps.map((step) => (
<Box key={step.id} paddingLeft={2} flexDirection="row" gap={1}>
<Box key={step.id} flexDirection="row" gap={1}>
<Text color={STEP_COLOR[step.status]}>{STEP_GLYPH[step.status]}</Text>
<Text bold color={FG.body}>
{(step.toolName ?? "step").padEnd(7)}
@@ -55,6 +56,6 @@ export function TaskCard({ card }: { card: TaskCardData }): React.ReactElement {
) : null}
</Box>
))}
</Box>
</Card>
);
}
+14 -14
View File
@@ -2,6 +2,7 @@ import { Box, Text, useStdout } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { clipToCells } from "../../../frame/width.js";
import { Card } from "../primitives/Card.js";
import { CardHeader, type MetaItem } from "../primitives/CardHeader.js";
import { Spinner } from "../primitives/Spinner.js";
import type { ToolCard as ToolCardData } from "../state/cards.js";
@@ -9,7 +10,6 @@ import { FG, TONE } from "../theme/tokens.js";
const READ_TAIL = 2;
const OTHER_TAIL = 5;
const BODY_PAD = 2;
/** Read-style tools dump file/list bodies — short tail is enough; the model already has the full text in context. */
function tailLinesFor(name: string): number {
@@ -23,7 +23,7 @@ function tailLinesFor(name: string): number {
export function ToolCard({ card }: { card: ToolCardData }): React.ReactElement {
const { stdout } = useStdout();
const cols = stdout?.columns ?? 80;
const lineCells = Math.max(20, cols - BODY_PAD - 4);
const lineCells = Math.max(20, cols - 4);
const argsLabel = formatArgsSummary(card.args);
const allLines = card.output.length > 0 ? card.output.split("\n") : [];
const tail = tailLinesFor(card.name);
@@ -47,7 +47,7 @@ export function ToolCard({ card }: { card: ToolCardData }): React.ReactElement {
for (const part of metaTrail(card)) meta.push(part);
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={headColor}>
<CardHeader
glyph={statusGlyph(status)}
tone={headColor}
@@ -61,22 +61,22 @@ export function ToolCard({ card }: { card: ToolCardData }): React.ReactElement {
{showBody && (
<>
{hidden > 0 ? (
<Box paddingLeft={BODY_PAD}>
<Text color={FG.faint}>
{`⋮ ${hidden} earlier line${hidden === 1 ? "" : "s"} (use /tool to read full)`}
</Text>
</Box>
<Text color={FG.faint}>
{`⋮ ${hidden} earlier line${hidden === 1 ? "" : "s"} (use /tool to read full)`}
</Text>
) : null}
{visible.map((line, i) => (
<Box key={`${card.id}:${hidden + i}`} paddingLeft={BODY_PAD}>
<Text color={errColor} dimColor={!card.exitCode || card.exitCode === 0}>
{clipToCells(line, lineCells) || " "}
</Text>
</Box>
<Text
key={`${card.id}:${hidden + i}`}
color={errColor}
dimColor={!card.exitCode || card.exitCode === 0}
>
{clipToCells(line, lineCells) || " "}
</Text>
))}
</>
)}
</Box>
</Card>
);
}
+8 -7
View File
@@ -1,6 +1,7 @@
import { Box, Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { UsageCard as UsageCardData } from "../state/cards.js";
import { FG, TONE, formatCNY } from "../theme/tokens.js";
@@ -34,9 +35,9 @@ export function UsageCard({ card }: { card: UsageCardData }): React.ReactElement
const headerMeta: string[] = [`turn ${card.turn}`, formatCNY(card.cost)];
if (card.elapsedMs !== undefined) headerMeta.push(`${(card.elapsedMs / 1000).toFixed(1)}s`);
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={FG.meta}>
<CardHeader glyph="Σ" tone={FG.meta} title="usage" meta={headerMeta} />
<Box paddingLeft={2} flexDirection="row" gap={1}>
<Box flexDirection="row" gap={1}>
<Text color={FG.sub}>prompt</Text>
{bar(promptRatio, TONE.brand)}
<Text bold color={FG.body}>
@@ -44,26 +45,26 @@ export function UsageCard({ card }: { card: UsageCardData }): React.ReactElement
</Text>
<Text color={FG.faint}>{`/ 1M · ${(promptRatio * 100).toFixed(1)}%`}</Text>
</Box>
<Box paddingLeft={2} flexDirection="row" gap={1}>
<Box flexDirection="row" gap={1}>
<Text color={FG.sub}>reason</Text>
{bar(reasonRatio, TONE.accent)}
<Text bold color={FG.body}>
{card.tokens.reason.toLocaleString()}
</Text>
</Box>
<Box paddingLeft={2} flexDirection="row" gap={1}>
<Box flexDirection="row" gap={1}>
<Text color={FG.sub}>output</Text>
{bar(outputRatio, TONE.brand)}
<Text bold color={FG.body}>
{card.tokens.output.toLocaleString()}
</Text>
</Box>
<Box paddingLeft={2} flexDirection="row" gap={1}>
<Box flexDirection="row" gap={1}>
<Text color={FG.sub}>cache </Text>
{bar(card.cacheHit, TONE.ok)}
<Text bold color={TONE.ok}>{`${(card.cacheHit * 100).toFixed(1)}%`}</Text>
</Box>
<Box paddingLeft={2} flexDirection="row" gap={1}>
<Box flexDirection="row" gap={1}>
<Text color={FG.faint}>session</Text>
<Text bold color={FG.body}>{`⛁ ${formatCNY(card.sessionCost, 3)}`}</Text>
{card.balance !== undefined ? (
@@ -73,7 +74,7 @@ export function UsageCard({ card }: { card: UsageCardData }): React.ReactElement
</>
) : null}
</Box>
</Box>
</Card>
);
}
+4 -6
View File
@@ -1,7 +1,7 @@
import { Box } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Markdown } from "../markdown.js";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { UserCard as UserCardData } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
@@ -9,7 +9,7 @@ import { formatRelativeTime } from "./time.js";
export function UserCard({ card }: { card: UserCardData }): React.ReactElement {
return (
<Box flexDirection="column" marginTop={1}>
<Card tone={TONE.accent}>
<CardHeader
glyph="›"
tone={TONE.accent}
@@ -17,9 +17,7 @@ export function UserCard({ card }: { card: UserCardData }): React.ReactElement {
titleColor={FG.sub}
meta={[formatRelativeTime(card.ts)]}
/>
<Box paddingLeft={2} flexDirection="column">
<Markdown text={card.text} />
</Box>
</Box>
<Markdown text={card.text} />
</Card>
);
}
+14 -13
View File
@@ -1,25 +1,26 @@
import { Box, Text } from "ink";
import { Text } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
import { Card } from "../primitives/Card.js";
import { CardHeader } from "../primitives/CardHeader.js";
import type { WarnCard as WarnCardData } from "../state/cards.js";
import { FG, TONE } from "../theme/tokens.js";
export function WarnCard({ card }: { card: WarnCardData }): React.ReactElement {
const messageLines = card.message.length > 0 ? card.message.split("\n") : [];
return (
<Box flexDirection="column" marginTop={1}>
<Box flexDirection="row" gap={1}>
<Text color={TONE.warn}>⚠</Text>
<Text color={TONE.warn} bold>
{card.title}
</Text>
{card.detail ? <Text color={FG.faint}>{`· ${card.detail}`}</Text> : null}
</Box>
<Card tone={TONE.warn}>
<CardHeader
glyph="⚠"
tone={TONE.warn}
title={card.title}
meta={card.detail ? [card.detail] : undefined}
/>
{messageLines.map((line, i) => (
<Box key={`${card.id}:${i}`} paddingLeft={2}>
<Text color={FG.body}>{line || " "}</Text>
</Box>
<Text key={`${card.id}:${i}`} color={FG.body}>
{line || " "}
</Text>
))}
</Box>
</Card>
);
}
+16
View File
@@ -0,0 +1,16 @@
import { Box } from "ink";
// biome-ignore lint/style/useImportType: tsconfig jsx=react needs React in value scope for JSX compilation
import React from "react";
export interface CardProps {
tone: string;
children: React.ReactNode;
}
export function Card({ tone, children }: CardProps): React.ReactElement {
return (
<Box flexDirection="column" borderStyle="round" borderColor={tone} paddingX={1} marginTop={1}>
{children}
</Box>
);
}