@leroylabs/cli 0.1.6 → 0.1.7

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.
Files changed (3) hide show
  1. package/bin/leroy.mjs +16 -15
  2. package/package.json +1 -6
  3. package/src/live-ui.mjs +391 -433
package/bin/leroy.mjs CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { stdin as input, stderr, stdout as output } from "node:process";
5
- import React from "react";
6
5
 
7
6
  import { parseArgs } from "../src/args.mjs";
8
7
  import { LeroyMcpClient } from "../src/client.mjs";
@@ -10,7 +9,7 @@ import { configPath, resolvedConfig, writeConfig } from "../src/config.mjs";
10
9
  import { createDemoResponse } from "../src/demo.mjs";
11
10
  import { usageError } from "../src/errors.mjs";
12
11
  import { formatEvaluation, formatHelp } from "../src/format.mjs";
13
- import { LiveEvidenceApp, render } from "../src/live-ui.mjs";
12
+ import { renderLiveEvidence } from "../src/live-ui.mjs";
14
13
  import { ansiForeground, LEROY_COLORS } from "../src/colors.mjs";
15
14
 
16
15
  const ANSI = {
@@ -45,22 +44,20 @@ function printMissingApiKeyGuidance(options) {
45
44
  printError(`${promptStyle("▲", ["green"], colorEnabled)} ${promptStyle("Connect Leroy", ["bold", "white"], colorEnabled)}`);
46
45
  printError();
47
46
  printError(` ${promptStyle("Live market evidence requires an MCP key.", ["white"], colorEnabled)}`);
48
- printError(` ${promptStyle("Create or activate one at", ["neutral"], colorEnabled)}`);
49
- printError(` ${promptStyle("https://getleroy.com/mcp", ["green"], colorEnabled)}`);
47
+ printError(` ${promptStyle("Create or activate one at", ["neutral"], colorEnabled)} ${promptStyle("https://getleroy.com/mcp", ["green"], colorEnabled)}`);
50
48
  printError();
51
49
  printError(` ${promptStyle("›", ["green"], colorEnabled)} Run ${promptStyle("leroy connect", ["bold", "white"], colorEnabled)} to continue.`);
52
50
  }
53
51
 
54
52
  async function connect(options) {
55
53
  const config = await resolvedConfig(options);
54
+ const colorEnabled = output.isTTY && !options.noColor;
56
55
  let apiKey = config.apiKey;
57
56
  if (!apiKey && input.isTTY) {
58
- const colorEnabled = output.isTTY && !options.noColor;
59
57
  print();
60
58
  print(`${promptStyle("▲", ["green"], colorEnabled)} ${promptStyle("Connect Leroy", ["bold", "white"], colorEnabled)}`);
61
59
  print();
62
- print(` ${promptStyle("Create or activate an MCP key at", ["neutral"], colorEnabled)}`);
63
- print(` ${promptStyle("https://getleroy.com/mcp", ["green"], colorEnabled)}`);
60
+ print(` ${promptStyle("Create or activate an MCP key at", ["neutral"], colorEnabled)} ${promptStyle("https://getleroy.com/mcp", ["green"], colorEnabled)}`);
64
61
  print();
65
62
  const readline = createInterface({ input, output });
66
63
  apiKey = (await readline.question(` ${promptStyle("›", ["green"], colorEnabled)} Paste your ${promptStyle("lr_live_", ["bold", "white"], colorEnabled)} key: `)).trim();
@@ -70,11 +67,16 @@ async function connect(options) {
70
67
  print(`No API key supplied. Run: leroy connect --api-key lr_live_...`);
71
68
  return 2;
72
69
  }
73
- print("Checking Leroy connection...");
70
+ print();
71
+ print(` ${promptStyle("Checking Leroy connection...", ["white"], colorEnabled)}`);
74
72
  const client = new LeroyMcpClient({ endpoint: config.endpoint, apiKey });
75
73
  await client.initialize();
76
74
  await writeConfig({ endpoint: config.endpoint, apiKey });
77
- print(`Connected. Credentials saved to ${configPath()}`);
75
+ print();
76
+ print(` ${promptStyle("Connected!", ["bold", "green"], colorEnabled)}`);
77
+ print();
78
+ print(` ${promptStyle(`Credentials saved to ${configPath()}`, ["white"], colorEnabled)}`);
79
+ print();
78
80
  return 0;
79
81
  }
80
82
 
@@ -99,13 +101,12 @@ async function evaluate(options) {
99
101
  client = new LeroyMcpClient({ endpoint: config.endpoint, apiKey: config.apiKey });
100
102
  }
101
103
  if (input.isTTY && output.isTTY && !options.json && !options.setup && !options.verbose) {
102
- const app = render(React.createElement(LiveEvidenceApp, {
104
+ await renderLiveEvidence({
103
105
  client,
104
106
  request,
105
107
  colorEnabled: !options.noColor,
106
108
  polling: false,
107
- }), { exitOnCtrlC: true });
108
- await app.waitUntilExit();
109
+ });
109
110
  return 0;
110
111
  }
111
112
  const response = await client.evaluate(request);
@@ -143,13 +144,13 @@ async function watch(options) {
143
144
  }
144
145
  client = new LeroyMcpClient({ endpoint: config.endpoint, apiKey: config.apiKey });
145
146
  }
146
- const app = render(React.createElement(LiveEvidenceApp, {
147
+ await renderLiveEvidence({
147
148
  client,
148
149
  request,
149
150
  colorEnabled: !options.noColor,
150
151
  intervalMs: options.intervalSeconds * 1_000,
151
- }), { exitOnCtrlC: true });
152
- await app.waitUntilExit();
152
+ polling: true,
153
+ });
153
154
  return 0;
154
155
  }
155
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leroylabs/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Command-line access to Leroy market evidence.",
5
5
  "homepage": "https://getleroy.com",
6
6
  "type": "module",
@@ -15,11 +15,6 @@
15
15
  "bin": {
16
16
  "leroy": "bin/leroy.mjs"
17
17
  },
18
- "dependencies": {
19
- "cli-table3": "^0.6.5",
20
- "ink": "^7.1.1",
21
- "react": "^19.2.8"
22
- },
23
18
  "engines": {
24
19
  "node": ">=22"
25
20
  }
package/src/live-ui.mjs CHANGED
@@ -1,84 +1,86 @@
1
- import React, { useEffect, useState } from "react";
2
- import Table from "cli-table3";
3
- import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
1
+ import { stdin as processInput, stdout as processOutput } from "node:process";
4
2
 
5
3
  import { ansiForeground, LEROY_COLORS } from "./colors.mjs";
6
4
  import { buildEvaluationViewModel } from "./format.mjs";
7
5
 
8
- const COLORS = {
9
- green: LEROY_COLORS.green,
10
- red: LEROY_COLORS.red,
11
- white: "white",
12
- muted: LEROY_COLORS.neutral,
13
- content: LEROY_COLORS.neutral,
14
- neutral: LEROY_COLORS.neutral,
15
- inactive: LEROY_COLORS.inactive,
16
- border: LEROY_COLORS.inactive,
17
- };
18
-
19
- const MONOCHROME = Object.fromEntries(Object.keys(COLORS).map((key) => [key, undefined]));
20
6
  const MAX_CARD_WIDTH = 67;
21
7
  const MIN_CARD_WIDTH = 64;
22
8
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
23
9
  const LOADING_STEP_DELAY_MS = 110;
24
10
  const STRATEGY_COUNT = 41;
25
- const STRATEGY_BAR_COUNT = 20;
26
- const STRATEGY_CELL_COUNT = STRATEGY_BAR_COUNT;
27
- const TABLE_STYLE = {
28
- "padding-left": 0,
29
- "padding-right": 0,
30
- head: [],
31
- border: [],
32
- };
33
- const ANSI_CODES = {
34
- white: 37,
35
- green: `38;2;0;212;27`,
36
- red: `38;2;242;86;47`,
37
- muted: 90,
38
- neutral: `38;2;143;149;145`,
39
- inactive: `38;2;41;51;43`,
11
+ const STRATEGY_CELL_COUNT = 20;
12
+
13
+ const ANSI = {
14
+ reset: "\u001b[0m",
15
+ bold: "\u001b[1m",
16
+ white: "\u001b[37m",
17
+ muted: "\u001b[90m",
18
+ green: ansiForeground(LEROY_COLORS.green),
19
+ red: ansiForeground(LEROY_COLORS.red),
20
+ neutral: ansiForeground(LEROY_COLORS.neutral),
21
+ inactive: ansiForeground(LEROY_COLORS.inactive),
22
+ yellow: "\u001b[33m",
23
+ hideCursor: "\u001b[?25l",
24
+ showCursor: "\u001b[?25h",
40
25
  };
41
26
 
42
- function tableChars() {
27
+ function record(value) {
28
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
29
+ }
30
+
31
+ function clamp(value, minimum, maximum) {
32
+ return Math.min(maximum, Math.max(minimum, value));
33
+ }
34
+
35
+ function terminalColumns(output = processOutput) {
36
+ return Number.isFinite(output.columns) && output.columns > 0 ? output.columns : 80;
37
+ }
38
+
39
+ function layoutFor(columns) {
40
+ const width = clamp(columns - 4, MIN_CARD_WIDTH, MAX_CARD_WIDTH);
43
41
  return {
44
- top: "",
45
- "top-mid": "",
46
- "top-left": "",
47
- "top-right": "",
48
- bottom: "",
49
- "bottom-mid": "",
50
- "bottom-left": "",
51
- "bottom-right": "",
52
- left: "",
53
- "left-mid": "",
54
- mid: "",
55
- "mid-mid": "",
56
- right: "",
57
- "right-mid": "",
58
- middle: "",
42
+ width,
43
+ innerWidth: width - 4,
44
+ sectionWidth: width - 6,
45
+ padding: " ",
59
46
  };
60
47
  }
61
48
 
62
- function palette(colorEnabled) {
63
- return colorEnabled ? COLORS : MONOCHROME;
49
+ function style(value, tones, colorEnabled) {
50
+ const text = String(value);
51
+ if (!colorEnabled || !tones?.length) return text;
52
+ const prefix = tones.map((tone) => ANSI[tone] ?? "").join("");
53
+ return prefix ? `${prefix}${text}${ANSI.reset}` : text;
64
54
  }
65
55
 
66
- function record(value) {
67
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
56
+ function tone(value, colorName, colorEnabled, bold = false) {
57
+ return style(value, [bold ? "bold" : null, colorName].filter(Boolean), colorEnabled);
68
58
  }
69
59
 
70
- function clamp(value, minimum, maximum) {
71
- return Math.min(maximum, Math.max(minimum, value));
60
+ function visibleLength(value) {
61
+ return String(value).replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "").length;
72
62
  }
73
63
 
74
- function frameKey(response, view) {
75
- const frame = record(response.current_market_frame);
76
- return JSON.stringify({
77
- sourceTime: view.frameSourceTime,
78
- tapeId: frame.tape_id ?? frame.tapeId ?? null,
79
- last: view.last,
80
- status: view.status,
81
- });
64
+ function alignCell(cell, width) {
65
+ const padding = " ".repeat(Math.max(0, width - visibleLength(cell.content)));
66
+ return cell.hAlign === "right" ? `${padding}${cell.content}` : `${cell.content}${padding}`;
67
+ }
68
+
69
+ function tableValue(value, colorName, colorEnabled, { bold = false, hAlign = "left" } = {}) {
70
+ return { content: tone(value, colorName, colorEnabled, bold), hAlign };
71
+ }
72
+
73
+ function tableColumnWidths(rows) {
74
+ return rows[0].map((_, columnIndex) => Math.max(
75
+ ...rows.map((row) => visibleLength(row[columnIndex]?.content ?? "")),
76
+ ) + 2);
77
+ }
78
+
79
+ function renderTable(rows, colWidths, colorEnabled, { divider = true } = {}) {
80
+ const lines = rows.map((row) => row.map((cell, index) => alignCell(cell, colWidths[index])).join("").trimEnd());
81
+ if (!divider) return lines;
82
+ const dividerLine = tone("─".repeat(Math.max(1, colWidths.reduce((total, width) => total + width, 0))), "inactive", colorEnabled);
83
+ return [lines[0], dividerLine, ...lines.slice(1)];
82
84
  }
83
85
 
84
86
  function numberLabel(value, digits = 2) {
@@ -108,14 +110,11 @@ function signedPercent(value) {
108
110
  }
109
111
 
110
112
  function absolutePercent(value) {
111
- return typeof value === "number" && Number.isFinite(value)
112
- ? `${Math.abs(value).toFixed(2)}%`
113
- : null;
113
+ return typeof value === "number" && Number.isFinite(value) ? `${Math.abs(value).toFixed(2)}%` : null;
114
114
  }
115
115
 
116
116
  function marketVolumeLabel(value) {
117
- if (typeof value !== "number" || !Number.isFinite(value)) return "—";
118
- return `${value.toFixed(2)}x`;
117
+ return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(2)}x` : "—";
119
118
  }
120
119
 
121
120
  function metricTone(value) {
@@ -123,47 +122,12 @@ function metricTone(value) {
123
122
  return value > 0 ? "green" : "red";
124
123
  }
125
124
 
126
- function tableContent(value, tone, colorEnabled, bold = false) {
127
- const text = String(value);
128
- const code = ANSI_CODES[tone];
129
- return colorEnabled && code
130
- ? `${bold ? "\u001b[1m" : ""}\u001b[${code}m${text}\u001b[39m${bold ? "\u001b[22m" : ""}`
131
- : text;
132
- }
133
-
134
- function tableValue(value, tone, colorEnabled, { bold = false, hAlign = "left" } = {}) {
135
- const content = tableContent(value, tone, colorEnabled, bold);
136
- return { content, hAlign };
137
- }
138
-
139
- function visibleLength(value) {
140
- return String(value).replace(/\u001b\[[0-9;?]*[A-Za-z]/g, "").length;
141
- }
142
-
143
- function renderTable(rows, colWidths, colorEnabled, { divider = true } = {}) {
144
- const table = new Table({ chars: tableChars(), style: TABLE_STYLE, colWidths });
145
- table.push(...rows);
146
- const lines = table.toString().split("\n").map((line) => line.trimEnd());
147
- if (!divider) return lines.join("\n");
148
- const dividerLine = "─".repeat(Math.max(1, colWidths.reduce((total, width) => total + width, 0)));
149
- const coloredDivider = colorEnabled
150
- ? `${ansiForeground(LEROY_COLORS.inactive)}${dividerLine}\u001b[39m`
151
- : dividerLine;
152
- return [lines[0], coloredDivider, ...lines.slice(1)].join("\n");
153
- }
154
-
155
- function tableColumnWidths(rows) {
156
- return rows[0].map((_, columnIndex) => Math.max(
157
- ...rows.map((row) => visibleLength(row[columnIndex]?.content ?? "")),
158
- ) + 2);
159
- }
160
-
161
125
  function hasMarketMetrics(setup) {
162
126
  return [setup?.sessionReturnPct, setup?.volumeRatio, setup?.vwapDistancePct, setup?.atrPct]
163
127
  .some((value) => typeof value === "number" && Number.isFinite(value));
164
128
  }
165
129
 
166
- function MarketSetupTable({ symbol, setup, market, colorEnabled }) {
130
+ function renderMarketSetupTable({ symbol, setup, market, colorEnabled }) {
167
131
  const columns = [
168
132
  { symbol, setup },
169
133
  ...(hasMarketMetrics(market) ? [{ symbol: market.symbol, setup: market }] : []),
@@ -200,15 +164,7 @@ function MarketSetupTable({ symbol, setup, market, colorEnabled }) {
200
164
  ...columns.map((column) => tableValue(absolutePercent(column.setup.atrPct) ?? "—", "white", colorEnabled, { bold: true })),
201
165
  ],
202
166
  ];
203
- return React.createElement(Box, { flexDirection: "column", width: "100%" },
204
- React.createElement(Text, null, renderTable(rows, tableColumnWidths(rows), colorEnabled)),
205
- );
206
- }
207
-
208
- function strategyLabel(text, tone, alignment, width) {
209
- return React.createElement(Box, { key: text, width: `${100 / width}%`, justifyContent: alignment },
210
- React.createElement(Text, { color: tone }, text),
211
- );
167
+ return renderTable(rows, tableColumnWidths(rows), colorEnabled);
212
168
  }
213
169
 
214
170
  function proportionalSignalCells(segments, total) {
@@ -232,59 +188,50 @@ function proportionalSignalCells(segments, total) {
232
188
  return allocations.flatMap((segment) => Array.from({ length: segment.count }, () => segment.key));
233
189
  }
234
190
 
235
- function StrategyMix({ strategy, colorEnabled }) {
236
- const colors = palette(colorEnabled);
191
+ function alignedLabel(text, alignment, width) {
192
+ const space = Math.max(0, width - visibleLength(text));
193
+ if (alignment === "right") return `${" ".repeat(space)}${text}`;
194
+ if (alignment === "center") {
195
+ const left = Math.floor(space / 2);
196
+ return `${" ".repeat(left)}${text}${" ".repeat(space - left)}`;
197
+ }
198
+ return `${text}${" ".repeat(space)}`;
199
+ }
200
+
201
+ function renderStrategyMix(strategy, colorEnabled) {
237
202
  const neutralCount = strategy.counts.neutral + strategy.counts.warming;
238
203
  const segments = [
239
- { key: "buy", count: strategy.counts.buy, tone: colors.green },
240
- { key: "neutral", count: neutralCount, tone: colors.neutral },
241
- { key: "sell", count: strategy.counts.sell, tone: colors.red },
204
+ { key: "buy", count: strategy.counts.buy, colorName: "green" },
205
+ { key: "neutral", count: neutralCount, colorName: "neutral" },
206
+ { key: "sell", count: strategy.counts.sell, colorName: "red" },
242
207
  ];
243
208
  const total = segments.reduce((sum, segment) => sum + segment.count, 0);
244
- const stateList = proportionalSignalCells(segments, total);
245
- const toneByState = Object.fromEntries(segments.map((segment) => [segment.key, segment.tone]));
246
- const paddedStates = [
247
- ...stateList,
248
- ...Array.from({ length: STRATEGY_CELL_COUNT - stateList.length }, () => null),
249
- ];
209
+ const toneByState = Object.fromEntries(segments.map((segment) => [segment.key, segment.colorName]));
210
+ const activeStates = proportionalSignalCells(segments, total);
211
+ const states = [
212
+ ...activeStates,
213
+ ...Array.from({ length: STRATEGY_CELL_COUNT - activeStates.length }, () => null),
214
+ ].slice(0, STRATEGY_CELL_COUNT);
215
+ const cells = states.map((state, index) => `${tone(
216
+ strengthCellGlyph(Boolean(state), colorEnabled),
217
+ state ? toneByState[state] ?? "neutral" : "inactive",
218
+ colorEnabled,
219
+ )}${index < states.length - 1 ? " " : ""}`).join("");
250
220
  const rowWidth = STRATEGY_CELL_COUNT * 2 + STRATEGY_CELL_COUNT - 1;
251
- const strategyRow = React.createElement(Text, {
252
- width: rowWidth,
253
- }, paddedStates.map((state, cellIndex) => React.createElement(Text, {
254
- key: `strategy-cell-${cellIndex}`,
255
- color: state ? toneByState[state] ?? colors.neutral : colors.inactive,
256
- }, `${strengthCellGlyph(Boolean(state), colorEnabled)}${cellIndex < paddedStates.length - 1 ? " " : ""}`)));
257
221
  const labels = segments.map((segment) => `${segment.key[0].toUpperCase()}${segment.key.slice(1)} ${segment.count}`);
258
-
259
- return React.createElement(Box, { flexDirection: "column" },
260
- strategyRow,
261
- React.createElement(Box, { flexDirection: "row", width: rowWidth + 1, marginTop: 1 },
262
- labels.map((label, index) => strategyLabel(
263
- label,
264
- segments[index].tone,
265
- index === 0 ? "flex-start" : index === labels.length - 1 ? "flex-end" : "center",
266
- segments.length,
267
- )),
268
- ),
269
- );
270
- }
271
-
272
- function StatusLine({ icon, iconColor, textColor, children, marginTop = 0 }) {
273
- return React.createElement(Box, { flexDirection: "row", marginTop },
274
- React.createElement(Box, { width: 3 },
275
- React.createElement(Text, { color: iconColor, bold: true }, icon),
276
- ),
277
- React.createElement(Text, { color: textColor }, children),
278
- );
222
+ const labelWidth = (rowWidth + 1) / labels.length;
223
+ const labelLine = labels.map((label, index) => tone(
224
+ alignedLabel(label, index === 0 ? "left" : index === labels.length - 1 ? "right" : "center", labelWidth),
225
+ segments[index].colorName,
226
+ colorEnabled,
227
+ )).join("");
228
+ return [cells, "", labelLine];
279
229
  }
280
230
 
281
- function OutcomeTable({ outcomes, colorEnabled }) {
282
- const colors = palette(colorEnabled);
231
+ function renderOutcomeTable(outcomes, colorEnabled) {
283
232
  const hasHistoricalOutcomes = outcomes.some((outcome) => [outcome.mean, outcome.winRate, outcome.liftBps]
284
233
  .some((value) => typeof value === "number" && Number.isFinite(value)));
285
- if (!hasHistoricalOutcomes) {
286
- return React.createElement(Text, { color: colors.content }, "No historical outcomes available.");
287
- }
234
+ if (!hasHistoricalOutcomes) return [tone("No historical outcomes available.", "neutral", colorEnabled)];
288
235
  const rows = [
289
236
  [
290
237
  tableValue("Timeframe", "muted", colorEnabled, { bold: true }),
@@ -303,44 +250,13 @@ function OutcomeTable({ outcomes, colorEnabled }) {
303
250
  ...outcomes.map((outcome) => tableValue(bpsLabel(outcome.liftBps), metricTone(outcome.liftBps), colorEnabled, { bold: true, hAlign: "right" })),
304
251
  ],
305
252
  ];
306
- return React.createElement(Box, { flexDirection: "column" },
307
- React.createElement(Text, null, renderTable(rows, tableColumnWidths(rows), colorEnabled)),
308
- );
253
+ return renderTable(rows, tableColumnWidths(rows), colorEnabled);
309
254
  }
310
255
 
311
- function sideTone(side, colors) {
312
- if (side === "buy") return colors.green;
313
- if (side === "sell") return colors.red;
314
- return colors.white;
315
- }
316
-
317
- function SectionHeading({ label, meta, colors, width = "100%" }) {
318
- return React.createElement(Box, {
319
- flexDirection: "row",
320
- width,
321
- justifyContent: "space-between",
322
- },
323
- React.createElement(Box, { flexDirection: "row", flexGrow: 1 },
324
- React.createElement(Box, { width: 2 },
325
- React.createElement(Text, { color: colors.green, bold: true }, "▲"),
326
- ),
327
- React.createElement(Text, { color: colors.white, bold: true }, label),
328
- ),
329
- meta ? React.createElement(Text, { color: colors.white, bold: true }, meta) : null,
330
- );
331
- }
332
-
333
- function SectionBody({ children, colors, paddingLeft = 1 }) {
334
- return React.createElement(Box, {
335
- flexDirection: "column",
336
- width: "100%",
337
- borderStyle: "single",
338
- borderColor: colors.border,
339
- borderTop: false,
340
- borderBottom: false,
341
- borderRight: false,
342
- paddingLeft,
343
- }, children);
256
+ function sideTone(side) {
257
+ if (side === "buy") return "green";
258
+ if (side === "sell") return "red";
259
+ return "white";
344
260
  }
345
261
 
346
262
  function fallbackMarketSessionStatus(value = new Date()) {
@@ -363,293 +279,335 @@ function fallbackMarketSessionStatus(value = new Date()) {
363
279
  : "closed";
364
280
  }
365
281
 
366
- function RailSpacer({ colors }) {
367
- return React.createElement(Box, {
368
- height: 1,
369
- width: "100%",
370
- borderStyle: "single",
371
- borderColor: colors.border,
372
- borderTop: false,
373
- borderBottom: false,
374
- borderRight: false,
375
- });
282
+ function fitColumns(left, right, width) {
283
+ if (!right) return left;
284
+ return `${left}${" ".repeat(Math.max(1, width - visibleLength(left) - visibleLength(right)))}${right}`;
376
285
  }
377
286
 
378
- function QueryHeader({ view, colors }) {
287
+ function renderQueryHeader(view, colorEnabled, layout) {
379
288
  const side = typeof view.side === "string" ? view.side.toUpperCase() : "MARKET";
380
289
  const marketSessionStatus = view.marketSessionStatus ?? fallbackMarketSessionStatus();
381
290
  const marketIsOpen = marketSessionStatus === "open";
382
- const timestamp = [view.frameDate, view.frameTime]
383
- .filter((value) => value && value !== "")
384
- .join(" · ") || "";
385
- return React.createElement(Box, {
386
- flexDirection: "column",
387
- width: "100%",
388
- marginTop: 1,
389
- },
390
- React.createElement(Box, { flexDirection: "row", width: "100%", paddingRight: 2, justifyContent: "space-between" },
391
- React.createElement(Text, { color: colors.white },
392
- React.createElement(Text, { color: sideTone(view.side, colors), bold: true }, side),
393
- " · ",
394
- React.createElement(Text, { color: colors.white, bold: true }, view.symbol),
395
- " @ ",
396
- view.last,
397
- ),
398
- React.createElement(Text, { color: colors.muted }, timestamp),
399
- ),
400
- React.createElement(Box, { flexDirection: "row", width: "100%", paddingRight: 2, justifyContent: "flex-end" },
401
- React.createElement(Text, { color: marketIsOpen ? colors.green : colors.red, bold: true }, "●"),
402
- React.createElement(Text, { color: colors.white, bold: true }, ` Market ${marketIsOpen ? "Open" : "Closed"}`),
403
- ),
404
- );
405
- }
406
-
407
- function HistoricalEvidence({ view, colorEnabled }) {
291
+ const timestamp = [view.frameDate, view.frameTime].filter((value) => value && value !== "—").join(" · ") || "—";
292
+ const left = `${tone(side, sideTone(view.side), colorEnabled, true)} · ${tone(view.symbol, "white", colorEnabled, true)} @ ${tone(view.last, "white", colorEnabled)}`;
293
+ const status = `${tone("●", marketIsOpen ? "green" : "red", colorEnabled, true)}${tone(` Market ${marketIsOpen ? "Open" : "Closed"}`, "white", colorEnabled, true)}`;
294
+ return [
295
+ `${layout.padding}${fitColumns(left, tone(timestamp, "neutral", colorEnabled), layout.sectionWidth)}`,
296
+ `${layout.padding}${fitColumns("", status, layout.sectionWidth)}`,
297
+ ];
298
+ }
299
+
300
+ function renderSectionHeading(label, meta, colorEnabled, layout) {
301
+ const left = `${tone("▲", "green", colorEnabled, true)} ${tone(label, "white", colorEnabled, true)}`;
302
+ const right = meta ? tone(meta, "white", colorEnabled, true) : "";
303
+ return `${layout.padding}${fitColumns(left, right, layout.sectionWidth)}`;
304
+ }
305
+
306
+ function railLine(colorEnabled, layout) {
307
+ return `${layout.padding}${tone("│", "inactive", colorEnabled)}`;
308
+ }
309
+
310
+ function bodyLine(content, colorEnabled, layout) {
311
+ return `${layout.padding}${tone("", "inactive", colorEnabled)} ${content}`;
312
+ }
313
+
314
+ function renderHistoricalEvidence(view, colorEnabled) {
408
315
  const sampleLabel = view.match.score === 5
409
316
  ? " exact observations"
410
317
  : view.match.score > 0
411
318
  ? " comparable observations"
412
319
  : " observations";
413
320
  const matchLabel = view.match.label === "No match" ? "No Match" : `${view.match.label} Match`;
414
- const matchDetails = view.match.score > 0
415
- ? `${view.match.rowCount}${sampleLabel}`
416
- : "No comparable observations.";
321
+ const matchDetails = view.match.score > 0 ? `${view.match.rowCount}${sampleLabel}` : "No comparable observations.";
417
322
  const matchStrength = [
418
- Array.from({ length: 5 }, (_, index) => tableContent(
323
+ Array.from({ length: 5 }, (_, index) => tone(
419
324
  strengthCellGlyph(index < view.match.score, colorEnabled),
420
325
  index < view.match.score ? "green" : "neutral",
421
326
  colorEnabled,
422
327
  )).join(" "),
423
- tableContent(matchLabel, view.match.score > 0 ? "white" : "muted", colorEnabled, true),
328
+ tone(matchLabel, view.match.score > 0 ? "white" : "muted", colorEnabled, true),
424
329
  ].join(" ");
425
330
  const rows = [
426
- [
427
- tableValue("Match Strength", "muted", colorEnabled, { bold: true }),
428
- { content: matchStrength, hAlign: "left" },
429
- ],
430
- [
431
- tableValue("Match Details", "muted", colorEnabled, { bold: true }),
432
- tableValue(matchDetails, "content", colorEnabled, { bold: true }),
433
- ],
434
- [
435
- tableValue("Date Range", "muted", colorEnabled, { bold: true }),
436
- tableValue(view.archiveWindow, "content", colorEnabled),
437
- ],
331
+ [tableValue("Match Strength", "muted", colorEnabled, { bold: true }), { content: matchStrength, hAlign: "left" }],
332
+ [tableValue("Match Details", "muted", colorEnabled, { bold: true }), tableValue(matchDetails, "neutral", colorEnabled, { bold: true })],
333
+ [tableValue("Date Range", "muted", colorEnabled, { bold: true }), tableValue(view.archiveWindow, "neutral", colorEnabled)],
438
334
  ];
439
- return React.createElement(Text, null, renderTable(rows, tableColumnWidths(rows), colorEnabled, { divider: false }));
335
+ return renderTable(rows, tableColumnWidths(rows), colorEnabled, { divider: false });
440
336
  }
441
337
 
442
- function LoadingSteps({ columns, colorEnabled, symbol, progress, error, paddingX = 2, terminalWidth }) {
443
- const colors = palette(colorEnabled);
444
- const width = terminalWidth ?? clamp(columns - 4, MIN_CARD_WIDTH, MAX_CARD_WIDTH);
445
- const [spinnerIndex, setSpinnerIndex] = useState(0);
446
-
447
- useEffect(() => {
448
- if (progress.completed >= 4) return undefined;
449
- const timer = setInterval(() => {
450
- setSpinnerIndex((current) => (current + 1) % SPINNER_FRAMES.length);
451
- }, 90);
452
- return () => clearInterval(timer);
453
- }, [colorEnabled, progress.completed]);
454
-
455
- const steps = [
338
+ function loadingLabels(symbol, durationMs) {
339
+ return [
456
340
  `Loading live market data for ${String(symbol).toUpperCase()}.`,
457
341
  `Running ${STRATEGY_COUNT} strategy evaluations.`,
458
342
  "Checking for historical matches.",
459
- progress.durationMs === null ? "Finishing…" : `Finished in ${progress.durationMs}ms.`,
343
+ durationMs === null ? "Finishing…" : `Finished in ${durationMs}ms.`,
460
344
  ];
345
+ }
461
346
 
462
- return React.createElement(Box, {
463
- flexDirection: "column",
464
- width,
465
- paddingX,
466
- }, steps.map((label, index) => {
347
+ export function renderLoadingSteps({ symbol, progress = { completed: 0, durationMs: null }, error = null, spinnerIndex = 0, colorEnabled = true, columns = 80 }) {
348
+ const layout = layoutFor(columns);
349
+ const labels = loadingLabels(symbol, progress.durationMs);
350
+ return ["", ...labels.map((label, index) => {
467
351
  const isDone = index < progress.completed;
468
- const isActive = index === progress.completed && progress.completed < steps.length;
352
+ const isActive = index === progress.completed && progress.completed < labels.length;
469
353
  const isFailed = Boolean(error) && isActive;
470
- return React.createElement(StatusLine, {
471
- key: label,
472
- icon: isFailed ? "!" : isDone ? "" : isActive ? SPINNER_FRAMES[spinnerIndex] : "·",
473
- iconColor: isFailed ? colors.red : isDone ? colors.green : isActive ? colors.white : colors.muted,
474
- textColor: isDone ? colors.white : isFailed ? colors.white : colors.muted,
475
- marginTop: index === 0 ? 1 : 0,
476
- }, isFailed ? `${label} ${error}` : label);
477
- }),
478
- );
479
- }
480
-
481
- function EvidenceView({ response, colorEnabled, columns, phase, updatedAt, error, progress }) {
482
- const colors = palette(colorEnabled);
354
+ const icon = isFailed ? "!" : isDone ? "✓" : isActive ? SPINNER_FRAMES[spinnerIndex % SPINNER_FRAMES.length] : "·";
355
+ const iconColor = isFailed ? "red" : isDone ? "green" : isActive ? "white" : "muted";
356
+ const textColor = isDone || isFailed ? "white" : "muted";
357
+ const text = isFailed ? `${label} ${error}` : label;
358
+ return `${layout.padding}${tone(icon, iconColor, colorEnabled, true)} ${tone(text, textColor, colorEnabled)}`;
359
+ })].join("\n");
360
+ }
361
+
362
+ export function renderEvidenceView(response, { colorEnabled = true, columns = 80, phase = "live", updatedAt = null, error = null, progress = { completed: 4, durationMs: null } } = {}) {
363
+ const layout = layoutFor(columns);
483
364
  const view = buildEvaluationViewModel(response);
484
- const width = clamp(columns - 4, MIN_CARD_WIDTH, MAX_CARD_WIDTH);
485
- const sectionWidth = Math.max(1, width - 6);
486
365
  const updatedText = updatedAt
487
366
  ? new Date(updatedAt).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
488
367
  : "—";
489
- const liveTone = phase === "error" ? colors.red : phase === "checking" ? "yellow" : colors.green;
368
+ const liveTone = phase === "error" ? "red" : phase === "checking" ? "yellow" : "green";
490
369
  if (view.status === "artifact_unavailable") {
491
- return React.createElement(Box, {
492
- flexDirection: "column",
493
- width,
494
- paddingX: 2,
370
+ const lines = ["", ""];
371
+ lines.push(`${layout.padding}${tone("!", liveTone, colorEnabled, true)} ${tone("Market Memory unavailable", "white", colorEnabled)}`);
372
+ lines.push("");
373
+ lines.push(`${layout.padding}${tone("The private evidence reader is unavailable. Retrying on the next tape check.", "neutral", colorEnabled)}`);
374
+ lines.push("");
375
+ lines.push(`${layout.padding}${tone(`updated ${updatedText} · q/esc to exit`, "neutral", colorEnabled)}`);
376
+ if (error) {
377
+ lines.push("");
378
+ lines.push(`${layout.padding}${tone("!", "red", colorEnabled, true)} ${tone(error, "white", colorEnabled)}`);
379
+ }
380
+ return lines.join("\n");
381
+ }
382
+
383
+ const lines = renderLoadingSteps({
384
+ symbol: view.symbol,
385
+ progress,
386
+ colorEnabled,
387
+ columns,
388
+ }).split("\n");
389
+ lines.push("");
390
+ lines.push(...renderQueryHeader(view, colorEnabled, layout));
391
+ lines.push("");
392
+
393
+ lines.push(renderSectionHeading("Strategy Signals", `${view.strategy.total} Evaluated`, colorEnabled, layout));
394
+ lines.push(railLine(colorEnabled, layout));
395
+ lines.push(...renderStrategyMix(view.strategy, colorEnabled).map((line) => bodyLine(line, colorEnabled, layout)));
396
+ lines.push(railLine(colorEnabled, layout));
397
+
398
+ lines.push(renderSectionHeading("Market Context", null, colorEnabled, layout));
399
+ lines.push(railLine(colorEnabled, layout));
400
+ lines.push(...renderMarketSetupTable({
401
+ symbol: view.symbol,
402
+ setup: view.marketSetup,
403
+ market: view.marketOverview,
404
+ colorEnabled,
405
+ }).map((line) => bodyLine(line, colorEnabled, layout)));
406
+ lines.push(railLine(colorEnabled, layout));
407
+
408
+ lines.push(renderSectionHeading("Historical Evidence", null, colorEnabled, layout));
409
+ lines.push(railLine(colorEnabled, layout));
410
+ lines.push(...renderHistoricalEvidence(view, colorEnabled).map((line) => bodyLine(line, colorEnabled, layout)));
411
+ lines.push(railLine(colorEnabled, layout));
412
+
413
+ lines.push(renderSectionHeading("Observed Outcomes", null, colorEnabled, layout));
414
+ lines.push(railLine(colorEnabled, layout));
415
+ lines.push(...renderOutcomeTable(view.outcomes, colorEnabled).map((line) => bodyLine(line, colorEnabled, layout)));
416
+ if (error) {
417
+ lines.push("");
418
+ lines.push(`${layout.padding}${tone("!", "red", colorEnabled, true)} ${tone(error, "white", colorEnabled)}`);
419
+ }
420
+ lines.push("", "");
421
+ return lines.join("\n");
422
+ }
423
+
424
+ function frameKey(response, view) {
425
+ const frame = record(response.current_market_frame);
426
+ return JSON.stringify({
427
+ sourceTime: view.frameSourceTime,
428
+ tapeId: frame.tape_id ?? frame.tapeId ?? null,
429
+ last: view.last,
430
+ status: view.status,
431
+ });
432
+ }
433
+
434
+ function createExitController() {
435
+ let stopped = false;
436
+ const listeners = new Set();
437
+ return {
438
+ get stopped() {
439
+ return stopped;
440
+ },
441
+ stop() {
442
+ if (stopped) return;
443
+ stopped = true;
444
+ for (const listener of listeners) listener();
445
+ listeners.clear();
446
+ },
447
+ wait(milliseconds) {
448
+ if (stopped) return Promise.resolve(true);
449
+ return new Promise((resolve) => {
450
+ const onStop = () => {
451
+ clearTimeout(timer);
452
+ resolve(true);
453
+ };
454
+ const timer = setTimeout(() => {
455
+ listeners.delete(onStop);
456
+ resolve(false);
457
+ }, milliseconds);
458
+ listeners.add(onStop);
459
+ });
495
460
  },
496
- React.createElement(StatusLine, { icon: "!", iconColor: liveTone, textColor: colors.white, marginTop: 2 }, "Market Memory unavailable"),
497
- React.createElement(Text, { color: colors.muted, marginTop: 1 }, "The private evidence reader is unavailable. Retrying on the next tape check."),
498
- React.createElement(Text, { color: colors.muted, marginTop: 1 }, `updated ${updatedText} · q/esc to exit`),
499
- error ? React.createElement(StatusLine, { icon: "!", iconColor: colors.red, textColor: colors.white, marginTop: 1 }, error) : null,
500
- );
461
+ };
462
+ }
463
+
464
+ class NativeTerminalRenderer {
465
+ constructor(output) {
466
+ this.output = output;
467
+ this.lineCount = 0;
468
+ this.started = false;
501
469
  }
502
470
 
503
- return React.createElement(Box, {
504
- flexDirection: "column",
505
- width,
506
- paddingX: 2,
507
- },
508
- React.createElement(LoadingSteps, {
509
- columns,
510
- colorEnabled,
511
- symbol: view.symbol,
512
- progress,
513
- error: null,
514
- paddingX: 0,
515
- terminalWidth: Math.max(1, width - 4),
516
- }),
517
- React.createElement(QueryHeader, { view, colors }),
518
- React.createElement(Box, { flexDirection: "column", marginTop: 1 },
519
- React.createElement(SectionHeading, {
520
- label: "Strategy Signals",
521
- meta: `${view.strategy.total} Evaluated`,
522
- colors,
523
- width: sectionWidth,
524
- }),
525
- React.createElement(RailSpacer, { colors }),
526
- React.createElement(SectionBody, { colors },
527
- React.createElement(StrategyMix, {
528
- strategy: view.strategy,
529
- colorEnabled,
530
- }),
531
- ),
532
- ),
533
- React.createElement(RailSpacer, { colors }),
534
- React.createElement(Box, { flexDirection: "column" },
535
- React.createElement(SectionHeading, { label: "Market Context", colors, width: sectionWidth }),
536
- React.createElement(RailSpacer, { colors }),
537
- React.createElement(SectionBody, { colors },
538
- React.createElement(MarketSetupTable, {
539
- symbol: view.symbol,
540
- setup: view.marketSetup,
541
- market: view.marketOverview,
542
- colorEnabled,
543
- }),
544
- ),
545
- ),
546
- React.createElement(RailSpacer, { colors }),
547
- React.createElement(Box, { flexDirection: "column" },
548
- React.createElement(SectionHeading, { label: "Historical Evidence", colors, width: sectionWidth }),
549
- React.createElement(RailSpacer, { colors }),
550
- React.createElement(SectionBody, { colors },
551
- React.createElement(HistoricalEvidence, { view, colorEnabled }),
552
- ),
553
- ),
554
- React.createElement(RailSpacer, { colors }),
555
- React.createElement(Box, { flexDirection: "column" },
556
- React.createElement(SectionHeading, { label: "Observed Outcomes", colors, width: sectionWidth }),
557
- React.createElement(RailSpacer, { colors }),
558
- React.createElement(SectionBody, { colors },
559
- React.createElement(OutcomeTable, {
560
- outcomes: view.outcomes,
561
- colorEnabled,
562
- }),
563
- ),
564
- ),
565
- error ? React.createElement(StatusLine, { icon: "!", iconColor: colors.red, textColor: colors.white, marginTop: 1 }, error) : null,
566
- React.createElement(Box, { height: 2 }),
567
- );
471
+ draw(frame) {
472
+ if (!this.output.isTTY) {
473
+ this.output.write(`${frame}\n`);
474
+ return;
475
+ }
476
+ if (!this.started) {
477
+ this.output.write(ANSI.hideCursor);
478
+ this.started = true;
479
+ }
480
+ if (this.lineCount > 0) this.output.write(`\u001b[${this.lineCount}F`);
481
+ this.output.write("\u001b[J");
482
+ this.output.write(`${frame}\n`);
483
+ const columns = terminalColumns(this.output);
484
+ this.lineCount = frame.split("\n").reduce((total, line) => (
485
+ total + Math.max(1, Math.ceil(visibleLength(line) / columns))
486
+ ), 0);
487
+ }
488
+
489
+ finish() {
490
+ if (this.started) this.output.write(ANSI.showCursor);
491
+ }
568
492
  }
569
493
 
570
- export function LiveEvidenceApp({ client, request, colorEnabled = true, intervalMs = 60_000, polling = true }) {
571
- const { columns = 80 } = useWindowSize();
572
- const { exit } = useApp();
573
- const [state, setState] = useState({ response: null, phase: "connecting", updatedAt: null, frameKey: null, updateCount: 0, error: null, progress: { completed: 0, durationMs: null } });
494
+ function attachExitKeys(input, controller) {
495
+ if (!input.isTTY || typeof input.on !== "function") return () => {};
496
+ const canSetRawMode = typeof input.setRawMode === "function";
497
+ const wasRaw = Boolean(input.isRaw);
498
+ const onData = (chunk) => {
499
+ const key = String(chunk);
500
+ if (key === "\u0003" || key === "\u001b" || key.toLowerCase() === "q") controller.stop();
501
+ };
502
+ if (canSetRawMode) input.setRawMode(true);
503
+ input.resume();
504
+ input.on("data", onData);
505
+ return () => {
506
+ input.off("data", onData);
507
+ if (canSetRawMode) input.setRawMode(wasRaw);
508
+ if (typeof input.pause === "function") input.pause();
509
+ };
510
+ }
574
511
 
575
- useInput((input, key) => {
576
- if (input.toLowerCase() === "q" || key.escape) exit();
577
- });
512
+ async function loadEvidence({ client, request, response, renderer, colorEnabled, columns, controller }) {
513
+ const startedAt = Date.now();
514
+ let spinnerIndex = 0;
515
+ let progress = { completed: response ? 4 : 0, durationMs: response ? 0 : null };
516
+ if (!response) renderer.draw(renderLoadingSteps({ symbol: request.symbol, progress, spinnerIndex, colorEnabled, columns }));
517
+
518
+ const evaluation = Promise.resolve()
519
+ .then(() => client.evaluate(request))
520
+ .then((nextResponse) => ({ kind: "response", response: nextResponse }))
521
+ .catch((error) => ({ kind: "error", error }));
522
+
523
+ let result;
524
+ while (!controller.stopped && !result) {
525
+ result = await Promise.race([
526
+ evaluation,
527
+ controller.wait(90).then((stopped) => stopped ? { kind: "stopped" } : { kind: "tick" }),
528
+ ]);
529
+ if (result.kind === "tick") {
530
+ spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length;
531
+ if (!response) renderer.draw(renderLoadingSteps({ symbol: request.symbol, progress, spinnerIndex, colorEnabled, columns }));
532
+ result = null;
533
+ }
534
+ }
535
+ if (controller.stopped || result?.kind === "stopped") return { stopped: true };
536
+ if (result.kind === "error") {
537
+ const message = result.error instanceof Error ? result.error.message : String(result.error);
538
+ if (response) {
539
+ renderer.draw(renderEvidenceView(response, { colorEnabled, columns, phase: "error", error: message, progress }));
540
+ } else {
541
+ renderer.draw(renderLoadingSteps({ symbol: request.symbol, progress: { completed: 0, durationMs: null }, error: message, spinnerIndex, colorEnabled, columns }));
542
+ }
543
+ return { error: message };
544
+ }
578
545
 
579
- useEffect(() => {
580
- let active = true;
581
- let busy = false;
582
- const refresh = async () => {
583
- if (!active || busy) return;
584
- busy = true;
585
- const startedAt = Date.now();
586
- setState((current) => ({ ...current, phase: current.response ? "checking" : "connecting", error: null }));
587
- try {
588
- const response = await client.evaluate(request);
589
- if (!active) return;
546
+ const durationMs = Date.now() - startedAt;
547
+ for (let completed = 1; completed <= 4; completed += 1) {
548
+ progress = { completed, durationMs: completed === 4 ? durationMs : progress.durationMs };
549
+ if (response) {
550
+ renderer.draw(renderEvidenceView(response, { colorEnabled, columns, phase: "checking", progress }));
551
+ } else {
552
+ renderer.draw(renderLoadingSteps({ symbol: request.symbol, progress, spinnerIndex, colorEnabled, columns }));
553
+ }
554
+ if (completed < 4 && await controller.wait(LOADING_STEP_DELAY_MS)) return { stopped: true };
555
+ }
556
+ if (await controller.wait(LOADING_STEP_DELAY_MS)) return { stopped: true };
557
+ return { response: result.response, durationMs, progress };
558
+ }
559
+
560
+ export async function renderLiveEvidence({
561
+ client,
562
+ request,
563
+ colorEnabled = true,
564
+ intervalMs = 60_000,
565
+ polling = true,
566
+ input = processInput,
567
+ output = processOutput,
568
+ } = {}) {
569
+ const renderer = new NativeTerminalRenderer(output);
570
+ const controller = createExitController();
571
+ const detachKeys = attachExitKeys(input, controller);
572
+ let response = null;
573
+ let updatedAt = null;
574
+ let currentFrameKey = null;
575
+ let progress = { completed: 0, durationMs: null };
576
+ try {
577
+ do {
578
+ const loaded = await loadEvidence({
579
+ client,
580
+ request,
581
+ response,
582
+ renderer,
583
+ colorEnabled,
584
+ columns: terminalColumns(output),
585
+ controller,
586
+ });
587
+ if (loaded.stopped) break;
588
+ if (loaded.response) {
589
+ response = loaded.response;
590
+ progress = loaded.progress;
590
591
  const view = buildEvaluationViewModel(response);
591
- const nextKey = frameKey(response, view);
592
- const durationMs = Date.now() - startedAt;
593
- for (let completed = 1; completed <= 4; completed += 1) {
594
- if (!active) return;
595
- setState((current) => ({
596
- ...current,
597
- progress: { completed, durationMs: completed === 4 ? durationMs : current.progress.durationMs },
598
- }));
599
- if (completed < 4) await new Promise((resolve) => setTimeout(resolve, LOADING_STEP_DELAY_MS));
600
- }
601
- if (!active) return;
602
- await new Promise((resolve) => setTimeout(resolve, LOADING_STEP_DELAY_MS));
603
- if (!active) return;
604
- setState((current) => ({
605
- response,
592
+ const nextFrameKey = frameKey(response, view);
593
+ updatedAt = currentFrameKey === nextFrameKey ? updatedAt : Date.now();
594
+ currentFrameKey = nextFrameKey;
595
+ renderer.draw(renderEvidenceView(response, {
596
+ colorEnabled,
597
+ columns: terminalColumns(output),
606
598
  phase: "live",
607
- updatedAt: current.frameKey === nextKey ? current.updatedAt : Date.now(),
608
- frameKey: nextKey,
609
- updateCount: current.frameKey === nextKey ? current.updateCount : current.updateCount + 1,
610
- error: null,
611
- progress: { completed: 4, durationMs },
599
+ updatedAt,
600
+ progress,
612
601
  }));
613
- } catch (error) {
614
- if (!active) return;
615
- setState((current) => ({ ...current, phase: "error", updatedAt: Date.now(), error: error instanceof Error ? error.message : String(error) }));
616
- } finally {
617
- busy = false;
618
602
  }
619
- };
620
- void refresh();
621
- const timer = polling ? setInterval(() => { void refresh(); }, intervalMs) : null;
622
- return () => {
623
- active = false;
624
- if (timer) clearInterval(timer);
625
- };
626
- }, [client, intervalMs, polling, request]);
627
-
628
- useEffect(() => {
629
- if (polling || !state.response) return undefined;
630
- const timer = setTimeout(() => exit(), 120);
631
- return () => clearTimeout(timer);
632
- }, [exit, polling, state.response]);
633
-
634
- if (!state.response) {
635
- return React.createElement(LoadingSteps, {
636
- columns,
637
- colorEnabled,
638
- symbol: request.symbol,
639
- progress: state.progress,
640
- error: state.error,
641
- });
603
+ if (!polling) {
604
+ await controller.wait(120);
605
+ break;
606
+ }
607
+ if (await controller.wait(intervalMs)) break;
608
+ } while (!controller.stopped);
609
+ } finally {
610
+ detachKeys();
611
+ renderer.finish();
642
612
  }
643
-
644
- return React.createElement(EvidenceView, {
645
- response: state.response,
646
- colorEnabled,
647
- columns,
648
- phase: state.phase,
649
- updatedAt: state.updatedAt,
650
- error: state.error,
651
- progress: state.progress,
652
- });
653
613
  }
654
-
655
- export { render };