autotel-terminal 17.0.5 → 17.0.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.
package/dist/cli.cjs CHANGED
@@ -10,6 +10,9 @@ var autotel = require('autotel');
10
10
  var tracerProvider = require('autotel/tracer-provider');
11
11
  var ai = require('ai');
12
12
  var zod = require('zod');
13
+ var core = require('@json-render/core');
14
+ var catalog = require('@json-render/ink/catalog');
15
+ var ink$1 = require('@json-render/ink');
13
16
  require('autotel/exporters');
14
17
  var jsxRuntime = require('react/jsx-runtime');
15
18
 
@@ -828,17 +831,31 @@ You have tools to query the telemetry data precisely. Use them to answer questio
828
831
  - getTraceDetail: deep dive into a specific trace
829
832
  - searchSpans: search spans by name
830
833
  - searchLogs: search logs by message content
834
+ - renderUI: display rich terminal UI (tables, charts, badges)
831
835
 
832
- Use tools first to gather data, then synthesize a concise answer.
833
- Keep responses under 300 words.
836
+ ## Workflow
837
+ 1. Use data tools first to gather data
838
+ 2. Use renderUI to display structured results as tables, charts, or cards
839
+ 3. Add a brief text explanation after the rendered UI
840
+
841
+ ## When to use renderUI
842
+ Use renderUI for tables, comparisons, and metrics. Do NOT use it for short text answers.
843
+
844
+ renderUI spec format: { root: "id", elements: { "id": { type: "ComponentName", props: {...}, children: [] } } }
845
+
846
+ Components: Table (columns, rows), KeyValue (label, value), Badge (label, variant: success/error/warning/info), BarChart (data: [{label,value}]), Card (title, children), Heading (text), Divider, Text (text, color, bold), Box (flexDirection, children).
847
+
848
+ Table example: { type: "Table", props: { columns: [{ header: "Name", key: "name" }], rows: [{ name: "api" }] }, children: [] }
849
+
850
+ Keep text responses under 300 words.
834
851
  Use specific span names, durations, and attribute values from the data.
835
- Format for a narrow terminal column \u2014 use short paragraphs, not wide tables.
836
852
 
837
853
  Current dashboard summary:
838
854
  ${contextJson}`;
839
855
  }
856
+ var COMPONENT_NAMES = Object.keys(catalog.standardComponentDefinitions);
840
857
  var t = ai.tool;
841
- function createTelemetryTools(ctx) {
858
+ function createTelemetryTools(ctx, onRenderUI) {
842
859
  return {
843
860
  getOverviewStats: t({
844
861
  description: "Get high-level stats: total spans, error count, average duration, p95 duration, and service count.",
@@ -1006,6 +1023,33 @@ function createTelemetryTools(ctx) {
1006
1023
  attrs: l.attributes
1007
1024
  }));
1008
1025
  }
1026
+ }),
1027
+ renderUI: t({
1028
+ description: "Render rich terminal UI (tables, charts, badges) to display structured data. Use this when showing tabular data, comparisons, or metrics \u2014 not for short text answers. Available components: Table (columns + rows), KeyValue (key-value pairs), Badge (status labels: default/info/success/warning/error), BarChart (horizontal bars with labels), Card (grouped content with title), Heading (section title), Divider (separator), Text (styled text), Box (layout container).",
1029
+ parameters: zod.z.object({
1030
+ spec: zod.z.object({
1031
+ root: zod.z.string().describe("ID of the root element"),
1032
+ elements: zod.z.record(
1033
+ zod.z.string(),
1034
+ zod.z.object({
1035
+ type: zod.z.enum(COMPONENT_NAMES).describe("Component name"),
1036
+ props: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
1037
+ children: zod.z.array(zod.z.string()).describe("Child element keys")
1038
+ })
1039
+ ).describe("Map of element ID to component definition")
1040
+ }).describe("json-render spec defining the UI to display")
1041
+ }),
1042
+ execute: async ({ spec }) => {
1043
+ const validation = core.validateSpec(spec);
1044
+ if (!validation.valid) {
1045
+ return {
1046
+ rendered: false,
1047
+ error: validation.issues.map((i) => i.message).join("; ")
1048
+ };
1049
+ }
1050
+ onRenderUI?.(spec);
1051
+ return { rendered: true };
1052
+ }
1009
1053
  })
1010
1054
  };
1011
1055
  }
@@ -1072,11 +1116,11 @@ function Dashboard({
1072
1116
  const throttleRef = react.useRef(null);
1073
1117
  const pendingSpansRef = react.useRef([]);
1074
1118
  const [logs, setLogs] = react.useState([]);
1075
- const [aiActive, setAiActive] = react.useState(false);
1076
1119
  const [aiMessages, setAiMessages] = react.useState([]);
1077
1120
  const [aiInput, setAiInput] = react.useState("");
1078
1121
  const [aiState, setAiState] = react.useState({ status: "unconfigured" });
1079
1122
  const [aiInputMode, setAiInputMode] = react.useState(false);
1123
+ const [aiSpec, setAiSpec] = react.useState(null);
1080
1124
  const aiModelRef = react.useRef(null);
1081
1125
  const aiAbortRef = react.useRef(null);
1082
1126
  react.useEffect(() => {
@@ -1289,6 +1333,7 @@ function Dashboard({
1289
1333
  setAiInput("");
1290
1334
  const abort = new AbortController();
1291
1335
  aiAbortRef.current = abort;
1336
+ setAiSpec(null);
1292
1337
  setAiState({ status: "streaming", abortController: abort });
1293
1338
  const toolCtx = {
1294
1339
  spans,
@@ -1298,7 +1343,7 @@ function Dashboard({
1298
1343
  serviceStats,
1299
1344
  errorSummaries
1300
1345
  };
1301
- const tools = createTelemetryTools(toolCtx);
1346
+ const tools = createTelemetryTools(toolCtx, (spec) => setAiSpec(spec));
1302
1347
  const statsContext = JSON.stringify({
1303
1348
  viewMode,
1304
1349
  stats: {
@@ -1344,15 +1389,31 @@ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.len
1344
1389
  return updated;
1345
1390
  });
1346
1391
  }
1392
+ if (!fullText.trim()) {
1393
+ setAiMessages((prev) => {
1394
+ const updated = [...prev];
1395
+ const lastMsg = updated.at(-1);
1396
+ if (lastMsg?.role === "assistant" && !lastMsg.content.trim()) {
1397
+ updated[updated.length - 1] = {
1398
+ role: "assistant",
1399
+ content: "(No response from model \u2014 try a simpler question or a larger model)"
1400
+ };
1401
+ }
1402
+ return updated;
1403
+ });
1404
+ }
1347
1405
  setAiState({ status: "idle" });
1348
1406
  } catch (error) {
1349
1407
  if (abort.signal.aborted) {
1350
1408
  setAiState({ status: "idle" });
1351
1409
  return;
1352
1410
  }
1411
+ const errorMsg = error instanceof Error ? error.message : String(error);
1412
+ process.stderr.write(`[autotel-terminal] AI error: ${errorMsg}
1413
+ `);
1353
1414
  setAiState({
1354
1415
  status: "error",
1355
- message: error instanceof Error ? error.message : String(error)
1416
+ message: errorMsg
1356
1417
  });
1357
1418
  } finally {
1358
1419
  aiAbortRef.current = null;
@@ -1437,7 +1498,7 @@ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.len
1437
1498
  aiAbortRef.current?.abort();
1438
1499
  } else {
1439
1500
  setAiInputMode(false);
1440
- setAiActive(false);
1501
+ setViewMode("trace");
1441
1502
  }
1442
1503
  return;
1443
1504
  }
@@ -1457,14 +1518,19 @@ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.len
1457
1518
  return;
1458
1519
  }
1459
1520
  if (input === "a") {
1460
- setAiActive((v) => !v);
1461
- if (aiActive) {
1521
+ if (viewMode === "ai") {
1522
+ setViewMode("trace");
1462
1523
  setAiInputMode(false);
1463
1524
  } else {
1525
+ setViewMode("ai");
1464
1526
  if (aiState.status !== "unconfigured") {
1465
1527
  setAiInputMode(true);
1466
1528
  }
1467
1529
  }
1530
+ setSelected(0);
1531
+ setDrilldownTraceId(null);
1532
+ setDrilldownSelectedIndex(0);
1533
+ setDrilldownScrollOffset(0);
1468
1534
  return;
1469
1535
  }
1470
1536
  if (key.escape) {
@@ -1772,7 +1838,7 @@ ${json}
1772
1838
  { isActive: isRawModeSupported }
1773
1839
  );
1774
1840
  const headerRight = recording ? "[Recording]" : paused ? "[Paused]" : "[Live]";
1775
- const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : viewMode === "topology" ? "topology" : "errors";
1841
+ const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : viewMode === "topology" ? "topology" : viewMode === "ai" ? "AI" : "errors";
1776
1842
  const showNewError = newErrorCount > 0;
1777
1843
  function renderTreeRow(node, index) {
1778
1844
  const isSel = drilldownTraceId != null && index === drilldownSelectedIndex;
@@ -1853,10 +1919,10 @@ ${json}
1853
1919
  "\u2026"
1854
1920
  ] })
1855
1921
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", justifyContent: "space-between", children: [
1856
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: drilldownTraceId == null ? "\u2191/\u2193 select \u2022 Enter open \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" : "\u2191/\u2193 select \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" }),
1857
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: viewMode === "trace" ? `traces ${filteredSummaries.length}/${traceSummaries.length}` : viewMode === "span" ? `spans ${filteredSpans.length}/${spans.length}` : viewMode === "service-summary" ? `services ${serviceStats.length}` : viewMode === "errors" ? `errors ${filteredErrorSummaries.length}/${errorSummaries.length}` : viewMode === "topology" ? `services ${serviceGraph.services.length} \xB7 edges ${serviceGraph.edges.length}` : `logs ${filteredLogs.length}/${logs.length}` })
1922
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: drilldownTraceId == null ? "\u2191/\u2193 select \u2022 Enter open \u2022 Tab cycle tabs \u2022 Esc back \u2022 t trace \u2022 l logs \u2022 a AI \u2022 ? help" : "\u2191/\u2193 select \u2022 Tab cycle tabs \u2022 Esc back \u2022 t trace \u2022 l logs \u2022 a AI \u2022 ? help" }),
1923
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: viewMode === "trace" ? `traces ${filteredSummaries.length}/${traceSummaries.length}` : viewMode === "span" ? `spans ${filteredSpans.length}/${spans.length}` : viewMode === "service-summary" ? `services ${serviceStats.length}` : viewMode === "errors" ? `errors ${filteredErrorSummaries.length}/${errorSummaries.length}` : viewMode === "topology" ? `services ${serviceGraph.services.length} \xB7 edges ${serviceGraph.edges.length}` : viewMode === "ai" ? `messages ${aiMessages.length}` : `logs ${filteredLogs.length}/${logs.length}` })
1858
1924
  ] }),
1859
- showHelp && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t/l/v/E/G \u2022 Search: / \u2022 Filters: e/S/R/H/f/x \u2022 Capture: p/r/J \u2022 AI: a \u2022 Clear: c" })
1925
+ showHelp && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t/l/v/E/G/a \u2022 Search: / \u2022 Filters: e/S/R/H/f/x \u2022 Capture: p/r/J \u2022 Clear: c" })
1860
1926
  ] }),
1861
1927
  /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 0, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: spanFilters.serviceName || spanFilters.route || spanFilters.statusGroup !== "all" || spanFilters.traceId ? `filters:${spanFilters.serviceName ? ` service=${spanFilters.serviceName}` : ""}${spanFilters.route ? ` route=${spanFilters.route}` : ""}${spanFilters.statusGroup && spanFilters.statusGroup !== "all" ? ` status=${spanFilters.statusGroup}` : ""}${spanFilters.traceId ? ` trace=${spanFilters.traceId.slice(0, 8)}\u2026` : ""}` : "filters: none" }) }),
1862
1928
  viewMode === "topology" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
@@ -1867,7 +1933,34 @@ ${json}
1867
1933
  return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: hasErr ? "red" : void 0, children: line }, `topo-${i}`);
1868
1934
  }) })
1869
1935
  ] }),
1870
- viewMode !== "topology" && (drilldownTraceId != null ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, children: [
1936
+ viewMode === "ai" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, children: [
1937
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, justifyContent: "space-between", children: [
1938
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "AI Assistant" }),
1939
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: aiState.status === "streaming" ? "(streaming...)" : aiState.status === "unconfigured" ? "(no provider)" : aiState.status === "error" ? "(error)" : "" })
1940
+ ] }),
1941
+ aiState.status === "unconfigured" ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
1942
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No AI provider configured." }),
1943
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Set AI_PROVIDER and AI_MODEL env vars, or start Ollama locally." }),
1944
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press a to close this view." })
1945
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1946
+ aiMessages.length === 0 && aiState.status !== "error" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Ask a question about your telemetry data. Press Enter to send." }),
1947
+ aiMessages.slice(-10).map((msg, i) => /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", marginBottom: msg.role === "assistant" ? 1 : 0, children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: msg.role === "user" ? "cyan" : void 0, children: [
1948
+ msg.role === "user" ? "> " : "",
1949
+ msg.content.slice(0, 1e3),
1950
+ msg.content.length > 1e3 ? "..." : ""
1951
+ ] }) }, `ai-msg-${i}`)),
1952
+ aiSpec && /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", marginBottom: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink$1.Renderer, { spec: aiSpec }) }),
1953
+ aiState.status === "error" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
1954
+ "Error: ",
1955
+ aiState.message
1956
+ ] }),
1957
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [
1958
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "cyan", children: "> " }),
1959
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: aiInput || (aiInputMode ? "(type your question)" : "(press a to focus)") })
1960
+ ] })
1961
+ ] })
1962
+ ] }),
1963
+ viewMode !== "topology" && viewMode !== "ai" && (drilldownTraceId != null ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, children: [
1871
1964
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "column", children: [
1872
1965
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, children: [
1873
1966
  "Trace ",
@@ -2346,49 +2439,7 @@ ${json}
2346
2439
  borderColor: "gray",
2347
2440
  paddingX: 1,
2348
2441
  paddingY: 0,
2349
- children: aiActive ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2350
- /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, justifyContent: "space-between", children: [
2351
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "AI Assistant" }),
2352
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: aiState.status === "streaming" ? "(streaming...)" : aiState.status === "unconfigured" ? "(no provider)" : aiState.status === "error" ? "(error)" : "" })
2353
- ] }),
2354
- aiState.status === "unconfigured" ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2355
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No AI provider configured." }),
2356
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Set AI_PROVIDER and AI_MODEL env vars, or start Ollama locally." }),
2357
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press 'a' to close this panel." })
2358
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2359
- aiMessages.length === 0 && aiState.status !== "error" && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Ask a question about your telemetry data. Press Enter to send." }),
2360
- aiMessages.slice(-10).map((msg, i) => /* @__PURE__ */ jsxRuntime.jsx(
2361
- ink.Box,
2362
- {
2363
- flexDirection: "column",
2364
- marginBottom: msg.role === "assistant" ? 1 : 0,
2365
- children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: msg.role === "user" ? "cyan" : void 0, children: [
2366
- msg.role === "user" ? "> " : "",
2367
- msg.content.slice(0, 500),
2368
- msg.content.length > 500 ? "..." : ""
2369
- ] })
2370
- },
2371
- i
2372
- )),
2373
- aiState.status === "error" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
2374
- "Error: ",
2375
- aiState.message
2376
- ] }),
2377
- /* @__PURE__ */ jsxRuntime.jsxs(
2378
- ink.Box,
2379
- {
2380
- marginTop: 1,
2381
- borderStyle: "single",
2382
- borderColor: "cyan",
2383
- paddingX: 1,
2384
- children: [
2385
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "cyan", children: "> " }),
2386
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: aiInput || (aiInputMode ? "(type your question)" : "(press a to focus)") })
2387
- ]
2388
- }
2389
- )
2390
- ] })
2391
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2442
+ children: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2392
2443
  /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Details" }) }),
2393
2444
  viewMode === "errors" ? (() => {
2394
2445
  const e = filteredErrorSummaries[selected] ?? null;