autotel-terminal 17.0.4 → 17.0.6

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
 
@@ -111,6 +114,25 @@ function createTerminalSpanStream(processor) {
111
114
  const startTime = timeToMs(span.startTime);
112
115
  const endTime = timeToMs(span.endTime);
113
116
  const durationMs = endTime - startTime;
117
+ const resourceAttrs = span.resource?.attributes ?? {};
118
+ const spanAttrs = span.attributes;
119
+ const mergedAttrs = {};
120
+ for (const [k, v] of Object.entries(resourceAttrs)) {
121
+ mergedAttrs[k] = v;
122
+ }
123
+ for (const [k, v] of Object.entries(spanAttrs)) {
124
+ mergedAttrs[k] = v;
125
+ }
126
+ const spanEvents = span.events?.length ? span.events.map((e) => ({
127
+ name: e.name,
128
+ timeMs: timeToMs(e.time),
129
+ attributes: e.attributes
130
+ })) : void 0;
131
+ const spanLinks = span.links?.length ? span.links.map((l) => ({
132
+ traceId: l.context.traceId,
133
+ spanId: l.context.spanId,
134
+ attributes: l.attributes
135
+ })) : void 0;
114
136
  const event = {
115
137
  name: span.name,
116
138
  spanId: spanContext.spanId,
@@ -121,7 +143,9 @@ function createTerminalSpanStream(processor) {
121
143
  durationMs,
122
144
  status: mapStatus(span.status.code),
123
145
  kind: mapKind(span.kind),
124
- attributes: span.attributes
146
+ attributes: mergedAttrs,
147
+ ...spanEvents ? { events: spanEvents } : {},
148
+ ...spanLinks ? { links: spanLinks } : {}
125
149
  };
126
150
  callback(event);
127
151
  });
@@ -561,6 +585,123 @@ function buildErrorSummaries(traceSummaries) {
561
585
  return out;
562
586
  }
563
587
 
588
+ // src/lib/topology-model.ts
589
+ function getServiceName2(span) {
590
+ const attrs = span.attributes ?? {};
591
+ const serviceName = attrs["service.name"] ?? attrs["resource.service.name"];
592
+ return serviceName || "unknown";
593
+ }
594
+ function getPeerService(span) {
595
+ const attrs = span.attributes ?? {};
596
+ const peerService = attrs["peer.service"] ?? attrs["db.system"] ?? attrs["messaging.system"] ?? attrs["http.host"];
597
+ return peerService ?? null;
598
+ }
599
+ function buildServiceGraph(spans) {
600
+ const byService = /* @__PURE__ */ new Map();
601
+ for (const span of spans) {
602
+ const svc = getServiceName2(span);
603
+ const entry = byService.get(svc) ?? {
604
+ durations: [],
605
+ spanCount: 0,
606
+ errorCount: 0
607
+ };
608
+ entry.spanCount += 1;
609
+ entry.durations.push(span.durationMs);
610
+ if (span.status === "ERROR") entry.errorCount += 1;
611
+ byService.set(svc, entry);
612
+ }
613
+ const services = [];
614
+ for (const [serviceName, { durations, spanCount, errorCount }] of byService) {
615
+ if (spanCount === 0) continue;
616
+ const avgDurationMs = durations.reduce((acc, d) => acc + d, 0) / durations.length;
617
+ const sorted = durations.toSorted((a, b) => a - b);
618
+ const p95Index = Math.floor(sorted.length * 0.95);
619
+ const p95DurationMs = sorted[p95Index] ?? sorted.at(-1) ?? 0;
620
+ services.push({
621
+ serviceName,
622
+ spanCount,
623
+ errorCount,
624
+ avgDurationMs,
625
+ p95DurationMs
626
+ });
627
+ }
628
+ const byEdge = /* @__PURE__ */ new Map();
629
+ for (const span of spans) {
630
+ const to = getPeerService(span);
631
+ if (!to) continue;
632
+ const from = getServiceName2(span);
633
+ const key = `${from}\u2192${to}`;
634
+ const entry = byEdge.get(key) ?? { spanCount: 0, errorCount: 0 };
635
+ entry.spanCount += 1;
636
+ if (span.status === "ERROR") entry.errorCount += 1;
637
+ byEdge.set(key, entry);
638
+ }
639
+ const edges = [];
640
+ for (const [key, { spanCount, errorCount }] of byEdge) {
641
+ const [fromService, toService] = key.split("\u2192");
642
+ edges.push({ fromService, toService, spanCount, errorCount });
643
+ }
644
+ services.sort((a, b) => b.spanCount - a.spanCount);
645
+ edges.sort((a, b) => b.spanCount - a.spanCount);
646
+ return { services, edges };
647
+ }
648
+
649
+ // src/lib/topology-render.ts
650
+ function renderTopologyAscii(graph) {
651
+ if (graph.services.length === 0) {
652
+ return [" No services detected yet."];
653
+ }
654
+ const targetServices = new Set(graph.edges.map((e) => e.toService));
655
+ const roots = graph.services.filter(
656
+ (s) => !targetServices.has(s.serviceName)
657
+ );
658
+ const rootList = roots.length > 0 ? roots : [...graph.services];
659
+ const lines = [];
660
+ for (const root of rootList) {
661
+ renderNode(graph, root.serviceName, "", true, lines, /* @__PURE__ */ new Set());
662
+ }
663
+ return lines;
664
+ }
665
+ function renderNode(graph, serviceName, prefix, isRoot, lines, ancestors) {
666
+ const svc = graph.services.find((s) => s.serviceName === serviceName);
667
+ const label = svc ? formatServiceLine(svc) : `[${serviceName}]`;
668
+ if (isRoot) {
669
+ lines.push(label);
670
+ }
671
+ if (ancestors.has(serviceName)) return;
672
+ const pathAncestors = new Set(ancestors);
673
+ pathAncestors.add(serviceName);
674
+ const outgoing = graph.edges.filter((e) => e.fromService === serviceName);
675
+ for (let i = 0; i < outgoing.length; i++) {
676
+ const edge = outgoing[i];
677
+ const isLast = i === outgoing.length - 1;
678
+ const connector = isLast ? "\u2514" : "\u251C";
679
+ const childPrefix = isLast ? " " : "\u2502 ";
680
+ const downstream = graph.services.find(
681
+ (s) => s.serviceName === edge.toService
682
+ );
683
+ const edgeLabel = formatEdgeLabel(edge);
684
+ const downstreamLabel = downstream ? formatServiceLine(downstream) : `[${edge.toService}]`;
685
+ lines.push(`${prefix} ${connector}\u2500\u2500${edgeLabel}\u2500\u2500\u2192 ${downstreamLabel}`);
686
+ renderNode(
687
+ graph,
688
+ edge.toService,
689
+ prefix + " " + childPrefix,
690
+ false,
691
+ lines,
692
+ pathAncestors
693
+ );
694
+ }
695
+ }
696
+ function formatServiceLine(svc) {
697
+ const errPart = svc.errorCount > 0 ? ` \xB7 ${svc.errorCount} err` : "";
698
+ return `[${svc.serviceName}] ${svc.spanCount} spans${errPart} \xB7 p95 ${formatDurationMs(svc.p95DurationMs)}`;
699
+ }
700
+ function formatEdgeLabel(edge) {
701
+ const errPart = edge.errorCount > 0 ? `, ${edge.errorCount} err` : "";
702
+ return `(${edge.spanCount}${errPart})`;
703
+ }
704
+
564
705
  // src/lib/export-model.ts
565
706
  function exportTraceToJson(trace, logs) {
566
707
  const exported = {
@@ -690,17 +831,31 @@ You have tools to query the telemetry data precisely. Use them to answer questio
690
831
  - getTraceDetail: deep dive into a specific trace
691
832
  - searchSpans: search spans by name
692
833
  - searchLogs: search logs by message content
834
+ - renderUI: display rich terminal UI (tables, charts, badges)
835
+
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
693
840
 
694
- Use tools first to gather data, then synthesize a concise answer.
695
- Keep responses under 300 words.
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.
696
851
  Use specific span names, durations, and attribute values from the data.
697
- Format for a narrow terminal column \u2014 use short paragraphs, not wide tables.
698
852
 
699
853
  Current dashboard summary:
700
854
  ${contextJson}`;
701
855
  }
856
+ var COMPONENT_NAMES = Object.keys(catalog.standardComponentDefinitions);
702
857
  var t = ai.tool;
703
- function createTelemetryTools(ctx) {
858
+ function createTelemetryTools(ctx, onRenderUI) {
704
859
  return {
705
860
  getOverviewStats: t({
706
861
  description: "Get high-level stats: total spans, error count, average duration, p95 duration, and service count.",
@@ -868,6 +1023,33 @@ function createTelemetryTools(ctx) {
868
1023
  attrs: l.attributes
869
1024
  }));
870
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
+ }
871
1053
  })
872
1054
  };
873
1055
  }
@@ -934,11 +1116,11 @@ function Dashboard({
934
1116
  const throttleRef = react.useRef(null);
935
1117
  const pendingSpansRef = react.useRef([]);
936
1118
  const [logs, setLogs] = react.useState([]);
937
- const [aiActive, setAiActive] = react.useState(false);
938
1119
  const [aiMessages, setAiMessages] = react.useState([]);
939
1120
  const [aiInput, setAiInput] = react.useState("");
940
1121
  const [aiState, setAiState] = react.useState({ status: "unconfigured" });
941
1122
  const [aiInputMode, setAiInputMode] = react.useState(false);
1123
+ const [aiSpec, setAiSpec] = react.useState(null);
942
1124
  const aiModelRef = react.useRef(null);
943
1125
  const aiAbortRef = react.useRef(null);
944
1126
  react.useEffect(() => {
@@ -1078,6 +1260,8 @@ function Dashboard({
1078
1260
  [spansForSelectedService]
1079
1261
  );
1080
1262
  const selectedTraceSummary = drilldownTraceId == null ? filteredSummaries[selected] ?? null : filteredSummaries.find((t2) => t2.traceId === drilldownTraceId) ?? null;
1263
+ const serviceGraph = react.useMemo(() => buildServiceGraph(spans), [spans]);
1264
+ const topologyLines = react.useMemo(() => renderTopologyAscii(serviceGraph), [serviceGraph]);
1081
1265
  const errorSummaries = react.useMemo(
1082
1266
  () => buildErrorSummaries(traceSummaries),
1083
1267
  [traceSummaries]
@@ -1149,6 +1333,7 @@ function Dashboard({
1149
1333
  setAiInput("");
1150
1334
  const abort = new AbortController();
1151
1335
  aiAbortRef.current = abort;
1336
+ setAiSpec(null);
1152
1337
  setAiState({ status: "streaming", abortController: abort });
1153
1338
  const toolCtx = {
1154
1339
  spans,
@@ -1158,7 +1343,7 @@ function Dashboard({
1158
1343
  serviceStats,
1159
1344
  errorSummaries
1160
1345
  };
1161
- const tools = createTelemetryTools(toolCtx);
1346
+ const tools = createTelemetryTools(toolCtx, (spec) => setAiSpec(spec));
1162
1347
  const statsContext = JSON.stringify({
1163
1348
  viewMode,
1164
1349
  stats: {
@@ -1204,15 +1389,31 @@ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.len
1204
1389
  return updated;
1205
1390
  });
1206
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
+ }
1207
1405
  setAiState({ status: "idle" });
1208
1406
  } catch (error) {
1209
1407
  if (abort.signal.aborted) {
1210
1408
  setAiState({ status: "idle" });
1211
1409
  return;
1212
1410
  }
1411
+ const errorMsg = error instanceof Error ? error.message : String(error);
1412
+ process.stderr.write(`[autotel-terminal] AI error: ${errorMsg}
1413
+ `);
1213
1414
  setAiState({
1214
1415
  status: "error",
1215
- message: error instanceof Error ? error.message : String(error)
1416
+ message: errorMsg
1216
1417
  });
1217
1418
  } finally {
1218
1419
  aiAbortRef.current = null;
@@ -1297,7 +1498,7 @@ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.len
1297
1498
  aiAbortRef.current?.abort();
1298
1499
  } else {
1299
1500
  setAiInputMode(false);
1300
- setAiActive(false);
1501
+ setViewMode("trace");
1301
1502
  }
1302
1503
  return;
1303
1504
  }
@@ -1317,14 +1518,19 @@ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.len
1317
1518
  return;
1318
1519
  }
1319
1520
  if (input === "a") {
1320
- setAiActive((v) => !v);
1321
- if (aiActive) {
1521
+ if (viewMode === "ai") {
1522
+ setViewMode("trace");
1322
1523
  setAiInputMode(false);
1323
1524
  } else {
1525
+ setViewMode("ai");
1324
1526
  if (aiState.status !== "unconfigured") {
1325
1527
  setAiInputMode(true);
1326
1528
  }
1327
1529
  }
1530
+ setSelected(0);
1531
+ setDrilldownTraceId(null);
1532
+ setDrilldownSelectedIndex(0);
1533
+ setDrilldownScrollOffset(0);
1328
1534
  return;
1329
1535
  }
1330
1536
  if (key.escape) {
@@ -1484,6 +1690,13 @@ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.len
1484
1690
  setDrilldownSelectedIndex(0);
1485
1691
  setDrilldownScrollOffset(0);
1486
1692
  }
1693
+ if (input === "G") {
1694
+ setViewMode((m) => m === "topology" ? "trace" : "topology");
1695
+ setSelected(0);
1696
+ setDrilldownTraceId(null);
1697
+ setDrilldownSelectedIndex(0);
1698
+ setDrilldownScrollOffset(0);
1699
+ }
1487
1700
  if (input === "c") {
1488
1701
  setSpans([]);
1489
1702
  setLogs([]);
@@ -1625,7 +1838,7 @@ ${json}
1625
1838
  { isActive: isRawModeSupported }
1626
1839
  );
1627
1840
  const headerRight = recording ? "[Recording]" : paused ? "[Paused]" : "[Live]";
1628
- const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : "errors";
1841
+ const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : viewMode === "topology" ? "topology" : viewMode === "ai" ? "AI" : "errors";
1629
1842
  const showNewError = newErrorCount > 0;
1630
1843
  function renderTreeRow(node, index) {
1631
1844
  const isSel = drilldownTraceId != null && index === drilldownSelectedIndex;
@@ -1639,8 +1852,9 @@ ${json}
1639
1852
  flexDirection: "row",
1640
1853
  children: [
1641
1854
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { backgroundColor: isSel ? "blue" : void 0, color: isSel ? "white" : void 0, children: isSel ? "\u25B8 " : " " }),
1855
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: node.span.status === "ERROR" ? "red" : void 0, children: node.span.status === "ERROR" ? "\u2717" : " " }),
1642
1856
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: prefix }),
1643
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name, 24) }),
1857
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name, 23) }),
1644
1858
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: svcColor, children: [
1645
1859
  " ",
1646
1860
  truncate(svcName, 10)
@@ -1705,13 +1919,48 @@ ${json}
1705
1919
  "\u2026"
1706
1920
  ] })
1707
1921
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", justifyContent: "space-between", children: [
1708
- /* @__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" }),
1709
- /* @__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}` : `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}` })
1710
1924
  ] }),
1711
- showHelp && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t/l/v/E \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" })
1712
1926
  ] }),
1713
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" }) }),
1714
- drilldownTraceId != null ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 0, children: [
1928
+ viewMode === "topology" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
1929
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Service Topology" }),
1930
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press G to toggle \xB7 Shows service dependencies from span data" }),
1931
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", marginTop: 1, children: topologyLines.map((line, i) => {
1932
+ const hasErr = line.includes(" err");
1933
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: hasErr ? "red" : void 0, children: line }, `topo-${i}`);
1934
+ }) })
1935
+ ] }),
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: [
1715
1964
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "column", children: [
1716
1965
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, children: [
1717
1966
  "Trace ",
@@ -1810,7 +2059,8 @@ ${json}
1810
2059
  const svcColor = getServiceColor(svcName);
1811
2060
  const kindStr = (s.kind ?? "").padEnd(KIND_COL);
1812
2061
  const svcStr = truncate(svcName, SERVICE_COL - 2).padEnd(SERVICE_COL);
1813
- const namePart = `${indent}${truncate(s.name, nameWidth)}`.padEnd(NAME_COL);
2062
+ const errorMark = s.status === "ERROR" ? "\u2717" : " ";
2063
+ const namePart = `${indent}${truncate(s.name, nameWidth - 1)}`.padEnd(NAME_COL - 1);
1814
2064
  const bar = buildWaterfallBar(
1815
2065
  s.startTime,
1816
2066
  s.durationMs,
@@ -1821,6 +2071,7 @@ ${json}
1821
2071
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
1822
2072
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { backgroundColor: isSel ? "blue" : void 0, color: isSel ? "white" : void 0, children: [
1823
2073
  isSel ? "\u25B8" : " ",
2074
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: s.status === "ERROR" ? "red" : void 0, children: errorMark }),
1824
2075
  namePart
1825
2076
  ] }),
1826
2077
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: svcColor, children: [
@@ -1927,6 +2178,53 @@ ${json}
1927
2178
  ": ",
1928
2179
  truncate(String(v), 28)
1929
2180
  ] }, k))
2181
+ ] }),
2182
+ span.events && span.events.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginTop: 0, children: [
2183
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, dimColor: true, children: [
2184
+ "Events (",
2185
+ span.events.length,
2186
+ ")"
2187
+ ] }),
2188
+ span.events.slice(0, 5).map((ev, i) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
2189
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: ev.name === "exception" ? "red" : "yellow", children: [
2190
+ " ",
2191
+ "\u25C6",
2192
+ " ",
2193
+ truncate(ev.name, 20)
2194
+ ] }),
2195
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2196
+ " ",
2197
+ "+",
2198
+ formatDurationMs(ev.timeMs - span.startTime)
2199
+ ] }),
2200
+ ev.attributes && Object.keys(ev.attributes).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2201
+ " ",
2202
+ Object.entries(ev.attributes).slice(0, 2).map(([k, v]) => `${k}=${String(v)}`).join(" ")
2203
+ ] })
2204
+ ] }, `ev-${i}`))
2205
+ ] }),
2206
+ span.links && span.links.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginTop: 0, children: [
2207
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { bold: true, dimColor: true, children: [
2208
+ "Links (",
2209
+ span.links.length,
2210
+ ")"
2211
+ ] }),
2212
+ span.links.slice(0, 5).map((lnk, i) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
2213
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
2214
+ " ",
2215
+ "\u2192",
2216
+ " trace:",
2217
+ lnk.traceId.slice(0, 8),
2218
+ "\u2026",
2219
+ " span:",
2220
+ lnk.spanId.slice(0, 8),
2221
+ "\u2026"
2222
+ ] }),
2223
+ lnk.attributes && Object.keys(lnk.attributes).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2224
+ " ",
2225
+ Object.entries(lnk.attributes).slice(0, 2).map(([k, v]) => `${k}=${String(v)}`).join(" ")
2226
+ ] })
2227
+ ] }, `lnk-${i}`))
1930
2228
  ] })
1931
2229
  ] });
1932
2230
  })(),
@@ -2141,49 +2439,7 @@ ${json}
2141
2439
  borderColor: "gray",
2142
2440
  paddingX: 1,
2143
2441
  paddingY: 0,
2144
- children: aiActive ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2145
- /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, justifyContent: "space-between", children: [
2146
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "AI Assistant" }),
2147
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: aiState.status === "streaming" ? "(streaming...)" : aiState.status === "unconfigured" ? "(no provider)" : aiState.status === "error" ? "(error)" : "" })
2148
- ] }),
2149
- aiState.status === "unconfigured" ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
2150
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No AI provider configured." }),
2151
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Set AI_PROVIDER and AI_MODEL env vars, or start Ollama locally." }),
2152
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press 'a' to close this panel." })
2153
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2154
- 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." }),
2155
- aiMessages.slice(-10).map((msg, i) => /* @__PURE__ */ jsxRuntime.jsx(
2156
- ink.Box,
2157
- {
2158
- flexDirection: "column",
2159
- marginBottom: msg.role === "assistant" ? 1 : 0,
2160
- children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: msg.role === "user" ? "cyan" : void 0, children: [
2161
- msg.role === "user" ? "> " : "",
2162
- msg.content.slice(0, 500),
2163
- msg.content.length > 500 ? "..." : ""
2164
- ] })
2165
- },
2166
- i
2167
- )),
2168
- aiState.status === "error" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
2169
- "Error: ",
2170
- aiState.message
2171
- ] }),
2172
- /* @__PURE__ */ jsxRuntime.jsxs(
2173
- ink.Box,
2174
- {
2175
- marginTop: 1,
2176
- borderStyle: "single",
2177
- borderColor: "cyan",
2178
- paddingX: 1,
2179
- children: [
2180
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "cyan", children: "> " }),
2181
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: aiInput || (aiInputMode ? "(type your question)" : "(press a to focus)") })
2182
- ]
2183
- }
2184
- )
2185
- ] })
2186
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2442
+ children: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2187
2443
  /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Details" }) }),
2188
2444
  viewMode === "errors" ? (() => {
2189
2445
  const e = filteredErrorSummaries[selected] ?? null;
@@ -2407,7 +2663,7 @@ ${json}
2407
2663
  ] })
2408
2664
  }
2409
2665
  )
2410
- ] }),
2666
+ ] })),
2411
2667
  showStats && /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2412
2668
  "Spans: ",
2413
2669
  stats.total,
@@ -2603,6 +2859,8 @@ function* extractSpans(payload) {
2603
2859
  if (!Array.isArray(resourceSpans)) return;
2604
2860
  for (const resourceSpan of resourceSpans) {
2605
2861
  if (!resourceSpan || typeof resourceSpan !== "object") continue;
2862
+ const resource = resourceSpan.resource;
2863
+ const resourceAttrs = attrsToRecord(resource?.attributes);
2606
2864
  const scopeSpans = resourceSpan.scopeSpans;
2607
2865
  if (!Array.isArray(scopeSpans)) continue;
2608
2866
  for (const scopeSpan of scopeSpans) {
@@ -2611,15 +2869,33 @@ function* extractSpans(payload) {
2611
2869
  if (!Array.isArray(spans)) continue;
2612
2870
  for (const span of spans) {
2613
2871
  if (span && typeof span === "object") {
2614
- yield span;
2872
+ yield { span, resourceAttrs };
2615
2873
  }
2616
2874
  }
2617
2875
  }
2618
2876
  }
2619
2877
  }
2620
- function otlpSpanToTerminalEvent(span) {
2878
+ function otlpSpanToTerminalEvent(span, resourceAttrs = {}) {
2621
2879
  const startTime = toMs(span.startTimeUnixNano);
2622
2880
  const endTime = toMs(span.endTimeUnixNano);
2881
+ const spanAttrs = attrsToRecord(span.attributes);
2882
+ const mergedAttrs = {};
2883
+ for (const [k, v] of Object.entries(resourceAttrs)) {
2884
+ mergedAttrs[k] = v;
2885
+ }
2886
+ for (const [k, v] of Object.entries(spanAttrs)) {
2887
+ mergedAttrs[k] = v;
2888
+ }
2889
+ const parsedEvents = span.events?.length ? span.events.map((e) => ({
2890
+ name: e.name || "",
2891
+ timeMs: toMs(e.timeUnixNano),
2892
+ attributes: attrsToRecord(e.attributes)
2893
+ })) : void 0;
2894
+ const parsedLinks = span.links?.length ? span.links.map((l) => ({
2895
+ traceId: normalizeHexId(l.traceId, 32),
2896
+ spanId: normalizeHexId(l.spanId, 16),
2897
+ attributes: attrsToRecord(l.attributes)
2898
+ })) : void 0;
2623
2899
  return {
2624
2900
  name: span.name || "unnamed",
2625
2901
  spanId: normalizeHexId(span.spanId, 16),
@@ -2630,7 +2906,9 @@ function otlpSpanToTerminalEvent(span) {
2630
2906
  durationMs: Math.max(0, endTime - startTime),
2631
2907
  status: mapStatus2(span.status?.code),
2632
2908
  kind: mapKind2(span.kind),
2633
- attributes: attrsToRecord(span.attributes)
2909
+ attributes: mergedAttrs,
2910
+ ...parsedEvents ? { events: parsedEvents } : {},
2911
+ ...parsedLinks ? { links: parsedLinks } : {}
2634
2912
  };
2635
2913
  }
2636
2914
  async function readJsonBody(req) {
@@ -2655,8 +2933,8 @@ function sendJson(res, status, data) {
2655
2933
  }
2656
2934
  function parseOtlpEvents(payload) {
2657
2935
  const events = [];
2658
- for (const span of extractSpans(payload)) {
2659
- events.push(otlpSpanToTerminalEvent(span));
2936
+ for (const { span, resourceAttrs } of extractSpans(payload)) {
2937
+ events.push(otlpSpanToTerminalEvent(span, resourceAttrs));
2660
2938
  }
2661
2939
  return events;
2662
2940
  }