autotel-terminal 17.0.1 → 17.0.2

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
@@ -8,6 +8,8 @@ var react = require('react');
8
8
  var ink = require('ink');
9
9
  var autotel = require('autotel');
10
10
  var tracerProvider = require('autotel/tracer-provider');
11
+ var ai = require('ai');
12
+ var zod = require('zod');
11
13
  require('autotel/exporters');
12
14
  var jsxRuntime = require('react/jsx-runtime');
13
15
 
@@ -200,7 +202,8 @@ function sortSpansForWaterfall(spans) {
200
202
  }
201
203
  const withDepth = spans.map((s) => ({ span: s, depth: depth(s) }));
202
204
  withDepth.sort((a, b) => {
203
- if (a.span.startTime !== b.span.startTime) return a.span.startTime - b.span.startTime;
205
+ if (a.span.startTime !== b.span.startTime)
206
+ return a.span.startTime - b.span.startTime;
204
207
  return a.depth - b.depth;
205
208
  });
206
209
  return withDepth;
@@ -215,7 +218,9 @@ function filterTraceSummaries(summaries, searchQuery, errorsOnly) {
215
218
  let list = summaries;
216
219
  if (searchQuery.trim() === "") return list;
217
220
  const q = searchQuery.toLowerCase();
218
- return list.filter((s) => s.spans.some((sp) => sp.name.toLowerCase().includes(q)));
221
+ return list.filter(
222
+ (s) => s.spans.some((sp) => sp.name.toLowerCase().includes(q))
223
+ );
219
224
  }
220
225
  function computeStats(spans) {
221
226
  const total = spans.length;
@@ -423,10 +428,11 @@ function getStatusCode(span) {
423
428
  return void 0;
424
429
  }
425
430
  function applySpanFilters(spans, state) {
426
- const { serviceName, route, statusGroup, errorsOnly, searchQuery } = state;
431
+ const { serviceName, route, statusGroup, errorsOnly, searchQuery, traceId } = state;
427
432
  const q = searchQuery?.trim().toLowerCase() ?? "";
428
433
  return spans.filter((span) => {
429
434
  const attrs = span.attributes ?? {};
435
+ if (traceId && span.traceId !== traceId) return false;
430
436
  if (errorsOnly && span.status !== "ERROR") return false;
431
437
  if (serviceName) {
432
438
  const svc = getServiceName(span);
@@ -482,17 +488,17 @@ function getStatusCodeFromSpans(spans) {
482
488
  }
483
489
  function buildErrorSummaries(traceSummaries) {
484
490
  const out = [];
485
- for (const t of traceSummaries) {
486
- const errorSpans = t.spans.filter((s) => s.status === "ERROR");
491
+ for (const t2 of traceSummaries) {
492
+ const errorSpans = t2.spans.filter((s) => s.status === "ERROR");
487
493
  if (errorSpans.length === 0) continue;
488
494
  out.push({
489
- traceId: t.traceId,
490
- rootName: t.rootName,
491
- serviceName: getServiceNameFromSpans(t.spans),
492
- route: getRouteFromSpans(t.spans),
493
- statusCode: getStatusCodeFromSpans(t.spans),
495
+ traceId: t2.traceId,
496
+ rootName: t2.rootName,
497
+ serviceName: getServiceNameFromSpans(t2.spans),
498
+ route: getRouteFromSpans(t2.spans),
499
+ statusCode: getStatusCodeFromSpans(t2.spans),
494
500
  errorCount: errorSpans.length,
495
- lastEndTime: t.lastEndTime
501
+ lastEndTime: t2.lastEndTime
496
502
  });
497
503
  }
498
504
  out.sort((a, b) => b.lastEndTime - a.lastEndTime);
@@ -528,6 +534,302 @@ function exportTraceToJson(trace, logs) {
528
534
  };
529
535
  return JSON.stringify(exported, null, 2);
530
536
  }
537
+
538
+ // src/ai/provider.ts
539
+ async function detectOllama(baseUrl = "http://127.0.0.1:11434") {
540
+ try {
541
+ const res = await fetch(`${baseUrl}/api/tags`, {
542
+ signal: AbortSignal.timeout(1e3)
543
+ });
544
+ return res.ok;
545
+ } catch {
546
+ return false;
547
+ }
548
+ }
549
+ function resolveConfig(options = {}) {
550
+ const provider = options.provider ?? process.env.AI_PROVIDER ?? void 0;
551
+ const model = options.model ?? process.env.AI_MODEL;
552
+ const apiKey = options.apiKey ?? process.env.AI_API_KEY ?? (provider === "openai" ? process.env.OPENAI_API_KEY : void 0);
553
+ const baseUrl = options.baseUrl ?? process.env.AI_BASE_URL;
554
+ if (provider) {
555
+ return {
556
+ provider,
557
+ model: model ?? (provider === "ollama" ? "granite4" : "gpt-4o"),
558
+ apiKey,
559
+ baseUrl
560
+ };
561
+ }
562
+ return null;
563
+ }
564
+ var defaultAutoDetectDeps = { detectOllama };
565
+ async function resolveConfigWithAutoDetect(options = {}, deps = defaultAutoDetectDeps) {
566
+ const config = resolveConfig(options);
567
+ if (config) return config;
568
+ const ollamaUrl = options.baseUrl ?? process.env.AI_BASE_URL ?? "http://127.0.0.1:11434";
569
+ if (await deps.detectOllama(ollamaUrl)) {
570
+ return {
571
+ provider: "ollama",
572
+ model: options.model ?? process.env.AI_MODEL ?? "granite4",
573
+ baseUrl: ollamaUrl
574
+ };
575
+ }
576
+ const openaiKey = options.apiKey ?? process.env.AI_API_KEY ?? process.env.OPENAI_API_KEY;
577
+ if (openaiKey) {
578
+ return {
579
+ provider: "openai",
580
+ model: options.model ?? process.env.AI_MODEL ?? "gpt-4o",
581
+ apiKey: openaiKey
582
+ };
583
+ }
584
+ return null;
585
+ }
586
+ async function createAIModel(config) {
587
+ switch (config.provider) {
588
+ case "ollama": {
589
+ const { createOllama } = await import('ai-sdk-ollama');
590
+ const ollama = createOllama({
591
+ baseURL: config.baseUrl ?? "http://127.0.0.1:11434"
592
+ });
593
+ return { model: ollama(config.model), providerType: "ollama", config };
594
+ }
595
+ case "openai": {
596
+ const { createOpenAI } = await import('@ai-sdk/openai');
597
+ const openai = createOpenAI({
598
+ apiKey: config.apiKey,
599
+ ...config.baseUrl ? { baseURL: config.baseUrl } : {}
600
+ });
601
+ return { model: openai(config.model), providerType: "openai", config };
602
+ }
603
+ case "openai-compatible": {
604
+ const { createOpenAICompatible } = await import('@ai-sdk/openai-compatible');
605
+ const provider = createOpenAICompatible({
606
+ baseURL: config.baseUrl ?? "http://127.0.0.1:11434/v1",
607
+ name: "custom",
608
+ ...config.apiKey ? { headers: { Authorization: `Bearer ${config.apiKey}` } } : {}
609
+ });
610
+ return {
611
+ model: provider(config.model),
612
+ providerType: "openai-compatible",
613
+ config
614
+ };
615
+ }
616
+ default: {
617
+ throw new Error(
618
+ `Unsupported provider: "${config.provider}". Expected "ollama", "openai", or "openai-compatible".`
619
+ );
620
+ }
621
+ }
622
+ }
623
+
624
+ // src/ai/system-prompt.ts
625
+ function buildSystemPrompt(viewMode, contextJson) {
626
+ return `You are an OpenTelemetry expert assistant analyzing live telemetry data from a running application.
627
+ The user is viewing their ${viewMode} dashboard in a terminal TUI.
628
+
629
+ You have tools to query the telemetry data precisely. Use them to answer questions:
630
+ - getOverviewStats: high-level stats (spans, errors, latency)
631
+ - listServices: all services with error rates and p95
632
+ - findSlowestSpans: find slow spans, optionally by service
633
+ - findErrorTraces: find traces with errors
634
+ - getTraceDetail: deep dive into a specific trace
635
+ - searchSpans: search spans by name
636
+ - searchLogs: search logs by message content
637
+
638
+ Use tools first to gather data, then synthesize a concise answer.
639
+ Keep responses under 300 words.
640
+ Use specific span names, durations, and attribute values from the data.
641
+ Format for a narrow terminal column \u2014 use short paragraphs, not wide tables.
642
+
643
+ Current dashboard summary:
644
+ ${contextJson}`;
645
+ }
646
+ var t = ai.tool;
647
+ function createTelemetryTools(ctx) {
648
+ return {
649
+ getOverviewStats: t({
650
+ description: "Get high-level stats: total spans, error count, average duration, p95 duration, and service count.",
651
+ parameters: zod.z.object({}),
652
+ execute: async () => ({
653
+ totalSpans: ctx.stats.total,
654
+ errors: ctx.stats.errors,
655
+ avgMs: Math.round(ctx.stats.avg),
656
+ p95Ms: Math.round(ctx.stats.p95),
657
+ serviceCount: ctx.serviceStats.length,
658
+ traceCount: ctx.traces.length
659
+ })
660
+ }),
661
+ listServices: t({
662
+ description: "List all services with their span counts, error counts, and p95 latencies.",
663
+ parameters: zod.z.object({}),
664
+ execute: async () => ctx.serviceStats.map((s) => ({
665
+ serviceName: s.serviceName,
666
+ total: s.total,
667
+ errors: s.errors,
668
+ avgMs: Math.round(s.avgMs),
669
+ p95Ms: Math.round(s.p95Ms)
670
+ }))
671
+ }),
672
+ findSlowestSpans: t({
673
+ description: "Find the slowest spans, optionally filtered by service name. Returns span name, duration, status, and key attributes.",
674
+ parameters: zod.z.object({
675
+ service: zod.z.string().optional().describe("Filter by service name"),
676
+ limit: zod.z.number().optional().describe("Max results (default 10)")
677
+ }),
678
+ execute: async ({
679
+ service,
680
+ limit
681
+ }) => {
682
+ const max = limit ?? 10;
683
+ let filtered = ctx.spans;
684
+ if (service) {
685
+ filtered = filtered.filter(
686
+ (s) => s.attributes?.["service.name"] === service
687
+ );
688
+ }
689
+ return filtered.toSorted((a, b) => b.durationMs - a.durationMs).slice(0, max).map((s) => ({
690
+ name: s.name,
691
+ durationMs: Math.round(s.durationMs),
692
+ status: s.status,
693
+ service: s.attributes?.["service.name"] ?? "unknown",
694
+ route: s.attributes?.["http.route"],
695
+ traceId: s.traceId.slice(0, 8)
696
+ }));
697
+ }
698
+ }),
699
+ findErrorTraces: t({
700
+ description: "Find traces that contain errors, with root span name, service, route, and error count.",
701
+ parameters: zod.z.object({
702
+ service: zod.z.string().optional().describe("Filter by service name"),
703
+ limit: zod.z.number().optional().describe("Max results (default 10)")
704
+ }),
705
+ execute: async ({
706
+ service,
707
+ limit
708
+ }) => {
709
+ const max = limit ?? 10;
710
+ let errors = ctx.errorSummaries;
711
+ if (service) {
712
+ errors = errors.filter((e) => e.serviceName === service);
713
+ }
714
+ return errors.slice(0, max).map((e) => ({
715
+ traceId: e.traceId.slice(0, 8),
716
+ rootName: e.rootName,
717
+ serviceName: e.serviceName,
718
+ route: e.route,
719
+ errorCount: e.errorCount
720
+ }));
721
+ }
722
+ }),
723
+ getTraceDetail: t({
724
+ description: "Get full detail of a specific trace by trace ID prefix. Returns all spans with their parent relationships, durations, and attributes.",
725
+ parameters: zod.z.object({
726
+ traceIdPrefix: zod.z.string().describe("First 8+ characters of the trace ID")
727
+ }),
728
+ execute: async ({ traceIdPrefix }) => {
729
+ const trace = ctx.traces.find(
730
+ (t2) => t2.traceId.startsWith(traceIdPrefix)
731
+ );
732
+ if (!trace) {
733
+ return { error: `No trace found matching ${traceIdPrefix}` };
734
+ }
735
+ const traceLogs = ctx.logs.filter((l) => l.traceId === trace.traceId);
736
+ return {
737
+ traceId: trace.traceId.slice(0, 16),
738
+ rootName: trace.rootName,
739
+ durationMs: Math.round(trace.durationMs),
740
+ hasError: trace.hasError,
741
+ spanCount: trace.spanCount,
742
+ spans: trace.spans.map((s) => ({
743
+ name: s.name,
744
+ durationMs: Math.round(s.durationMs),
745
+ status: s.status,
746
+ kind: s.kind,
747
+ parentSpanId: s.parentSpanId?.slice(0, 8),
748
+ attrs: Object.fromEntries(
749
+ Object.entries(s.attributes ?? {}).filter(
750
+ ([k]) => [
751
+ "http.method",
752
+ "http.route",
753
+ "http.status_code",
754
+ "db.operation",
755
+ "db.system",
756
+ "service.name",
757
+ "error.message",
758
+ "error.type"
759
+ ].includes(k)
760
+ )
761
+ )
762
+ })),
763
+ logs: traceLogs.slice(0, 10).map((l) => ({
764
+ level: l.level,
765
+ message: l.message.slice(0, 100)
766
+ }))
767
+ };
768
+ }
769
+ }),
770
+ searchSpans: t({
771
+ description: "Search spans by name pattern (case-insensitive substring match). Returns matching spans with details.",
772
+ parameters: zod.z.object({
773
+ query: zod.z.string().describe("Search string to match against span names"),
774
+ limit: zod.z.number().optional().describe("Max results (default 20)")
775
+ }),
776
+ execute: async ({ query, limit }) => {
777
+ const max = limit ?? 20;
778
+ const q = query.toLowerCase();
779
+ return ctx.spans.filter((s) => s.name.toLowerCase().includes(q)).slice(0, max).map((s) => ({
780
+ name: s.name,
781
+ durationMs: Math.round(s.durationMs),
782
+ status: s.status,
783
+ traceId: s.traceId.slice(0, 8),
784
+ service: s.attributes?.["service.name"] ?? "unknown"
785
+ }));
786
+ }
787
+ }),
788
+ searchLogs: t({
789
+ description: "Search logs by message content (case-insensitive). Returns matching log entries.",
790
+ parameters: zod.z.object({
791
+ query: zod.z.string().describe("Search string to match against log messages"),
792
+ level: zod.z.enum(["debug", "info", "warn", "error"]).optional().describe("Filter by log level"),
793
+ limit: zod.z.number().optional().describe("Max results (default 20)")
794
+ }),
795
+ execute: async ({
796
+ query,
797
+ level,
798
+ limit
799
+ }) => {
800
+ const max = limit ?? 20;
801
+ const q = query.toLowerCase();
802
+ let filtered = ctx.logs.filter(
803
+ (l) => l.message.toLowerCase().includes(q)
804
+ );
805
+ if (level) {
806
+ filtered = filtered.filter((l) => l.level === level);
807
+ }
808
+ return filtered.slice(0, max).map((l) => ({
809
+ level: l.level,
810
+ message: l.message.slice(0, 200),
811
+ traceId: l.traceId?.slice(0, 8),
812
+ attrs: l.attributes
813
+ }));
814
+ }
815
+ })
816
+ };
817
+ }
818
+
819
+ // src/ai/stream.ts
820
+ async function providerStreamText(providerType, params) {
821
+ if (providerType === "ollama") {
822
+ const mod2 = await import('ai-sdk-ollama');
823
+ const result = await mod2.streamText(
824
+ params
825
+ );
826
+ return result;
827
+ }
828
+ const mod = await import('ai');
829
+ return mod.streamText(
830
+ params
831
+ );
832
+ }
531
833
  var KEY_ATTR_KEYS = /* @__PURE__ */ new Set([
532
834
  "http.route",
533
835
  "http.method",
@@ -550,7 +852,8 @@ function Dashboard({
550
852
  maxSpans,
551
853
  colors,
552
854
  stream,
553
- logStream
855
+ logStream,
856
+ aiConfig
554
857
  }) {
555
858
  const [paused, setPaused] = react.useState(false);
556
859
  const [recording, setRecording] = react.useState(false);
@@ -563,13 +866,42 @@ function Dashboard({
563
866
  const [spanFilters, setSpanFilters] = react.useState({
564
867
  statusGroup: "all"
565
868
  });
566
- const [selectedTraceId, setSelectedTraceId] = react.useState(null);
567
- const [selectedSpanIndex, setSelectedSpanIndex] = react.useState(0);
869
+ const [drilldownTraceId, setDrilldownTraceId] = react.useState(null);
870
+ const [drilldownSelectedIndex, setDrilldownSelectedIndex] = react.useState(0);
871
+ const [drilldownTab, setDrilldownTab] = react.useState("timeline");
568
872
  const [newErrorCount, setNewErrorCount] = react.useState(0);
569
873
  const [searchMode, setSearchMode] = react.useState(false);
874
+ const [traceIdMode, setTraceIdMode] = react.useState(false);
875
+ const [traceIdInput, setTraceIdInput] = react.useState("");
570
876
  const throttleRef = react.useRef(null);
571
877
  const pendingSpansRef = react.useRef([]);
572
878
  const [logs, setLogs] = react.useState([]);
879
+ const [aiActive, setAiActive] = react.useState(false);
880
+ const [aiMessages, setAiMessages] = react.useState([]);
881
+ const [aiInput, setAiInput] = react.useState("");
882
+ const [aiState, setAiState] = react.useState({ status: "unconfigured" });
883
+ const [aiInputMode, setAiInputMode] = react.useState(false);
884
+ const aiModelRef = react.useRef(null);
885
+ const aiAbortRef = react.useRef(null);
886
+ react.useEffect(() => {
887
+ let cancelled = false;
888
+ resolveConfigWithAutoDetect(aiConfig).then(async (config) => {
889
+ if (cancelled || !config) return;
890
+ try {
891
+ const result = await createAIModel(config);
892
+ aiModelRef.current = result;
893
+ setAiState({ status: "idle" });
894
+ } catch {
895
+ setAiState({
896
+ status: "error",
897
+ message: "Failed to initialize AI model"
898
+ });
899
+ }
900
+ });
901
+ return () => {
902
+ cancelled = true;
903
+ };
904
+ }, [aiConfig]);
573
905
  react.useEffect(() => {
574
906
  if (!logStream) return;
575
907
  const unsubscribe = logStream.onLog((event) => {
@@ -601,13 +933,16 @@ function Dashboard({
601
933
  return next.slice(0, maxSpans);
602
934
  });
603
935
  setSelected(0);
604
- setSelectedTraceId(null);
936
+ setDrilldownTraceId(null);
605
937
  };
606
938
  const unsubscribe = stream.onSpanEnd((span) => {
607
939
  if (paused) return;
608
940
  if (span.status === "ERROR") {
609
941
  setNewErrorCount((n) => n + 1);
610
- setTimeout(() => setNewErrorCount((n) => Math.max(0, n - 1)), NEW_ERROR_DISPLAY_MS);
942
+ setTimeout(
943
+ () => setNewErrorCount((n) => Math.max(0, n - 1)),
944
+ NEW_ERROR_DISPLAY_MS
945
+ );
611
946
  }
612
947
  if (recording) {
613
948
  setSpans((prev) => {
@@ -619,7 +954,7 @@ function Dashboard({
619
954
  return next.slice(0, RECORD_LIMIT_DEFAULT);
620
955
  });
621
956
  setSelected(0);
622
- setSelectedTraceId(null);
957
+ setDrilldownTraceId(null);
623
958
  return;
624
959
  }
625
960
  pendingSpansRef.current = [span, ...pendingSpansRef.current];
@@ -650,21 +985,24 @@ function Dashboard({
650
985
  () => buildTraceSummaries(traceMap),
651
986
  [traceMap]
652
987
  );
653
- const filteredSummaries = react.useMemo(
654
- () => filterTraceSummaries(traceSummaries, ""),
655
- [traceSummaries]
656
- );
988
+ const filteredSummaries = react.useMemo(() => {
989
+ const base = filterTraceSummaries(traceSummaries, "");
990
+ return spanFilters.traceId ? base.filter((t2) => t2.traceId === spanFilters.traceId) : base;
991
+ }, [traceSummaries, spanFilters.traceId]);
657
992
  const filteredSpans = react.useMemo(
658
993
  () => filterBySearch(filteredSpanBuffer, ""),
659
994
  [filteredSpanBuffer]
660
995
  );
661
996
  const stats = react.useMemo(() => computeStats(spans), [spans]);
662
- const perSpanNameStats = react.useMemo(() => computePerSpanNameStats(spans), [spans]);
663
- const logStats = react.useMemo(() => computeLogStats(logs), [logs]);
664
- const filteredLogs = react.useMemo(
665
- () => filterLogsBySearch(logs, searchQuery),
666
- [logs, searchQuery]
997
+ const perSpanNameStats = react.useMemo(
998
+ () => computePerSpanNameStats(spans),
999
+ [spans]
667
1000
  );
1001
+ const logStats = react.useMemo(() => computeLogStats(logs), [logs]);
1002
+ const filteredLogs = react.useMemo(() => {
1003
+ const traceFilteredLogs = spanFilters.traceId ? logs.filter((l) => l.traceId === spanFilters.traceId) : logs;
1004
+ return filterLogsBySearch(traceFilteredLogs, searchQuery);
1005
+ }, [logs, searchQuery, spanFilters.traceId]);
668
1006
  const serviceStats = react.useMemo(() => computeServiceStats(spans), [spans]);
669
1007
  const selectedServiceName = serviceStats[selected]?.serviceName ?? null;
670
1008
  const spansForSelectedService = react.useMemo(() => {
@@ -681,24 +1019,147 @@ function Dashboard({
681
1019
  () => findHotSpanNames(spansForSelectedService, 8),
682
1020
  [spansForSelectedService]
683
1021
  );
684
- const selectedTraceSummary = selectedTraceId == null ? filteredSummaries[selected] ?? null : filteredSummaries.find((t) => t.traceId === selectedTraceId) ?? null;
1022
+ const selectedTraceSummary = drilldownTraceId == null ? filteredSummaries[selected] ?? null : filteredSummaries.find((t2) => t2.traceId === drilldownTraceId) ?? null;
685
1023
  const errorSummaries = react.useMemo(
686
1024
  () => buildErrorSummaries(traceSummaries),
687
1025
  [traceSummaries]
688
1026
  );
1027
+ const filteredErrorSummaries = spanFilters.traceId ? errorSummaries.filter((e) => e.traceId === spanFilters.traceId) : errorSummaries;
689
1028
  const traceTree = selectedTraceSummary == null ? [] : flattenTraceTree(buildTraceTree(selectedTraceSummary.spans));
690
1029
  const waterfallSpans = selectedTraceSummary == null ? [] : sortSpansForWaterfall(selectedTraceSummary.spans);
691
- const currentSpanInTrace = traceTree[selectedSpanIndex] ?? null;
1030
+ const currentSpanInTrace = traceTree[drilldownSelectedIndex] ?? null;
692
1031
  const currentSpanInFlat = filteredSpans[selected] ?? null;
693
- const selectedTraceSummaryForDetails = viewMode === "trace" && selectedTraceId == null && filteredSummaries[selected] ? filteredSummaries[selected] : null;
1032
+ const selectedTraceSummaryForDetails = viewMode === "trace" && drilldownTraceId == null && filteredSummaries[selected] ? filteredSummaries[selected] : null;
694
1033
  const rootSpanOfSelectedTrace = selectedTraceSummaryForDetails != null && selectedTraceSummaryForDetails.spans.length > 0 ? selectedTraceSummaryForDetails.spans.find(
695
1034
  (s) => !selectedTraceSummaryForDetails.spans.some(
696
1035
  (p) => p.spanId === s.parentSpanId
697
1036
  )
698
1037
  ) ?? selectedTraceSummaryForDetails.spans[0] : null;
699
- const currentSpan = viewMode === "trace" ? selectedTraceId == null ? rootSpanOfSelectedTrace ?? null : currentSpanInTrace?.span ?? null : viewMode === "span" ? currentSpanInFlat : null;
1038
+ const currentSpan = viewMode === "trace" ? drilldownTraceId == null ? rootSpanOfSelectedTrace ?? null : currentSpanInTrace?.span ?? null : viewMode === "span" ? currentSpanInFlat : null;
700
1039
  const selectedTraceLogs = selectedTraceSummary?.traceId && logs.length > 0 ? logs.filter((l) => l.traceId === selectedTraceSummary.traceId) : [];
701
1040
  const timelineItems = selectedTraceSummary && (selectedTraceSummary.spans.length > 0 || selectedTraceLogs.length > 0) ? buildTraceTimeline(selectedTraceSummary.spans, selectedTraceLogs) : [];
1041
+ const drilldownSpans = react.useMemo(
1042
+ () => drilldownTraceId ? spans.filter((s) => s.traceId === drilldownTraceId) : [],
1043
+ [spans, drilldownTraceId]
1044
+ );
1045
+ const drilldownTree = react.useMemo(
1046
+ () => drilldownSpans.length > 0 ? flattenTraceTree(buildTraceTree(drilldownSpans)) : [],
1047
+ [drilldownSpans]
1048
+ );
1049
+ const drilldownLogs = react.useMemo(
1050
+ () => drilldownTraceId ? logs.filter((l) => l.traceId === drilldownTraceId) : [],
1051
+ [logs, drilldownTraceId]
1052
+ );
1053
+ const drilldownTimeline = react.useMemo(
1054
+ () => drilldownSpans.length > 0 || drilldownLogs.length > 0 ? buildTraceTimeline(drilldownSpans, drilldownLogs) : [],
1055
+ [drilldownSpans, drilldownLogs]
1056
+ );
1057
+ const drilldownSummary = react.useMemo(
1058
+ () => drilldownTraceId ? traceSummaries.find((t2) => t2.traceId === drilldownTraceId) ?? null : null,
1059
+ [traceSummaries, drilldownTraceId]
1060
+ );
1061
+ const drilldownSelectedItem = react.useMemo(() => {
1062
+ if (!drilldownTraceId) return null;
1063
+ if (drilldownTab === "timeline") {
1064
+ const item = drilldownTimeline[drilldownSelectedIndex];
1065
+ if (!item) return null;
1066
+ return item;
1067
+ }
1068
+ if (drilldownTab === "spans") {
1069
+ return drilldownTree[drilldownSelectedIndex] ? {
1070
+ type: "span",
1071
+ span: drilldownTree[drilldownSelectedIndex].span
1072
+ } : null;
1073
+ }
1074
+ if (drilldownTab === "logs") {
1075
+ return drilldownLogs[drilldownSelectedIndex] ? { type: "log", log: drilldownLogs[drilldownSelectedIndex] } : null;
1076
+ }
1077
+ return null;
1078
+ }, [
1079
+ drilldownTraceId,
1080
+ drilldownTab,
1081
+ drilldownSelectedIndex,
1082
+ drilldownTimeline,
1083
+ drilldownTree,
1084
+ drilldownLogs
1085
+ ]);
1086
+ const sendAIQuery = async (question) => {
1087
+ const aiResult = aiModelRef.current;
1088
+ if (!aiResult || aiState.status === "streaming") return;
1089
+ const userMsg = { role: "user", content: question };
1090
+ setAiMessages((prev) => [...prev, userMsg]);
1091
+ setAiInput("");
1092
+ const abort = new AbortController();
1093
+ aiAbortRef.current = abort;
1094
+ setAiState({ status: "streaming", abortController: abort });
1095
+ const toolCtx = {
1096
+ spans,
1097
+ logs,
1098
+ traces: traceSummaries,
1099
+ stats,
1100
+ serviceStats,
1101
+ errorSummaries
1102
+ };
1103
+ const tools = createTelemetryTools(toolCtx);
1104
+ const statsContext = JSON.stringify({
1105
+ viewMode,
1106
+ stats: {
1107
+ totalSpans: stats.total,
1108
+ errors: stats.errors,
1109
+ avgMs: Math.round(stats.avg),
1110
+ p95Ms: Math.round(stats.p95)
1111
+ },
1112
+ services: serviceStats.length,
1113
+ traces: traceSummaries.length
1114
+ });
1115
+ const drilldownContext = drilldownTraceId ? `
1116
+
1117
+ Currently viewing trace ${drilldownTraceId}. This trace has ${drilldownSpans.length} spans and ${drilldownLogs.length} logs. The root span is "${drilldownSummary?.rootName ?? "unknown"}" with duration ${drilldownSummary ? formatDurationMs(drilldownSummary.durationMs) : "unknown"}.` : "";
1118
+ const systemPrompt = buildSystemPrompt(viewMode, statsContext) + drilldownContext;
1119
+ try {
1120
+ setAiMessages((prev) => [...prev, { role: "assistant", content: "" }]);
1121
+ const result = await providerStreamText(aiResult.providerType, {
1122
+ model: aiResult.model,
1123
+ system: systemPrompt,
1124
+ messages: [...aiMessages, userMsg].map((m) => ({
1125
+ role: m.role,
1126
+ content: m.content
1127
+ })),
1128
+ tools,
1129
+ maxSteps: 10,
1130
+ abortSignal: abort.signal
1131
+ });
1132
+ let fullText = "";
1133
+ for await (const chunk of result.textStream) {
1134
+ if (abort.signal.aborted) break;
1135
+ fullText += chunk;
1136
+ const captured = fullText;
1137
+ setAiMessages((prev) => {
1138
+ const updated = [...prev];
1139
+ const lastMsg = updated.at(-1);
1140
+ if (lastMsg?.role === "assistant") {
1141
+ updated[updated.length - 1] = {
1142
+ role: "assistant",
1143
+ content: captured
1144
+ };
1145
+ }
1146
+ return updated;
1147
+ });
1148
+ }
1149
+ setAiState({ status: "idle" });
1150
+ } catch (error) {
1151
+ if (abort.signal.aborted) {
1152
+ setAiState({ status: "idle" });
1153
+ return;
1154
+ }
1155
+ setAiState({
1156
+ status: "error",
1157
+ message: error instanceof Error ? error.message : String(error)
1158
+ });
1159
+ } finally {
1160
+ aiAbortRef.current = null;
1161
+ }
1162
+ };
702
1163
  const { isRawModeSupported } = ink.useStdin();
703
1164
  ink.useInput(
704
1165
  (input, key) => {
@@ -716,53 +1177,171 @@ function Dashboard({
716
1177
  return;
717
1178
  }
718
1179
  if (searchMode) {
719
- if (key.backspace || key.delete) {
720
- setSearchQuery((q) => q.slice(0, -1));
721
- } else if (key.return) {
1180
+ if (key.escape || key.return) {
722
1181
  setSearchMode(false);
1182
+ } else if (key.backspace || key.delete) {
1183
+ setSearchQuery((q) => q.slice(0, -1));
1184
+ } else if (key.tab) {
1185
+ if (searchQuery.length >= 4) {
1186
+ const match = traceSummaries.find(
1187
+ (t2) => t2.traceId.toLowerCase().startsWith(searchQuery.toLowerCase())
1188
+ );
1189
+ if (match) {
1190
+ setSpanFilters((prev) => ({ ...prev, traceId: match.traceId }));
1191
+ setSearchMode(false);
1192
+ setSearchQuery("");
1193
+ }
1194
+ }
723
1195
  } else if (input && input.length === 1 && !key.ctrl && !key.meta) {
724
1196
  setSearchQuery((q) => q + input);
725
1197
  }
726
1198
  return;
727
1199
  }
728
- if (key.escape) {
729
- if (viewMode === "trace" && selectedTraceId != null) {
730
- setSelectedTraceId(null);
731
- setSelectedSpanIndex(0);
1200
+ if (traceIdMode) {
1201
+ if (key.escape) {
1202
+ setTraceIdMode(false);
1203
+ setTraceIdInput("");
1204
+ } else if (key.return) {
1205
+ if (traceIdInput.trim()) {
1206
+ const match = traceSummaries.find(
1207
+ (t2) => t2.traceId.toLowerCase().startsWith(traceIdInput.toLowerCase())
1208
+ );
1209
+ if (match) {
1210
+ setSpanFilters((prev) => ({ ...prev, traceId: match.traceId }));
1211
+ }
1212
+ } else {
1213
+ setSpanFilters((prev) => {
1214
+ const { traceId: _, ...rest } = prev;
1215
+ return rest;
1216
+ });
1217
+ }
1218
+ setTraceIdMode(false);
1219
+ setTraceIdInput("");
1220
+ } else if (key.tab) {
1221
+ if (traceIdInput.length >= 2) {
1222
+ const match = traceSummaries.find(
1223
+ (t2) => t2.traceId.toLowerCase().startsWith(traceIdInput.toLowerCase())
1224
+ );
1225
+ if (match) {
1226
+ setTraceIdInput(match.traceId);
1227
+ }
1228
+ }
1229
+ } else if (key.backspace || key.delete) {
1230
+ setTraceIdInput((q) => q.slice(0, -1));
1231
+ } else if (input && input.length === 1 && !key.ctrl && !key.meta) {
1232
+ setTraceIdInput((q) => q + input);
1233
+ }
1234
+ return;
1235
+ }
1236
+ if (aiInputMode) {
1237
+ if (key.escape) {
1238
+ if (aiState.status === "streaming") {
1239
+ aiAbortRef.current?.abort();
1240
+ } else {
1241
+ setAiInputMode(false);
1242
+ setAiActive(false);
1243
+ }
1244
+ return;
1245
+ }
1246
+ if (key.backspace || key.delete) {
1247
+ setAiInput((q) => q.slice(0, -1));
1248
+ return;
1249
+ }
1250
+ if (key.return) {
1251
+ if (aiInput.trim()) {
1252
+ sendAIQuery(aiInput.trim());
1253
+ }
1254
+ return;
1255
+ }
1256
+ if (input && input.length === 1 && !key.ctrl && !key.meta) {
1257
+ setAiInput((q) => q + input);
1258
+ }
1259
+ return;
1260
+ }
1261
+ if (input === "a") {
1262
+ setAiActive((v) => !v);
1263
+ if (aiActive) {
1264
+ setAiInputMode(false);
732
1265
  } else {
1266
+ if (aiState.status !== "unconfigured") {
1267
+ setAiInputMode(true);
1268
+ }
1269
+ }
1270
+ return;
1271
+ }
1272
+ if (key.escape) {
1273
+ if (drilldownTraceId == null) {
733
1274
  setSearchMode(false);
1275
+ } else {
1276
+ setDrilldownTraceId(null);
1277
+ setDrilldownSelectedIndex(0);
1278
+ setDrilldownTab("timeline");
734
1279
  }
735
1280
  return;
736
1281
  }
737
- if (key.return && viewMode === "trace" && selectedTraceId == null && filteredSummaries[selected]) {
738
- setSelectedTraceId(filteredSummaries[selected].traceId);
739
- setSelectedSpanIndex(0);
1282
+ if (key.tab && drilldownTraceId != null) {
1283
+ const tabs = [
1284
+ "timeline",
1285
+ "spans",
1286
+ "logs"
1287
+ ];
1288
+ const currentIdx = tabs.indexOf(drilldownTab);
1289
+ const nextIdx = key.shift ? (currentIdx - 1 + tabs.length) % tabs.length : (currentIdx + 1) % tabs.length;
1290
+ setDrilldownTab(tabs[nextIdx]);
1291
+ setDrilldownSelectedIndex(0);
740
1292
  return;
741
1293
  }
1294
+ if (key.return && drilldownTraceId == null) {
1295
+ let targetTraceId;
1296
+ let preSelectIndex = 0;
1297
+ if (viewMode === "trace" && filteredSummaries[selected]) {
1298
+ targetTraceId = filteredSummaries[selected].traceId;
1299
+ } else if (viewMode === "log" && filteredLogs[selected]?.traceId) {
1300
+ targetTraceId = filteredLogs[selected].traceId;
1301
+ const originLog = filteredLogs[selected];
1302
+ const traceSpans = spans.filter((s) => s.traceId === targetTraceId);
1303
+ const traceLogs = logs.filter((l) => l.traceId === targetTraceId);
1304
+ const timeline = buildTraceTimeline(traceSpans, traceLogs);
1305
+ preSelectIndex = timeline.findIndex(
1306
+ (item) => item.type === "log" && item.log === originLog
1307
+ );
1308
+ if (preSelectIndex < 0) preSelectIndex = 0;
1309
+ } else if (viewMode === "span" && filteredSpans[selected]?.traceId) {
1310
+ targetTraceId = filteredSpans[selected].traceId;
1311
+ const originSpan = filteredSpans[selected];
1312
+ const traceSpans = spans.filter((s) => s.traceId === targetTraceId);
1313
+ const traceLogs = logs.filter((l) => l.traceId === targetTraceId);
1314
+ const timeline = buildTraceTimeline(traceSpans, traceLogs);
1315
+ preSelectIndex = timeline.findIndex(
1316
+ (item) => item.type === "span" && item.span?.spanId === originSpan.spanId
1317
+ );
1318
+ if (preSelectIndex < 0) preSelectIndex = 0;
1319
+ }
1320
+ if (targetTraceId) {
1321
+ setDrilldownTraceId(targetTraceId);
1322
+ setDrilldownSelectedIndex(preSelectIndex);
1323
+ setDrilldownTab("timeline");
1324
+ return;
1325
+ }
1326
+ }
742
1327
  if (key.upArrow || key.downArrow) {
1328
+ if (drilldownTraceId != null) {
1329
+ const listLength = drilldownTab === "timeline" ? drilldownTimeline.length : drilldownTab === "spans" ? drilldownTree.length : drilldownLogs.length;
1330
+ if (key.upArrow) {
1331
+ setDrilldownSelectedIndex((i) => Math.max(0, i - 1));
1332
+ } else {
1333
+ setDrilldownSelectedIndex((i) => Math.min(listLength - 1, i + 1));
1334
+ }
1335
+ return;
1336
+ }
743
1337
  switch (viewMode) {
744
1338
  case "trace": {
745
1339
  if (key.upArrow) {
746
- if (selectedTraceId != null && traceTree.length > 0) {
747
- setSelectedSpanIndex((i) => Math.max(0, i - 1));
748
- } else {
749
- setSelected((i) => Math.max(0, i - 1));
750
- setSelectedSpanIndex(0);
751
- }
1340
+ setSelected((i) => Math.max(0, i - 1));
1341
+ setDrilldownSelectedIndex(0);
752
1342
  } else if (key.downArrow) {
753
- if (selectedTraceId != null && selectedSpanIndex < traceTree.length - 1) {
754
- setSelectedSpanIndex((i) => Math.min(traceTree.length - 1, i + 1));
755
- } else if (selectedTraceId != null && traceTree.length > 0 && selectedSpanIndex >= traceTree.length - 1) {
756
- const nextIdx = filteredSummaries.findIndex((t) => t.traceId === selectedTraceId) + 1;
757
- if (nextIdx < filteredSummaries.length) {
758
- setSelected(nextIdx);
759
- setSelectedTraceId(filteredSummaries[nextIdx].traceId);
760
- setSelectedSpanIndex(0);
761
- }
762
- } else if (selectedTraceId == null) {
763
- setSelected((i) => Math.min(filteredSummaries.length - 1, i + 1));
764
- setSelectedSpanIndex(0);
765
- }
1343
+ setSelected((i) => Math.min(filteredSummaries.length - 1, i + 1));
1344
+ setDrilldownSelectedIndex(0);
766
1345
  }
767
1346
  break;
768
1347
  }
@@ -794,7 +1373,9 @@ function Dashboard({
794
1373
  if (key.upArrow) {
795
1374
  setSelected((i) => Math.max(0, i - 1));
796
1375
  } else if (key.downArrow) {
797
- setSelected((i) => Math.min(errorSummaries.length - 1, i + 1));
1376
+ setSelected(
1377
+ (i) => Math.min(filteredErrorSummaries.length - 1, i + 1)
1378
+ );
798
1379
  }
799
1380
  break;
800
1381
  }
@@ -805,35 +1386,35 @@ function Dashboard({
805
1386
  if (input === "t") {
806
1387
  setViewMode((m) => m === "trace" ? "span" : "trace");
807
1388
  setSelected(0);
808
- setSelectedTraceId(null);
809
- setSelectedSpanIndex(0);
1389
+ setDrilldownTraceId(null);
1390
+ setDrilldownSelectedIndex(0);
810
1391
  }
811
1392
  if (input === "l") {
812
1393
  setViewMode((m) => m === "log" ? "trace" : "log");
813
1394
  setSelected(0);
814
- setSelectedTraceId(null);
815
- setSelectedSpanIndex(0);
1395
+ setDrilldownTraceId(null);
1396
+ setDrilldownSelectedIndex(0);
816
1397
  }
817
1398
  if (input === "v") {
818
1399
  setViewMode(
819
1400
  (m) => m === "service-summary" ? "trace" : "service-summary"
820
1401
  );
821
1402
  setSelected(0);
822
- setSelectedTraceId(null);
823
- setSelectedSpanIndex(0);
1403
+ setDrilldownTraceId(null);
1404
+ setDrilldownSelectedIndex(0);
824
1405
  }
825
1406
  if (input === "E") {
826
1407
  setViewMode((m) => m === "errors" ? "trace" : "errors");
827
1408
  setSelected(0);
828
- setSelectedTraceId(null);
829
- setSelectedSpanIndex(0);
1409
+ setDrilldownTraceId(null);
1410
+ setDrilldownSelectedIndex(0);
830
1411
  }
831
1412
  if (input === "c") {
832
1413
  setSpans([]);
833
1414
  setLogs([]);
834
1415
  setSelected(0);
835
- setSelectedTraceId(null);
836
- setSelectedSpanIndex(0);
1416
+ setDrilldownTraceId(null);
1417
+ setDrilldownSelectedIndex(0);
837
1418
  setNewErrorCount(0);
838
1419
  setSpanFilters({ statusGroup: "all" });
839
1420
  setRecording(false);
@@ -843,8 +1424,8 @@ function Dashboard({
843
1424
  setSpans([]);
844
1425
  setLogs([]);
845
1426
  setSelected(0);
846
- setSelectedTraceId(null);
847
- setSelectedSpanIndex(0);
1427
+ setDrilldownTraceId(null);
1428
+ setDrilldownSelectedIndex(0);
848
1429
  setNewErrorCount(0);
849
1430
  setSpanFilters({ statusGroup: "all" });
850
1431
  setPaused(false);
@@ -853,8 +1434,13 @@ function Dashboard({
853
1434
  if (input === "x") {
854
1435
  setSpanFilters({ statusGroup: "all" });
855
1436
  setSelected(0);
856
- setSelectedTraceId(null);
857
- setSelectedSpanIndex(0);
1437
+ setDrilldownTraceId(null);
1438
+ setDrilldownSelectedIndex(0);
1439
+ }
1440
+ if (input === "f") {
1441
+ setTraceIdMode(true);
1442
+ setTraceIdInput(spanFilters.traceId ?? "");
1443
+ return;
858
1444
  }
859
1445
  if (input === "H") {
860
1446
  setSpanFilters((prev) => {
@@ -862,16 +1448,16 @@ function Dashboard({
862
1448
  return { ...prev, statusGroup: next };
863
1449
  });
864
1450
  setSelected(0);
865
- setSelectedTraceId(null);
866
- setSelectedSpanIndex(0);
1451
+ setDrilldownTraceId(null);
1452
+ setDrilldownSelectedIndex(0);
867
1453
  }
868
1454
  if (input === "S") {
869
1455
  const svc = currentSpan?.attributes?.["service.name"];
870
1456
  if (typeof svc === "string" && svc.trim()) {
871
1457
  setSpanFilters((prev) => ({ ...prev, serviceName: svc }));
872
1458
  setSelected(0);
873
- setSelectedTraceId(null);
874
- setSelectedSpanIndex(0);
1459
+ setDrilldownTraceId(null);
1460
+ setDrilldownSelectedIndex(0);
875
1461
  }
876
1462
  }
877
1463
  if (input === "R") {
@@ -879,14 +1465,74 @@ function Dashboard({
879
1465
  if (typeof route === "string" && route.trim()) {
880
1466
  setSpanFilters((prev) => ({ ...prev, route }));
881
1467
  setSelected(0);
882
- setSelectedTraceId(null);
883
- setSelectedSpanIndex(0);
1468
+ setDrilldownTraceId(null);
1469
+ setDrilldownSelectedIndex(0);
1470
+ }
1471
+ }
1472
+ if (input === "T") {
1473
+ let traceId;
1474
+ if (drilldownTraceId) {
1475
+ traceId = drilldownTraceId;
1476
+ } else {
1477
+ switch (viewMode) {
1478
+ case "log": {
1479
+ traceId = filteredLogs[selected]?.traceId;
1480
+ break;
1481
+ }
1482
+ case "span": {
1483
+ traceId = filteredSpans[selected]?.traceId;
1484
+ break;
1485
+ }
1486
+ case "errors": {
1487
+ traceId = filteredErrorSummaries[selected]?.traceId;
1488
+ break;
1489
+ }
1490
+ }
1491
+ }
1492
+ if (traceId && viewMode !== "trace") {
1493
+ setSpanFilters((prev) => ({ ...prev, traceId }));
1494
+ setViewMode("trace");
1495
+ setSelected(0);
1496
+ setDrilldownTraceId(null);
1497
+ setDrilldownSelectedIndex(0);
1498
+ setDrilldownTab("timeline");
1499
+ }
1500
+ return;
1501
+ }
1502
+ if (input === "L") {
1503
+ let traceId;
1504
+ if (drilldownTraceId) {
1505
+ traceId = drilldownTraceId;
1506
+ } else {
1507
+ switch (viewMode) {
1508
+ case "trace": {
1509
+ traceId = filteredSummaries[selected]?.traceId;
1510
+ break;
1511
+ }
1512
+ case "span": {
1513
+ traceId = filteredSpans[selected]?.traceId;
1514
+ break;
1515
+ }
1516
+ case "errors": {
1517
+ traceId = filteredErrorSummaries[selected]?.traceId;
1518
+ break;
1519
+ }
1520
+ }
1521
+ }
1522
+ if (traceId && viewMode !== "log") {
1523
+ setSpanFilters((prev) => ({ ...prev, traceId }));
1524
+ setViewMode("log");
1525
+ setSelected(0);
1526
+ setDrilldownTraceId(null);
1527
+ setDrilldownSelectedIndex(0);
1528
+ setDrilldownTab("timeline");
884
1529
  }
1530
+ return;
885
1531
  }
886
1532
  if (input === "J") {
887
- const t = selectedTraceSummary;
888
- if (!t) return;
889
- const json = exportTraceToJson(t, selectedTraceLogs);
1533
+ const t2 = selectedTraceSummary;
1534
+ if (!t2) return;
1535
+ const json = exportTraceToJson(t2, selectedTraceLogs);
890
1536
  process.stdout.write(`
891
1537
  [autotel-terminal] trace export
892
1538
  ${json}
@@ -899,22 +1545,32 @@ ${json}
899
1545
  const headerModeLabel = viewMode === "trace" ? "traces" : viewMode === "span" ? "spans" : viewMode === "log" ? "logs" : viewMode === "service-summary" ? "services" : "errors";
900
1546
  const showNewError = newErrorCount > 0;
901
1547
  function renderTreeRow(node, index) {
902
- const isSel = viewMode === "trace" && selectedTraceId != null && index === selectedSpanIndex;
1548
+ const isSel = drilldownTraceId != null && index === drilldownSelectedIndex;
903
1549
  const prefix = node.depth === 0 ? "" : " ".repeat(node.depth) + (node.children.length > 0 ? "\u251C\u2500\u2500 " : "\u2514\u2500\u2500 ");
904
1550
  const statusColor = node.span.status === "ERROR" ? "red" : node.span.durationMs > 500 ? "yellow" : "green";
905
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
906
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
907
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: prefix }),
908
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name, 24) }),
909
- /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
910
- " ",
911
- formatDurationMs(node.span.durationMs)
912
- ] })
913
- ] }, `${node.span.spanId}-${node.span.startTime}`);
1551
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1552
+ ink.Box,
1553
+ {
1554
+ flexDirection: "row",
1555
+ children: [
1556
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
1557
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: prefix }),
1558
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(node.span.name, 24) }),
1559
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1560
+ " ",
1561
+ formatDurationMs(node.span.durationMs)
1562
+ ] })
1563
+ ]
1564
+ },
1565
+ `${node.span.spanId}-${node.span.startTime}`
1566
+ );
914
1567
  }
915
1568
  function keyAttrsAndRest(attrs) {
916
1569
  if (!attrs || Object.keys(attrs).length === 0)
917
- return { key: [], rest: [] };
1570
+ return {
1571
+ key: [],
1572
+ rest: []
1573
+ };
918
1574
  const entries = Object.entries(attrs);
919
1575
  const key = entries.filter(([k]) => KEY_ATTR_KEYS.has(k));
920
1576
  const rest = entries.filter(([k]) => !KEY_ATTR_KEYS.has(k));
@@ -954,24 +1610,49 @@ ${json}
954
1610
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Shortcuts" }),
955
1611
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Navigation: \u2191/\u2193, Enter, Esc" }),
956
1612
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Views: t (trace/spans), l (logs), v (services), E (errors)" }),
957
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Search: /" }),
958
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Filters: e (errors-only), S (service), R (route), H (status), x (clear)" }),
1613
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Search: / (Tab autocompletes traceId)" }),
1614
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Filters: e (errors-only), S (service), R (route), H (status), f (traceId), x (clear)" }),
959
1615
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Capture: p (pause), r (record snapshot), J (export trace JSON)" }),
1616
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "AI: a (toggle AI panel)" }),
1617
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Jump: T (trace for item), L (logs for item)" }),
1618
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Drill-down: Enter (open trace), Tab (cycle tabs), Esc (back)" }),
960
1619
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Other: c (clear), ? (help), Ctrl+C (exit)" })
961
1620
  ]
962
1621
  }
963
- ) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, flexDirection: "row", justifyContent: "space-between", children: [
964
- searchMode ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
965
- "Search: ",
966
- searchQuery || "(type to filter)"
967
- ] }, "search") : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "\u2191/\u2193 select \u2022 Enter open \u2022 Esc back \u2022 t spans \u2022 l logs \u2022 v svc \u2022 E errors \u2022 / search \u2022 p pause \u2022 r record \u2022 e errors \u2022 c clear \u2022 ? help" }, "controls"),
968
- /* @__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}/${serviceStats.length}` : viewMode === "errors" ? `errors ${errorSummaries.length}/${errorSummaries.length}` : `logs ${filteredLogs.length}/${logs.length}` }, "count")
969
- ] }),
970
- (spanFilters.serviceName || spanFilters.route || spanFilters.statusGroup !== "all") && /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1622
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
1623
+ ink.Box,
1624
+ {
1625
+ marginBottom: 1,
1626
+ flexDirection: "row",
1627
+ justifyContent: "space-between",
1628
+ children: [
1629
+ searchMode ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [
1630
+ "Search: ",
1631
+ searchQuery || "(type to filter)",
1632
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: " (Tab: match traceId, Esc: cancel)" })
1633
+ ] }, "search") : traceIdMode ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "yellow", children: [
1634
+ "TraceId: ",
1635
+ traceIdInput || "(type prefix, Tab to complete)",
1636
+ traceIdInput.length >= 2 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1637
+ " ",
1638
+ "\u2192",
1639
+ " ",
1640
+ traceSummaries.find(
1641
+ (t2) => t2.traceId.toLowerCase().startsWith(traceIdInput.toLowerCase())
1642
+ )?.traceId.slice(0, 16) ?? "no match",
1643
+ "\u2026"
1644
+ ] })
1645
+ ] }, "traceid-input") : drilldownTraceId == null ? /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "\u2191/\u2193 select \u2022 Enter open \u2022 Esc back \u2022 t spans \u2022 l logs \u2022 v svc \u2022 E errors \u2022 T trace \u2022 L logs \u2022 / search \u2022 f traceId \u2022 p pause \u2022 r record \u2022 e errors \u2022 c clear \u2022 ? help" }, "controls") : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "\u2191/\u2193 select \u2022 Tab cycle tabs \u2022 Esc back \u2022 T trace \u2022 L logs \u2022 a AI \u2022 ? help" }, "controls"),
1646
+ /* @__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}/${serviceStats.length}` : viewMode === "errors" ? `errors ${filteredErrorSummaries.length}/${errorSummaries.length}` : `logs ${filteredLogs.length}/${logs.length}` }, "count")
1647
+ ]
1648
+ }
1649
+ ),
1650
+ (spanFilters.serviceName || spanFilters.route || spanFilters.statusGroup !== "all" || spanFilters.traceId) && /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
971
1651
  "filters:",
972
1652
  spanFilters.serviceName ? ` service=${spanFilters.serviceName}` : "",
973
1653
  spanFilters.route ? ` route=${spanFilters.route}` : "",
974
- spanFilters.statusGroup && spanFilters.statusGroup !== "all" ? ` status=${spanFilters.statusGroup}` : ""
1654
+ spanFilters.statusGroup && spanFilters.statusGroup !== "all" ? ` status=${spanFilters.statusGroup}` : "",
1655
+ spanFilters.traceId ? ` trace=${spanFilters.traceId.slice(0, 8)}\u2026` : ""
975
1656
  ] }) }),
976
1657
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", gap: 2, children: [
977
1658
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -986,51 +1667,202 @@ ${json}
986
1667
  children: [
987
1668
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 0, marginBottom: 1, children: [
988
1669
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: viewMode === "trace" ? "Recent traces" : viewMode === "span" ? "Recent spans" : viewMode === "service-summary" ? "Service summary" : viewMode === "errors" ? "Recent errors" : "Recent logs" }, "list-title"),
989
- filterErrorsOnly && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "red", children: " (errors only)" }, "errors-only-label"),
1670
+ filterErrorsOnly && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
1671
+ " ",
1672
+ "(errors only)"
1673
+ ] }, "errors-only-label"),
990
1674
  searchQuery && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
991
- " /",
1675
+ " ",
1676
+ "/",
992
1677
  searchQuery
993
1678
  ] }, "search-label")
994
1679
  ] }),
995
- viewMode === "trace" ? filteredSummaries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
1680
+ drilldownTraceId != null && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1681
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "row", gap: 2, children: [
1682
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "yellow", children: drilldownSummary?.rootName ?? "unknown" }),
1683
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1684
+ drilldownTraceId.slice(0, 16),
1685
+ "\u2026"
1686
+ ] }),
1687
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "green", children: drilldownSummary ? formatDurationMs(drilldownSummary.durationMs) : "?" }),
1688
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1689
+ drilldownSpans.length,
1690
+ " spans \u2022 ",
1691
+ drilldownLogs.length,
1692
+ " logs"
1693
+ ] })
1694
+ ] }),
1695
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 0, flexDirection: "row", gap: 2, children: [
1696
+ /* @__PURE__ */ jsxRuntime.jsx(
1697
+ ink.Text,
1698
+ {
1699
+ color: drilldownTab === "timeline" ? "yellow" : void 0,
1700
+ dimColor: drilldownTab !== "timeline",
1701
+ underline: drilldownTab === "timeline",
1702
+ children: "Timeline"
1703
+ }
1704
+ ),
1705
+ /* @__PURE__ */ jsxRuntime.jsxs(
1706
+ ink.Text,
1707
+ {
1708
+ color: drilldownTab === "spans" ? "yellow" : void 0,
1709
+ dimColor: drilldownTab !== "spans",
1710
+ underline: drilldownTab === "spans",
1711
+ children: [
1712
+ "Spans (",
1713
+ drilldownSpans.length,
1714
+ ")"
1715
+ ]
1716
+ }
1717
+ ),
1718
+ /* @__PURE__ */ jsxRuntime.jsxs(
1719
+ ink.Text,
1720
+ {
1721
+ color: drilldownTab === "logs" ? "yellow" : void 0,
1722
+ dimColor: drilldownTab !== "logs",
1723
+ underline: drilldownTab === "logs",
1724
+ children: [
1725
+ "Logs (",
1726
+ drilldownLogs.length,
1727
+ ")"
1728
+ ]
1729
+ }
1730
+ )
1731
+ ] })
1732
+ ] }),
1733
+ drilldownTraceId != null && drilldownTab === "timeline" && drilldownTimeline.map((item, i) => {
1734
+ const isSel = i === drilldownSelectedIndex;
1735
+ if (item.type === "span" && item.span) {
1736
+ const s = item.span;
1737
+ const node = drilldownTree.find(
1738
+ (n) => n.span.spanId === s.spanId
1739
+ );
1740
+ const depth = node?.depth ?? 0;
1741
+ const indent = " ".repeat(depth);
1742
+ const barLen = Math.max(
1743
+ 1,
1744
+ Math.round(
1745
+ s.durationMs / Math.max(1, drilldownSummary?.durationMs ?? 1) * 10
1746
+ )
1747
+ );
1748
+ const bar = "\u2588".repeat(barLen);
1749
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsxs(
1750
+ ink.Text,
1751
+ {
1752
+ backgroundColor: isSel ? "gray" : void 0,
1753
+ color: isSel ? "white" : void 0,
1754
+ children: [
1755
+ isSel ? "\u25B8" : " ",
1756
+ " ",
1757
+ indent,
1758
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "green", children: bar }),
1759
+ " ",
1760
+ s.name,
1761
+ " ",
1762
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1763
+ formatDurationMs(s.durationMs),
1764
+ s.kind ? ` ${s.kind}` : ""
1765
+ ] })
1766
+ ]
1767
+ }
1768
+ ) }, `${s.spanId}-${i}`);
1769
+ } else if (item.type === "log" && item.log) {
1770
+ const l = item.log;
1771
+ const levelColor = l.level === "error" ? "red" : l.level === "warn" ? "yellow" : "blue";
1772
+ const relTime = drilldownSummary ? `+${formatDurationMs(l.time - (drilldownSummary.spans[0]?.startTime ?? l.time))}` : "";
1773
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsxs(
1774
+ ink.Text,
1775
+ {
1776
+ backgroundColor: isSel ? "gray" : void 0,
1777
+ color: isSel ? "white" : void 0,
1778
+ children: [
1779
+ isSel ? "\u25B8" : " ",
1780
+ " ",
1781
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: levelColor, children: [
1782
+ "\u2139 ",
1783
+ l.level.toUpperCase()
1784
+ ] }),
1785
+ " ",
1786
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: truncate(l.message, 40) }),
1787
+ " ",
1788
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: relTime })
1789
+ ]
1790
+ }
1791
+ ) }, `log-${i}`);
1792
+ }
1793
+ return null;
1794
+ }),
1795
+ drilldownTraceId != null && drilldownTab === "spans" && drilldownTree.map((node, i) => renderTreeRow(node, i)),
1796
+ drilldownTraceId != null && drilldownTab === "logs" && drilldownLogs.map((log, i) => {
1797
+ const isSel = i === drilldownSelectedIndex;
1798
+ const levelColor = log.level === "error" ? "red" : log.level === "warn" ? "yellow" : log.level === "info" ? "green" : void 0;
1799
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsxs(
1800
+ ink.Text,
1801
+ {
1802
+ backgroundColor: isSel ? "gray" : void 0,
1803
+ color: isSel ? "white" : void 0,
1804
+ children: [
1805
+ isSel ? "\u25B8" : " ",
1806
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: levelColor, children: [
1807
+ " ",
1808
+ log.level.toUpperCase()
1809
+ ] }),
1810
+ " ",
1811
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1812
+ "[",
1813
+ truncate(log.message, 50),
1814
+ "]"
1815
+ ] })
1816
+ ]
1817
+ }
1818
+ ) }, `log-${i}`);
1819
+ }),
1820
+ drilldownTraceId == null && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: viewMode === "trace" ? filteredSummaries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
996
1821
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No traces yet. Call a traced function or hit an endpoint to see them here." }),
997
1822
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Tip: trace() your handlers with autotel to get spans." })
998
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: selectedTraceId == null ? filteredSummaries.slice(0, 20).map((t, i) => {
1823
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: drilldownTraceId == null ? filteredSummaries.slice(0, 20).map((t2, i) => {
999
1824
  const isSel = i === selected;
1000
1825
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
1001
1826
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
1002
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: t.hasError ? "red" : void 0, children: truncate(t.rootName, 20) }),
1827
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: t2.hasError ? "red" : void 0, children: truncate(t2.rootName, 20) }),
1003
1828
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1004
1829
  " ",
1005
- formatDurationMs(t.durationMs)
1830
+ formatDurationMs(t2.durationMs)
1006
1831
  ] }),
1007
1832
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1008
1833
  " ",
1009
- truncate(t.traceId, 8)
1834
+ truncate(t2.traceId, 8)
1010
1835
  ] }),
1011
1836
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1012
1837
  " ",
1013
- formatRelative(t.lastEndTime)
1838
+ formatRelative(t2.lastEndTime)
1014
1839
  ] })
1015
- ] }, t.traceId);
1840
+ ] }, t2.traceId);
1016
1841
  }) : traceTree.slice(0, 20).map((node, i) => renderTreeRow(node, i)) }) : viewMode === "span" ? filteredSpans.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
1017
1842
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No spans yet. Call a traced function or hit an endpoint to see them here." }),
1018
1843
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Tip: trace() your handlers with autotel to get spans." })
1019
1844
  ] }) : filteredSpans.slice(0, 20).map((s, i) => {
1020
1845
  const isSel = i === selected;
1021
1846
  const statusColor = s.status === "ERROR" ? "red" : s.durationMs > 500 ? "yellow" : "green";
1022
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
1023
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
1024
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(s.name, 26) }),
1025
- /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1026
- " ",
1027
- formatDurationMs(s.durationMs)
1028
- ] }),
1029
- /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1030
- " ",
1031
- formatRelative(s.endTime)
1032
- ] })
1033
- ] }, `${s.spanId}-${s.startTime}`);
1847
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1848
+ ink.Box,
1849
+ {
1850
+ flexDirection: "row",
1851
+ children: [
1852
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
1853
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: colors ? statusColor : void 0, children: truncate(s.name, 26) }),
1854
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1855
+ " ",
1856
+ formatDurationMs(s.durationMs)
1857
+ ] }),
1858
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1859
+ " ",
1860
+ formatRelative(s.endTime)
1861
+ ] })
1862
+ ]
1863
+ },
1864
+ `${s.spanId}-${s.startTime}`
1865
+ );
1034
1866
  }) : viewMode === "service-summary" ? serviceStats.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No service stats yet. Add `service.name` attributes to spans." }) }) : serviceStats.slice(0, 20).map((svc, i) => {
1035
1867
  const isSel = i === selected;
1036
1868
  const errorRate = svc.total ? svc.errors / svc.total * 100 : 0;
@@ -1053,7 +1885,7 @@ ${json}
1053
1885
  formatDurationMs(svc.p95Ms)
1054
1886
  ] })
1055
1887
  ] }, svc.serviceName);
1056
- }) : viewMode === "errors" ? errorSummaries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No errors yet." }) }) : errorSummaries.slice(0, 20).map((e, i) => {
1888
+ }) : viewMode === "errors" ? filteredErrorSummaries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No errors yet." }) }) : filteredErrorSummaries.slice(0, 20).map((e, i) => {
1057
1889
  const isSel = i === selected;
1058
1890
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
1059
1891
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSel ? "cyan" : void 0, children: isSel ? "\u203A " : " " }),
@@ -1088,11 +1920,11 @@ ${json}
1088
1920
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: " " }),
1089
1921
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: truncate(log.message, 32) })
1090
1922
  ] }, `${log.time}-${i}`);
1091
- })
1923
+ }) })
1092
1924
  ]
1093
1925
  }
1094
1926
  ),
1095
- /* @__PURE__ */ jsxRuntime.jsxs(
1927
+ /* @__PURE__ */ jsxRuntime.jsx(
1096
1928
  ink.Box,
1097
1929
  {
1098
1930
  flexDirection: "column",
@@ -1101,11 +1933,139 @@ ${json}
1101
1933
  borderColor: "gray",
1102
1934
  paddingX: 1,
1103
1935
  paddingY: 0,
1104
- children: [
1936
+ children: aiActive ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { 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 panel." })
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(
1948
+ ink.Box,
1949
+ {
1950
+ flexDirection: "column",
1951
+ marginBottom: msg.role === "assistant" ? 1 : 0,
1952
+ children: /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: msg.role === "user" ? "cyan" : void 0, children: [
1953
+ msg.role === "user" ? "> " : "",
1954
+ msg.content.slice(0, 500),
1955
+ msg.content.length > 500 ? "..." : ""
1956
+ ] })
1957
+ },
1958
+ i
1959
+ )),
1960
+ aiState.status === "error" && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", children: [
1961
+ "Error: ",
1962
+ aiState.message
1963
+ ] }),
1964
+ /* @__PURE__ */ jsxRuntime.jsxs(
1965
+ ink.Box,
1966
+ {
1967
+ marginTop: 1,
1968
+ borderStyle: "single",
1969
+ borderColor: "cyan",
1970
+ paddingX: 1,
1971
+ children: [
1972
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "cyan", children: "> " }),
1973
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: aiInput || (aiInputMode ? "(type your question)" : "(press a to focus)") })
1974
+ ]
1975
+ }
1976
+ )
1977
+ ] })
1978
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1105
1979
  /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Details" }) }),
1106
- viewMode === "errors" ? (() => {
1107
- const e = errorSummaries[selected] ?? null;
1108
- if (!e) return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select an error to view details." });
1980
+ drilldownTraceId != null && drilldownSelectedItem?.type === "span" && drilldownSelectedItem.span ? (() => {
1981
+ const span = drilldownSelectedItem.span;
1982
+ const { key: keyAttrs, rest: restAttrs } = keyAttrsAndRest(
1983
+ span.attributes
1984
+ );
1985
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1986
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
1987
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Name: " }),
1988
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: span.name })
1989
+ ] }),
1990
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
1991
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Status: " }),
1992
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: span.status === "ERROR" ? "red" : "green", children: span.status })
1993
+ ] }),
1994
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
1995
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Duration: " }),
1996
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: formatDurationMs(span.durationMs) })
1997
+ ] }),
1998
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1999
+ "Trace: ",
2000
+ span.traceId
2001
+ ] }),
2002
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2003
+ "Span: ",
2004
+ span.spanId
2005
+ ] }),
2006
+ span.parentSpanId && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2007
+ "Parent: ",
2008
+ span.parentSpanId
2009
+ ] }),
2010
+ span.kind && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2011
+ "Kind: ",
2012
+ span.kind
2013
+ ] }),
2014
+ keyAttrs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
2015
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Key attributes" }),
2016
+ keyAttrs.slice(0, 6).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2017
+ truncate(k, 18),
2018
+ ": ",
2019
+ truncate(String(v), 28)
2020
+ ] }, k))
2021
+ ] }),
2022
+ restAttrs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
2023
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Attributes" }),
2024
+ restAttrs.slice(0, 8).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2025
+ truncate(k, 18),
2026
+ ": ",
2027
+ truncate(String(v), 28)
2028
+ ] }, k))
2029
+ ] }),
2030
+ keyAttrs.length === 0 && restAttrs.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(no attributes)" })
2031
+ ] });
2032
+ })() : drilldownTraceId != null && drilldownSelectedItem?.type === "log" && drilldownSelectedItem.log ? (() => {
2033
+ const log = drilldownSelectedItem.log;
2034
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2035
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
2036
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Level: " }),
2037
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: log.level.toUpperCase() })
2038
+ ] }),
2039
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
2040
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Time: " }),
2041
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: new Date(log.time).toISOString() })
2042
+ ] }),
2043
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
2044
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Message: " }),
2045
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: log.message })
2046
+ ] }),
2047
+ log.traceId && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2048
+ "Trace: ",
2049
+ log.traceId
2050
+ ] }),
2051
+ log.spanId && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2052
+ "Span: ",
2053
+ log.spanId
2054
+ ] }),
2055
+ log.attributes && Object.keys(log.attributes).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
2056
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Attributes" }),
2057
+ Object.entries(log.attributes).slice(0, 10).map(([k, v]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
2058
+ truncate(k, 18),
2059
+ ": ",
2060
+ truncate(String(v), 40)
2061
+ ] }, k))
2062
+ ] })
2063
+ ] });
2064
+ })() : drilldownTraceId == null ? null : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select an item to view details." }),
2065
+ drilldownTraceId == null && (viewMode === "errors" ? (() => {
2066
+ const e = filteredErrorSummaries[selected] ?? null;
2067
+ if (!e)
2068
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select an error to view details." });
1109
2069
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1110
2070
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
1111
2071
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Trace: " }),
@@ -1121,11 +2081,12 @@ ${json}
1121
2081
  "Errors: ",
1122
2082
  e.errorCount
1123
2083
  ] }),
1124
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Tip: switch to trace view and search for this trace ID." })
2084
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press T to jump to trace view for this trace." })
1125
2085
  ] });
1126
2086
  })() : viewMode === "service-summary" ? (() => {
1127
2087
  const svc = serviceStats[selected] ?? null;
1128
- if (!svc) return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a service to view details." });
2088
+ if (!svc)
2089
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a service to view details." });
1129
2090
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1130
2091
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
1131
2092
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Service: " }),
@@ -1139,7 +2100,8 @@ ${json}
1139
2100
  " | Avg:",
1140
2101
  " ",
1141
2102
  formatDurationMs(svc.avgMs),
1142
- " | P95: ",
2103
+ " | P95:",
2104
+ " ",
1143
2105
  formatDurationMs(svc.p95Ms)
1144
2106
  ] }),
1145
2107
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
@@ -1159,7 +2121,8 @@ ${json}
1159
2121
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Hot spans" }),
1160
2122
  selectedServiceHotSpans.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(no spans)" }) : selectedServiceHotSpans.map((h) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1161
2123
  truncate(h.name, 20),
1162
- " p95 ",
2124
+ " p95",
2125
+ " ",
1163
2126
  formatDurationMs(h.p95Ms),
1164
2127
  " (",
1165
2128
  h.count,
@@ -1209,7 +2172,8 @@ ${json}
1209
2172
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
1210
2173
  "+",
1211
2174
  relMs,
1212
- "ms span ",
2175
+ "ms span",
2176
+ " ",
1213
2177
  truncate(item.span.name, 20)
1214
2178
  ] }, `span-${idx}`);
1215
2179
  }
@@ -1217,7 +2181,8 @@ ${json}
1217
2181
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
1218
2182
  "+",
1219
2183
  relMs,
1220
- "ms log ",
2184
+ "ms log",
2185
+ " ",
1221
2186
  truncate(item.log.message, 24)
1222
2187
  ] }, `log-${idx}`);
1223
2188
  }
@@ -1232,16 +2197,25 @@ ${json}
1232
2197
  ] }),
1233
2198
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
1234
2199
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Status: " }),
1235
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: currentSpan.status === "ERROR" ? "red" : "green", children: currentSpan.status })
2200
+ /* @__PURE__ */ jsxRuntime.jsx(
2201
+ ink.Text,
2202
+ {
2203
+ color: currentSpan.status === "ERROR" ? "red" : "green",
2204
+ children: currentSpan.status
2205
+ }
2206
+ )
1236
2207
  ] }),
1237
2208
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
1238
2209
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Duration: " }),
1239
2210
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { children: [
1240
2211
  formatDurationMs(currentSpan.durationMs),
1241
2212
  perSpanNameStats.byName.has(currentSpan.name) && (() => {
1242
- const p = perSpanNameStats.byName.get(currentSpan.name);
2213
+ const p = perSpanNameStats.byName.get(
2214
+ currentSpan.name
2215
+ );
1243
2216
  const ratio = p.avgMs > 0 ? currentSpan.durationMs / p.avgMs : 1;
1244
- if (ratio >= 1.5) return ` (${ratio.toFixed(1)}x avg)`;
2217
+ if (ratio >= 1.5)
2218
+ return ` (${ratio.toFixed(1)}x avg)`;
1245
2219
  return "";
1246
2220
  })()
1247
2221
  ] })
@@ -1284,10 +2258,12 @@ ${json}
1284
2258
  keyAttrs.length === 0 && restAttrs.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "(no attributes)" })
1285
2259
  ] });
1286
2260
  })(),
1287
- waterfallSpans.length > 0 && selectedTraceId != null && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
2261
+ waterfallSpans.length > 0 && drilldownTraceId != null && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginTop: 1, flexDirection: "column", children: [
1288
2262
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: "Waterfall" }),
1289
2263
  waterfallSpans.slice(0, 10).map((w) => {
1290
- const barLen = Math.round(w.span.durationMs / waterfallMaxMs * barWidth) || 1;
2264
+ const barLen = Math.round(
2265
+ w.span.durationMs / waterfallMaxMs * barWidth
2266
+ ) || 1;
1291
2267
  const bar = "\u2588".repeat(barLen);
1292
2268
  const indent = " ".repeat(w.depth);
1293
2269
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
@@ -1304,8 +2280,8 @@ ${json}
1304
2280
  ] }, w.span.spanId);
1305
2281
  })
1306
2282
  ] })
1307
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a trace or span to view details." })
1308
- ]
2283
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Select a trace or span to view details." }))
2284
+ ] })
1309
2285
  }
1310
2286
  )
1311
2287
  ] }),
@@ -1314,11 +2290,13 @@ ${json}
1314
2290
  stats.total,
1315
2291
  " | Span errors: ",
1316
2292
  stats.errors,
1317
- " | Logs: ",
2293
+ " | Logs:",
2294
+ " ",
1318
2295
  logStats.total,
1319
2296
  " | Log errors: ",
1320
2297
  logStats.errors,
1321
- " | Avg: ",
2298
+ " | Avg:",
2299
+ " ",
1322
2300
  formatDurationMs(stats.avg),
1323
2301
  " | P95: ",
1324
2302
  formatDurationMs(stats.p95)
@@ -1336,6 +2314,7 @@ function renderTerminal(options = {}, stream) {
1336
2314
  const showStats = options.showStats !== false;
1337
2315
  const maxSpans = options.maxSpans ?? 100;
1338
2316
  const colors = options.colors ?? Boolean(process.stdout.isTTY);
2317
+ const aiConfig = options.ai;
1339
2318
  const stdinOption = process.stdin.isTTY ? process.stdin : void 0;
1340
2319
  if (stream) {
1341
2320
  try {
@@ -1348,7 +2327,8 @@ function renderTerminal(options = {}, stream) {
1348
2327
  maxSpans,
1349
2328
  colors,
1350
2329
  stream,
1351
- logStream: getTerminalLogStream()
2330
+ logStream: getTerminalLogStream(),
2331
+ aiConfig
1352
2332
  }
1353
2333
  ),
1354
2334
  { stdin: stdinOption }
@@ -1464,7 +2444,8 @@ function toMs(unixNano) {
1464
2444
  }
1465
2445
  function mapStatus2(code) {
1466
2446
  const normalized = typeof code === "string" ? code.toUpperCase() : code;
1467
- if (normalized === 1 || normalized === "STATUS_CODE_OK" || normalized === "OK") return "OK";
2447
+ if (normalized === 1 || normalized === "STATUS_CODE_OK" || normalized === "OK")
2448
+ return "OK";
1468
2449
  if (normalized === 2 || normalized === "STATUS_CODE_ERROR" || normalized === "ERROR") {
1469
2450
  return "ERROR";
1470
2451
  }
@@ -1635,33 +2616,39 @@ function countOtlpMetrics(payload) {
1635
2616
 
1636
2617
  // src/cli.ts
1637
2618
  function printHelp() {
1638
- process.stdout.write(String.raw`autotel-terminal - Standalone OTLP receiver with terminal dashboard
2619
+ process.stdout.write(
2620
+ String.raw`autotel-terminal - Standalone OTLP receiver with terminal dashboard
1639
2621
 
1640
2622
  Usage: autotel-terminal [options]
1641
2623
 
1642
2624
  Options:
1643
- -p, --port <port> Port to listen on (default: 4319, env: AUTOTEL_TERMINAL_PORT)
1644
- -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_TERMINAL_HOST)
1645
- -t, --title <title> Dashboard title (env: AUTOTEL_TERMINAL_TITLE)
1646
- -h, --help Show this help message
1647
- -v, --version Show version number
2625
+ -p, --port <port> Port to listen on (default: 4319, env: AUTOTEL_TERMINAL_PORT)
2626
+ -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_TERMINAL_HOST)
2627
+ -t, --title <title> Dashboard title (env: AUTOTEL_TERMINAL_TITLE)
2628
+ -h, --help Show this help message
2629
+ -v, --version Show version number
2630
+
2631
+ AI Options:
2632
+ --ai-provider <provider> AI provider: ollama, openai, openai-compatible (env: AI_PROVIDER)
2633
+ --ai-model <model> AI model name (env: AI_MODEL)
2634
+ --ai-api-key <key> API key for cloud providers (env: AI_API_KEY)
2635
+ --ai-base-url <url> Custom AI endpoint URL (env: AI_BASE_URL)
2636
+
2637
+ Auto-detection: if Ollama is running locally, it is used automatically.
2638
+ If OPENAI_API_KEY is set, OpenAI is used. Press 'a' in the dashboard to toggle AI.
1648
2639
 
1649
2640
  Endpoints:
1650
- POST /v1/traces Receive OTLP JSON trace data
1651
- POST /v1/logs Receive OTLP JSON log data
1652
- POST /v1/metrics Receive OTLP JSON metric data (accepted and counted)
1653
- GET /healthz Health check
2641
+ POST /v1/traces Receive OTLP JSON trace data
2642
+ POST /v1/logs Receive OTLP JSON log data
2643
+ POST /v1/metrics Receive OTLP JSON metric data (accepted and counted)
2644
+ GET /healthz Health check
1654
2645
 
1655
2646
  Examples:
1656
2647
  npx autotel-terminal
1657
- npx autotel-terminal --port 4319
1658
- npx autotel-terminal -p 4319 -H 0.0.0.0
1659
-
1660
- Then in another terminal:
1661
- OTEL_EXPORTER_OTLP_PROTOCOL=http/json \
1662
- OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4319 \
1663
- node app.js
1664
- ` + "\n");
2648
+ npx autotel-terminal --ai-provider ollama --ai-model granite4
2649
+ AI_API_KEY=sk-... npx autotel-terminal --ai-provider openai --ai-model gpt-4o
2650
+ ` + "\n"
2651
+ );
1665
2652
  }
1666
2653
  function printVersion() {
1667
2654
  try {
@@ -1678,7 +2665,8 @@ function parseArgs(argv) {
1678
2665
  const options = {
1679
2666
  port: Number(process.env.AUTOTEL_TERMINAL_PORT || 4319),
1680
2667
  host: process.env.AUTOTEL_TERMINAL_HOST || "127.0.0.1",
1681
- title: process.env.AUTOTEL_TERMINAL_TITLE
2668
+ title: process.env.AUTOTEL_TERMINAL_TITLE,
2669
+ ai: {}
1682
2670
  };
1683
2671
  for (let i = 0; i < argv.length; i++) {
1684
2672
  const arg = argv[i];
@@ -1704,6 +2692,26 @@ function parseArgs(argv) {
1704
2692
  if ((arg === "--title" || arg === "-t") && next) {
1705
2693
  options.title = next;
1706
2694
  i++;
2695
+ continue;
2696
+ }
2697
+ if (arg === "--ai-provider" && next) {
2698
+ options.ai.provider = next;
2699
+ i++;
2700
+ continue;
2701
+ }
2702
+ if (arg === "--ai-model" && next) {
2703
+ options.ai.model = next;
2704
+ i++;
2705
+ continue;
2706
+ }
2707
+ if (arg === "--ai-api-key" && next) {
2708
+ options.ai.apiKey = next;
2709
+ i++;
2710
+ continue;
2711
+ }
2712
+ if (arg === "--ai-base-url" && next) {
2713
+ options.ai.baseUrl = next;
2714
+ i++;
1707
2715
  }
1708
2716
  }
1709
2717
  return options;
@@ -1718,7 +2726,8 @@ async function main() {
1718
2726
  const logStream = getTerminalLogStream();
1719
2727
  renderTerminal(
1720
2728
  {
1721
- title: options.title || `Autotel Terminal (${options.host}:${options.port})`
2729
+ title: options.title || `Autotel Terminal (${options.host}:${options.port})`,
2730
+ ai: options.ai
1722
2731
  },
1723
2732
  spanStream
1724
2733
  );