@nomad-e/bluma-cli 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomad-e/bluma-cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "BluMa independent agent for automation and advanced software engineering.",
5
5
  "author": "Alex Fonseca",
6
6
  "license": "Apache-2.0",
@@ -34,7 +34,6 @@
34
34
  "husky": "^9.1.7",
35
35
  "ink-testing-library": "^4.0.0",
36
36
  "jest": "^30.0.5",
37
- "lodash-es": "^4.18.1",
38
37
  "nodemon": "^3.1.10",
39
38
  "supports-hyperlinks": "^4.4.0",
40
39
  "ts-node-dev": "^2.0.0",
@@ -44,10 +43,10 @@
44
43
  "type": "module",
45
44
  "main": "dist/main.js",
46
45
  "scripts": {
47
- "build": "node scripts/build.js",
48
- "build:native": "cd native && npm run build",
46
+ "build": "npx tsc --noEmit && node scripts/build.js",
47
+ "build:native": "npx tsc --noEmit && cd native && npm run build",
49
48
  "build:all": "npm run build:native && npm run build",
50
- "start": "npx tsc --noEmit && node scripts/build.js && clear && node dist/main.js",
49
+ "start": "npm run build && clear && node dist/main.js",
51
50
  "test": "jest",
52
51
  "test:watch": "jest --watch",
53
52
  "test:parallel": "node scripts/test-parallel.js",
@@ -83,6 +82,7 @@
83
82
  "jquery": "^4.0.0",
84
83
  "js-tiktoken": "^1.0.21",
85
84
  "latest-version": "^9.0.0",
85
+ "lodash-es": "^4.18.1",
86
86
  "marked": "^16.1.2",
87
87
  "openai": "^4.47.3",
88
88
  "react": "^19.2.5",
@@ -94,7 +94,8 @@
94
94
  "yoga-layout": "^3.2.1"
95
95
  },
96
96
  "files": [
97
- "dist/"
97
+ "dist/",
98
+ "assets/"
98
99
  ],
99
100
  "publishConfig": {
100
101
  "access": "public"
@@ -1,130 +0,0 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * Indicador de trabalho em curso — pulso minimalista + shimmer suave.
4
- */
5
- import { useState, useEffect, memo, useRef } from "react";
6
- import { Box, Text } from "ink";
7
- import { BLUMA_TERMINAL as T } from "./theme/blumaTerminal.js";
8
- import { TOOL_DETAIL_PREFIX } from "./constants/toolUiSymbols.js";
9
- import { formatTurnDurationMs } from "./utils/formatTurnDurationMs.js";
10
- import { getToolActionLabel } from "./utils/toolActionLabels.js";
11
- // ─── Pulse ────────────────────────────────────────────────────────────────────
12
- // Frames com "peso visual" crescente/decrescente → sensação de respiração
13
- const PULSE_FRAMES = ["·", "○", "◌", "◎", "●", "◎", "◌", "○"];
14
- const PULSE_INTERVAL_MS = 200; // Increased from 110ms to reduce screen flicker
15
- // ─── Shimmer ──────────────────────────────────────────────────────────────────
16
- // Paleta de 5 níveis de brilho (sem mudar hue, apenas luminosidade percebida)
17
- // Usamos gray scale para ficar neutro com qualquer tema
18
- const SHIMMER_PALETTE = [
19
- "#4a4a4a", // escuro
20
- "#6e6e6e",
21
- "#9a9a9a", // mid
22
- "#c8c8c8",
23
- "#f0f0f0", // quase branco
24
- "#ffffff", // pico de brilho (branco puro)
25
- "#f0f0f0",
26
- "#c8c8c8",
27
- "#9a9a9a",
28
- "#6e6e6e",
29
- ];
30
- const SHIMMER_CYCLE_MS = 2000; // Increased from 1000ms to reduce flicker (slower shimmer)
31
- /** Shimmer com uma única sombra que percorre o texto da esquerda para a direita.
32
- * Cada passada (início → fim) demora SHIMMER_CYCLE_MS (1s).
33
- * Quando chega ao fim, volta instantaneamente ao início e recomeça.
34
- * Usa ref para animação fluída sem re-render quando o texto muda. */
35
- const ShimmerText = memo(({ text, baseColor = "#9a9a9a" }) => {
36
- const posRef = useRef(0);
37
- const [, forceUpdate] = useState({});
38
- const textRef = useRef(text);
39
- const intervalMsRef = useRef(50); // valor padrão inicial
40
- // Atualiza ref do texto sem afetar a animação
41
- useEffect(() => {
42
- textRef.current = text;
43
- // Recalcula o intervalo para que a passada (início → fim) demore SHIMMER_CYCLE_MS
44
- const cycleSteps = Math.max(1, text.length);
45
- intervalMsRef.current = SHIMMER_CYCLE_MS / cycleSteps;
46
- }, [text]);
47
- useEffect(() => {
48
- const rafId = setInterval(() => {
49
- const len = textRef.current.length;
50
- // Posição vai de 0 a len-1 (apenas ida, esquerda → direita)
51
- // Quando chega ao fim, volta ao início (0)
52
- posRef.current = (posRef.current + 1) % (len || 1);
53
- forceUpdate({}); // Força re-render mínimo apenas para atualizar cores
54
- }, intervalMsRef.current);
55
- return () => clearInterval(rafId);
56
- }, []);
57
- const currentText = textRef.current;
58
- const currentPos = posRef.current;
59
- // Calcula a cor baseada na distância do ponto de brilho
60
- // O caractere na currentPos tem o brilho máximo (índice 5 = branco)
61
- // Os adjacentes têm brilho decrescente
62
- const getColorForIndex = (i) => {
63
- const dist = Math.abs(i - currentPos);
64
- if (dist === 0)
65
- return SHIMMER_PALETTE[5]; // branco puro no pico
66
- if (dist === 1)
67
- return SHIMMER_PALETTE[4]; // quase branco
68
- if (dist === 2)
69
- return SHIMMER_PALETTE[3]; // claro
70
- if (dist === 3)
71
- return SHIMMER_PALETTE[2]; // mid
72
- return baseColor; // cor base para o resto
73
- };
74
- return (_jsx(_Fragment, { children: currentText.split("").map((char, i) => (_jsx(Text, { color: getColorForIndex(i), children: char }, i))) }));
75
- });
76
- // ─── Helpers ──────────────────────────────────────────────────────────────────
77
- function trimLine(s, max = 76) {
78
- const t = String(s ?? "")
79
- .replace(/\s+/g, " ")
80
- .trim();
81
- if (!t)
82
- return "";
83
- return t.length <= max ? t : `${t.slice(0, max - 1)}…`;
84
- }
85
- // ─── WorkingTimer ─────────────────────────────────────────────────────────────
86
- const WorkingTimerComponent = ({ eventBus, taskName, taskStatus, liveToolName, liveToolArgs, isReasoning, startedAtMs, }) => {
87
- const [currentAction, setCurrentAction] = useState("working");
88
- const [pulseFrame, setPulseFrame] = useState(0);
89
- const [nowTick, setNowTick] = useState(() => Date.now());
90
- const dynamicActionLabel = liveToolName
91
- ? getToolActionLabel(liveToolName, liveToolArgs)
92
- : null;
93
- useEffect(() => {
94
- if (!eventBus)
95
- return;
96
- const handleActionStatus = (data) => {
97
- if (data.action)
98
- setCurrentAction(data.action);
99
- };
100
- eventBus.on("action_status", handleActionStatus);
101
- return () => { eventBus.off("action_status", handleActionStatus); };
102
- }, [eventBus]);
103
- // Pulse — frame rate próprio, independente do shimmer
104
- useEffect(() => {
105
- const id = setInterval(() => {
106
- setPulseFrame((prev) => (prev + 1) % PULSE_FRAMES.length);
107
- }, PULSE_INTERVAL_MS);
108
- return () => clearInterval(id);
109
- }, []);
110
- // Elapsed timer
111
- useEffect(() => {
112
- if (startedAtMs == null)
113
- return;
114
- setNowTick(Date.now());
115
- const id = setInterval(() => setNowTick(Date.now()), 500);
116
- return () => clearInterval(id);
117
- }, [startedAtMs]);
118
- const displayAction = dynamicActionLabel ||
119
- (taskStatus || (isReasoning ? "thinking" : currentAction)).trim() ||
120
- "working";
121
- const actionLine = `${displayAction}…`;
122
- const elapsedMs = startedAtMs != null ? Math.max(0, nowTick - startedAtMs) : 0;
123
- const elapsedLabel = startedAtMs != null ? formatTurnDurationMs(elapsedMs) : null;
124
- // Intensidade do pulse: frame central (●) = full, bordas = dim
125
- const pulseSymbol = PULSE_FRAMES[pulseFrame];
126
- const isPulsePeak = pulseFrame === 4; // "●"
127
- const isPulseDim = pulseFrame === 0; // "·"
128
- return (_jsxs(Box, { flexDirection: "column", marginTop: 0.5, children: [_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsx(Text, { color: T.onSurfaceVariant, dimColor: isPulseDim, bold: isPulsePeak, children: pulseSymbol }), _jsx(Text, { children: " " }), _jsx(ShimmerText, { text: actionLine, baseColor: T.onSurfaceVariant ?? "#6e6e6e" }), elapsedLabel ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: T.m3OnSurface, dimColor: true, children: " \u00B7 " }), _jsx(Text, { color: T.m3OnSurface, dimColor: true, children: elapsedLabel })] })) : null] }), taskName ? (_jsx(Box, { paddingLeft: TOOL_DETAIL_PREFIX.length, children: _jsx(Text, { color: T.m3OnSurface, dimColor: true, wrap: "truncate", children: trimLine(taskName) }) })) : null] }));
129
- };
130
- export const WorkingTimer = memo(WorkingTimerComponent);
@@ -1,113 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useEffect, useRef, memo } from "react";
3
- import { Box, Text } from "ink";
4
- import { ChatBlock, MessageResponse } from "../theme/m3Layout.js";
5
- import { BLUMA_TERMINAL as T } from "../theme/blumaTerminal.js";
6
- import { applyStreamEndFlush } from "./streamingTextFlush.js";
7
- import { collapseRepeatedReasoningLines } from "./reasoningText.js";
8
- const THROTTLE_MS = 500; // Increased from 300ms to reduce Ink re-render flicker
9
- const MAX_VISIBLE_LINES = 20;
10
- const StreamingTextComponent = ({ eventBus, onReasoningComplete, onAssistantContentComplete, }) => {
11
- const [reasoning, setReasoning] = useState("");
12
- const [assistantContent, setAssistantContent] = useState("");
13
- const [isStreaming, setIsStreaming] = useState(false);
14
- const reasoningRef = useRef("");
15
- const contentRef = useRef("");
16
- const flushTimerRef = useRef(null);
17
- const onReasoningCompleteRef = useRef(onReasoningComplete);
18
- onReasoningCompleteRef.current = onReasoningComplete;
19
- const onAssistantContentCompleteRef = useRef(onAssistantContentComplete);
20
- onAssistantContentCompleteRef.current = onAssistantContentComplete;
21
- const streamEndHandledRef = useRef(false);
22
- useEffect(() => {
23
- const syncVisible = () => {
24
- setReasoning(reasoningRef.current);
25
- setAssistantContent(contentRef.current);
26
- };
27
- const startFlushTimer = () => {
28
- if (flushTimerRef.current)
29
- return;
30
- flushTimerRef.current = setInterval(() => {
31
- setReasoning(reasoningRef.current);
32
- setAssistantContent(contentRef.current);
33
- }, THROTTLE_MS);
34
- };
35
- const handleStart = () => {
36
- streamEndHandledRef.current = false;
37
- reasoningRef.current = "";
38
- contentRef.current = "";
39
- setReasoning("");
40
- setAssistantContent("");
41
- setIsStreaming(true);
42
- startFlushTimer();
43
- };
44
- const handleReasoningChunk = (data) => {
45
- if (data.delta) {
46
- reasoningRef.current += data.delta;
47
- }
48
- };
49
- const handleContentChunk = (data) => {
50
- if (data.delta) {
51
- contentRef.current += data.delta;
52
- }
53
- };
54
- const handleEnd = (payload) => {
55
- if (streamEndHandledRef.current)
56
- return;
57
- streamEndHandledRef.current = true;
58
- // Clear timers first
59
- if (flushTimerRef.current) {
60
- clearInterval(flushTimerRef.current);
61
- flushTimerRef.current = null;
62
- }
63
- // IMMEDIATELY clear the streaming UI before adding to history
64
- // This prevents the reasoning from appearing twice
65
- const finalReasoning = reasoningRef.current;
66
- const finalContent = contentRef.current;
67
- setReasoning("");
68
- setAssistantContent("");
69
- reasoningRef.current = "";
70
- contentRef.current = "";
71
- setIsStreaming(false);
72
- // NOW add to history (after UI is cleared)
73
- applyStreamEndFlush({
74
- finalReasoning,
75
- finalContent,
76
- payload,
77
- onReasoningComplete: onReasoningCompleteRef.current,
78
- onAssistantContentComplete: onAssistantContentCompleteRef.current,
79
- });
80
- };
81
- eventBus.on("stream_start", handleStart);
82
- eventBus.on("stream_reasoning_chunk", handleReasoningChunk);
83
- eventBus.on("stream_chunk", handleContentChunk);
84
- eventBus.on("stream_end", handleEnd);
85
- return () => {
86
- eventBus.off("stream_start", handleStart);
87
- eventBus.off("stream_reasoning_chunk", handleReasoningChunk);
88
- eventBus.off("stream_chunk", handleContentChunk);
89
- eventBus.off("stream_end", handleEnd);
90
- if (flushTimerRef.current) {
91
- clearInterval(flushTimerRef.current);
92
- flushTimerRef.current = null;
93
- }
94
- };
95
- }, [eventBus]);
96
- if (!isStreaming || (!reasoning && !assistantContent)) {
97
- return null;
98
- }
99
- const renderLines = (text, dim) => {
100
- const normalized = collapseRepeatedReasoningLines(text);
101
- const lines = normalized.split("\n");
102
- let displayLines = lines;
103
- let truncatedCount = 0;
104
- if (lines.length > MAX_VISIBLE_LINES) {
105
- truncatedCount = lines.length - MAX_VISIBLE_LINES;
106
- displayLines = lines.slice(-MAX_VISIBLE_LINES);
107
- }
108
- return (_jsxs(Box, { flexDirection: "column", children: [truncatedCount > 0 ? (_jsxs(Text, { dimColor: true, children: ["\u2026 ", truncatedCount, " lines above hidden"] })) : null, displayLines.map((line, i) => (_jsx(Text, { dimColor: dim, color: dim ? undefined : T.m3OnSurface, children: line }, i)))] }));
109
- };
110
- return (_jsxs(ChatBlock, { marginBottom: 0, children: [reasoning ? (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: renderLines(reasoning, true) })) : null, assistantContent ? (_jsx(MessageResponse, { children: renderLines(assistantContent, false) })) : null] }));
111
- };
112
- export const StreamingText = memo(StreamingTextComponent);
113
- export default StreamingText;
@@ -1,18 +0,0 @@
1
- export function collapseRepeatedReasoningLines(text) {
2
- const raw = String(text ?? "").replace(/\r\n/g, "\n");
3
- if (!raw.trim())
4
- return "";
5
- const lines = raw.split("\n");
6
- const collapsed = [];
7
- for (const line of lines) {
8
- const current = line.trimEnd();
9
- if (current === "" && collapsed[collapsed.length - 1] === "") {
10
- continue;
11
- }
12
- if (collapsed.length > 0 && collapsed[collapsed.length - 1] === current) {
13
- continue;
14
- }
15
- collapsed.push(current);
16
- }
17
- return collapsed.join("\n").trim();
18
- }
@@ -1,11 +0,0 @@
1
- export function applyStreamEndFlush(params) {
2
- const { finalReasoning, finalContent, payload, onReasoningComplete, onAssistantContentComplete, } = params;
3
- if (finalReasoning && onReasoningComplete) {
4
- onReasoningComplete(finalReasoning);
5
- }
6
- const trimmed = finalContent.trim();
7
- const skipAssistant = payload?.omitAssistantFlush === true;
8
- if (trimmed && onAssistantContentComplete && !skipAssistant) {
9
- onAssistantContentComplete(finalContent);
10
- }
11
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Marcador de invocação de tool — bullet compacto (U+2022 •).
3
- */
4
- export const TOOL_INVOCATION_MARK = "\u2022 ";
5
- /** Indentação do detalhe da tool. */
6
- export const TOOL_DETAIL_PREFIX = " ";
7
- /**
8
- * Prefixo da linha de resultado sob a invocação (• List / path) — ramo `└` em dim, estilo BluMa.
9
- */
10
- export const RESULT_LINE_GUTTER = " \u2514 ";
@@ -1,79 +0,0 @@
1
- /**
2
- * Paleta BluMa — magenta #FF70FE, ciano #53B2D2, laranja (#d77757).
3
- * Chaves semânticas (`accent`, `suggestion`, …) mantêm compat com o resto da UI.
4
- */
5
- export const BLUMA_TERMINAL = {
6
- /** Tríade principal (marca) */
7
- magenta: "#FF70FE",
8
- blue: "#53B2D2",
9
- /** Laranja (darkTheme) */
10
- orange: "#d77757",
11
- accent: "#d77757",
12
- accentShimmer: "#eb9f7f",
13
- /** UI estrutural, links, secções — ciano */
14
- permission: "#53B2D2",
15
- permissionShimmer: "#7ecce3",
16
- promptBorder: "#53B2D2",
17
- promptBorderShimmer: "#8fd4e8",
18
- suggestion: "#53B2D2",
19
- panelBorder: "#53B2D2",
20
- link: "#53B2D2",
21
- codeLabel: "#53B2D2",
22
- heading1: "#53B2D2",
23
- heading2: "#53B2D2",
24
- m3Outline: "#53B2D2",
25
- m3Rail: "#53B2D2",
26
- /** Ferramentas, bullets fortes — magenta */
27
- toolLabel: "#FF70FE",
28
- headingDeep: "#FF70FE",
29
- listBullet: "#FF70FE",
30
- listBulletSub: "#53B2D2",
31
- m3TonalOutline: "#FF70FE",
32
- merged: "#d896ff",
33
- subtle: "#505050",
34
- inactive: "#999999",
35
- text: "#ffffff",
36
- success: "#4eba65",
37
- err: "#ff6b80",
38
- warn: "#ffc107",
39
- diffAdded: "#225a2b",
40
- diffRemoved: "#7a2936",
41
- diffAddedWord: "#38a660",
42
- diffRemovedWord: "#b3596b",
43
- brandBlue: "#53B2D2",
44
- brandMagenta: "#FF70FE",
45
- /**
46
- * Rampa neutra para shimmer no texto “a trabalhar”.
47
- * O texto base fica branco e o brilho entra em cinzas suaves.
48
- */
49
- workingShimmerRamp: [
50
- "#6b6b6b", // cinza base
51
- "#7b7b7b", // cinza base+
52
- "#8f8f8f", // cinza médio
53
- "#a8a8a8", // cinza claro
54
- "#e6e6e6", // brilho máximo
55
- "#a8a8a8", // cinza claro
56
- "#8f8f8f", // cinza médio
57
- "#7b7b7b", // cinza base+
58
- ],
59
- muted: "#999999",
60
- dim: "#999999",
61
- code: "#999999",
62
- linkUnderline: true,
63
- toolMeta: "#999999",
64
- rule: "#505050",
65
- m3Label: "#999999",
66
- m3OnSurface: "#ffffff",
67
- // Material Design 3 surface colors
68
- surface: "#1E1E1E",
69
- surfaceVariant: "#2D2D2D",
70
- surfaceContainer: "#333333",
71
- onSurfaceVariant: "#B0B0B0",
72
- outline: "#909090",
73
- outlineVariant: "#707070",
74
- // Cores semânticas adicionais para compatibilidade
75
- primary: "#7C4DFF",
76
- primaryVariant: "#6C3DCC",
77
- onPrimary: "#FFFFFF",
78
- warning: "#ffc107",
79
- };
@@ -1,68 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * Layout chat — MessageResponse: indentação suave (RESULT_LINE_GUTTER) + conteúdo.
4
- * Contexto evita repetir o gutter quando há MessageResponse aninhados.
5
- */
6
- import { createContext, useContext } from "react";
7
- import { Box, Text } from "ink";
8
- import { BLUMA_TERMINAL as T } from "./blumaTerminal.js";
9
- import { RESULT_LINE_GUTTER } from "../constants/toolUiSymbols.js";
10
- import { formatTurnDurationMs } from "../utils/formatTurnDurationMs.js";
11
- export { formatTurnDurationMs };
12
- const MessageResponseNestedContext = createContext(false);
13
- export function MessageResponse({ children, height, }) {
14
- const nested = useContext(MessageResponseNestedContext);
15
- if (nested) {
16
- return _jsx(_Fragment, { children: children });
17
- }
18
- return (_jsx(MessageResponseNestedContext.Provider, { value: true, children: _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", height: height, overflow: height !== undefined ? "hidden" : undefined, children: [_jsx(Text, { dimColor: true, children: RESULT_LINE_GUTTER }), _jsx(Box, { flexShrink: 1, flexGrow: 1, flexDirection: "column", children: children })] }) }));
19
- }
20
- export function ChatBlock({ children, marginBottom = 1, }) {
21
- return (_jsx(Box, { flexDirection: "column", marginBottom: marginBottom, children: children }));
22
- }
23
- export function ChatUserImageBlock({ imageCount, caption, captionDim = false, }) {
24
- if (imageCount < 1)
25
- return null;
26
- const cap = caption?.trim() ?? "";
27
- const capLines = cap.length > 0 ? cap.split("\n") : [];
28
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [Array.from({ length: imageCount }, (_, i) => (_jsx(Text, { bold: true, color: T.orange, children: `[IMAGE #${i + 1}]` }, `img-${i}`))), capLines.map((line, i) => (_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [_jsx(Text, { dimColor: true, children: i === 0 ? "└─ " : " " }), captionDim ? (_jsx(Text, { dimColor: true, bold: true, wrap: "wrap", children: line })) : (_jsx(Text, { color: T.m3OnSurface, bold: true, wrap: "wrap", children: line }))] }, `cap-${i}`)))] }));
29
- }
30
- /** @deprecated usar ChatUserImageBlock */
31
- export function ChatUserImageTree({ count }) {
32
- return _jsx(ChatUserImageBlock, { imageCount: count });
33
- }
34
- /** Mensagem do utilizador: moldura só topo/fundo (como o InputPrompt), sem `>`. */
35
- export function ChatUserMessage({ children }) {
36
- return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: T.m3OnSurface, borderLeft: false, borderRight: false, paddingX: 1, children: children }) }));
37
- }
38
- export function ChatMeta({ children }) {
39
- return (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: children }) }));
40
- }
41
- export function ChatStatusRow({ children }) {
42
- return (_jsxs(Box, { flexDirection: "row", marginBottom: 1, flexWrap: "wrap", children: [_jsxs(Text, { color: T.orange, bold: true, children: ["*", " "] }), children] }));
43
- }
44
- export function ChatTurnDuration({ durationMs }) {
45
- return (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: T.blue, children: "\u00B7 " }), _jsx(Text, { color: T.magenta, children: formatTurnDurationMs(durationMs) })] }) }));
46
- }
47
- export function Divider({ width, title, color, }) {
48
- const w = width ?? 60;
49
- const char = "─";
50
- if (title) {
51
- const titleWidth = title.length + 2;
52
- const sideWidth = Math.max(0, w - titleWidth);
53
- const leftWidth = Math.floor(sideWidth / 2);
54
- const rightWidth = sideWidth - leftWidth;
55
- return (_jsxs(Text, { color: color, dimColor: !color, children: [char.repeat(leftWidth), " ", _jsx(Text, { dimColor: true, children: title }), " ", char.repeat(rightWidth)] }));
56
- }
57
- return _jsx(Text, { color: color, dimColor: !color, children: char.repeat(w) });
58
- }
59
- export function M3Surface({ children, marginBottom = 1, }) {
60
- return _jsx(ChatBlock, { marginBottom: marginBottom, children: children });
61
- }
62
- export const M3UserBubble = ChatUserMessage;
63
- export const M3SystemRow = ChatMeta;
64
- export const M3InputDock = ({ children }) => (_jsx(Box, { flexDirection: "column", marginTop: 1, children: children }));
65
- export const M3StatusStrip = ChatStatusRow;
66
- export function TerminalRule({ width = 48 }) {
67
- return _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(8, width)) });
68
- }
@@ -1,16 +0,0 @@
1
- /** E.g. 211s → `3min · 31s`; under 60s → `45s` or `9.3s` if under 10s. */
2
- export function formatTurnDurationMs(ms) {
3
- if (ms < 0) {
4
- return "0s";
5
- }
6
- const secTotal = Math.round(ms / 1000);
7
- if (secTotal < 60) {
8
- if (ms < 10_000) {
9
- return `${(ms / 1000).toFixed(1)}s`;
10
- }
11
- return `${secTotal}s`;
12
- }
13
- const min = Math.floor(secTotal / 60);
14
- const sec = secTotal % 60;
15
- return `${min}min · ${sec}s`;
16
- }