chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { Box, Text } from "ink";
|
||||
|
||||
export function CodeBlock({ code = "", language }) {
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="single" borderColor="gray" paddingX={1}>
|
||||
{language && (
|
||||
<Text dimColor bold>
|
||||
{language}
|
||||
</Text>
|
||||
)}
|
||||
<Text>{code}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
|
||||
export function ConfirmDialog({ message, onConfirm, onCancel }) {
|
||||
const [selected, setSelected] = useState(1); // 0=yes, 1=no (default no)
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.leftArrow || input === "y") setSelected(0);
|
||||
if (key.rightArrow || input === "n") setSelected(1);
|
||||
if (key.return) {
|
||||
if (selected === 0) onConfirm?.();
|
||||
else onCancel?.();
|
||||
}
|
||||
if (key.escape) onCancel?.();
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="round" borderColor="yellow" padding={1}>
|
||||
<Text bold color="yellow">
|
||||
{message}
|
||||
</Text>
|
||||
<Box marginTop={1} gap={2}>
|
||||
<Text
|
||||
bold={selected === 0}
|
||||
color={selected === 0 ? "green" : undefined}
|
||||
inverse={selected === 0}
|
||||
>
|
||||
Yes
|
||||
</Text>
|
||||
<Text
|
||||
bold={selected === 1}
|
||||
color={selected === 1 ? "red" : undefined}
|
||||
inverse={selected === 1}
|
||||
>
|
||||
No
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import { theme } from "./theme.jsx";
|
||||
|
||||
function formatCell(v, col) {
|
||||
if (v == null) return "-";
|
||||
if (col.formatter) return col.formatter(v);
|
||||
return String(v);
|
||||
}
|
||||
|
||||
export function DataTable({ rows = [], schema = [], selectable = false, onSelect }) {
|
||||
const [selectedIdx, setSelectedIdx] = useState(0);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (!selectable || rows.length === 0) return;
|
||||
if (key.upArrow) setSelectedIdx((i) => Math.max(0, i - 1));
|
||||
if (key.downArrow) setSelectedIdx((i) => Math.min(rows.length - 1, i + 1));
|
||||
if (key.return && onSelect) onSelect(rows[selectedIdx]);
|
||||
});
|
||||
|
||||
if (rows.length === 0) {
|
||||
return <Text dimColor>No data.</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
{schema.map((col) => (
|
||||
<Box key={col.key} width={col.width ?? 16} marginRight={1}>
|
||||
<Text bold color={theme.header}>
|
||||
{col.header}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{rows.map((row, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
backgroundColor={selectable && idx === selectedIdx ? theme.selected : undefined}
|
||||
>
|
||||
{schema.map((col) => (
|
||||
<Box key={col.key} width={col.width ?? 16} marginRight={1}>
|
||||
<Text>{formatCell(row[col.key], col)}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Text } from "ink";
|
||||
|
||||
export function HeaderSwr({ fetcher, interval = 5000, render, initial = null }) {
|
||||
const [data, setData] = useState(initial);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function tick() {
|
||||
try {
|
||||
const next = await fetcher();
|
||||
if (!cancelled) setData(next);
|
||||
} catch {}
|
||||
}
|
||||
tick();
|
||||
const id = setInterval(tick, interval);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(id);
|
||||
};
|
||||
}, [fetcher, interval]);
|
||||
|
||||
if (!data) return <Text dimColor>Loading…</Text>;
|
||||
return render(data);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import { Text } from "ink";
|
||||
|
||||
export function KeyMaskedDisplay({ apiKey, revealed = false }) {
|
||||
if (!apiKey) return <Text dimColor>(none)</Text>;
|
||||
const display = revealed
|
||||
? apiKey
|
||||
: apiKey.length <= 8
|
||||
? "***"
|
||||
: `${apiKey.slice(0, 6)}***${apiKey.slice(-4)}`;
|
||||
return <Text>{display}</Text>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { Text } from "ink";
|
||||
|
||||
export function MarkdownView({ content = "" }) {
|
||||
// Render markdown as plain text with basic bold/italic stripping.
|
||||
// For rich rendering, use marked-terminal externally before passing content.
|
||||
const clean = content
|
||||
.replace(/\*\*(.+?)\*\*/g, "$1")
|
||||
.replace(/\*(.+?)\*/g, "$1")
|
||||
.replace(/`(.+?)`/g, "$1")
|
||||
.replace(/^#+\s+/gm, "");
|
||||
return <Text>{clean}</Text>;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Text, useInput } from "ink";
|
||||
|
||||
export function MenuSelect({ items = [], onSelect, initial = 0 }) {
|
||||
const [idx, setIdx] = useState(Math.min(initial, Math.max(0, items.length - 1)));
|
||||
|
||||
useInput((input, key) => {
|
||||
if (items.length === 0) return;
|
||||
if (key.upArrow) setIdx((i) => (i - 1 + items.length) % items.length);
|
||||
if (key.downArrow) setIdx((i) => (i + 1) % items.length);
|
||||
if (key.return && onSelect) onSelect(items[idx]);
|
||||
const n = parseInt(input, 10);
|
||||
if (!isNaN(n) && n >= 1 && n <= items.length) {
|
||||
const newIdx = n - 1;
|
||||
setIdx(newIdx);
|
||||
if (onSelect) onSelect(items[newIdx]);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{items.map((item, i) => (
|
||||
<Box key={i}>
|
||||
<Text bold={i === idx} color={i === idx ? "yellow" : undefined} dimColor={item.disabled}>
|
||||
{i === idx ? "▶ " : " "}
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.hint && (
|
||||
<Text dimColor>
|
||||
{" "}
|
||||
{item.hint}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { Box, Text } from "ink";
|
||||
import TextInput from "ink-text-input";
|
||||
|
||||
export function MultilineInput({ label, value, onChange, placeholder }) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{label && (
|
||||
<Text bold dimColor>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
<Box borderStyle="single" borderColor="gray" paddingX={1}>
|
||||
<TextInput value={value} onChange={onChange} placeholder={placeholder} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { Box, Text } from "ink";
|
||||
|
||||
export function ProgressBar({ value = 0, max = 100, width = 20, color = "green" }) {
|
||||
const pct = Math.min(1, Math.max(0, value / max));
|
||||
const filled = Math.round(pct * width);
|
||||
const empty = width - filled;
|
||||
return (
|
||||
<Box>
|
||||
<Text color={color}>{"█".repeat(filled)}</Text>
|
||||
<Text dimColor>{"░".repeat(empty)}</Text>
|
||||
<Text> {Math.round(pct * 100)}%</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { Text } from "ink";
|
||||
|
||||
const BARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
||||
|
||||
export function Sparkline({ data = [], color = "cyan", width = 10 }) {
|
||||
const slice = data.slice(-width);
|
||||
if (slice.length === 0) return <Text dimColor>{"─".repeat(width)}</Text>;
|
||||
const max = Math.max(...slice, 1);
|
||||
const chars = slice.map((v) => {
|
||||
const idx = Math.round((v / max) * (BARS.length - 1));
|
||||
return BARS[Math.min(Math.max(idx, 0), BARS.length - 1)];
|
||||
});
|
||||
return <Text color={color}>{chars.join("")}</Text>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { Text } from "ink";
|
||||
|
||||
const STATUS_MAP = {
|
||||
running: { label: "● running", color: "green" },
|
||||
stopped: { label: "● stopped", color: "red" },
|
||||
starting: { label: "◌ starting", color: "yellow" },
|
||||
error: { label: "✗ error", color: "red" },
|
||||
unknown: { label: "? unknown", color: "gray" },
|
||||
ok: { label: "✓ ok", color: "green" },
|
||||
warn: { label: "⚠ warn", color: "yellow" },
|
||||
};
|
||||
|
||||
export function StatusBadge({ status = "unknown" }) {
|
||||
const s = STATUS_MAP[status] ?? { label: status, color: "gray" };
|
||||
return <Text color={s.color}>{s.label}</Text>;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import { Box, Text } from "ink";
|
||||
|
||||
export function TokenCounter({ tokensIn = 0, tokensOut = 0, costUsd = 0, model }) {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Text bold>In: </Text>
|
||||
<Text>{tokensIn.toLocaleString()}</Text>
|
||||
<Text bold> Out: </Text>
|
||||
<Text>{tokensOut.toLocaleString()}</Text>
|
||||
<Text bold> Cost: </Text>
|
||||
<Text color="yellow">${costUsd.toFixed(4)}</Text>
|
||||
</Box>
|
||||
{model && <Text dimColor>Model: {model}</Text>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const theme = {
|
||||
primary: "cyan",
|
||||
secondary: "yellow",
|
||||
success: "green",
|
||||
error: "red",
|
||||
warning: "yellow",
|
||||
muted: "gray",
|
||||
header: "cyan",
|
||||
selected: "blue",
|
||||
border: "cyan",
|
||||
};
|
||||
Reference in New Issue
Block a user