mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
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:
1 parent
51a5f9dd8e
commit
e78112bf4f
17 files changed
+159
-183
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user