@sentry/junior-dashboard 0.62.0 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/app.d.ts +2 -0
  2. package/dist/app.js +1285 -39
  3. package/dist/assets.d.ts +2 -0
  4. package/dist/client/api.d.ts +1 -1
  5. package/dist/client/components/Button.d.ts +15 -0
  6. package/dist/client/components/Metric.d.ts +22 -0
  7. package/dist/client/components/StatusBadge.d.ts +2 -1
  8. package/dist/client/components/TelemetryMetrics.d.ts +24 -0
  9. package/dist/client/components/ToolFrame.d.ts +3 -1
  10. package/dist/client/components/Transcript.d.ts +2 -0
  11. package/dist/client/components/TranscriptHeader.d.ts +2 -0
  12. package/dist/client/components/TranscriptHeadingRow.d.ts +16 -0
  13. package/dist/client/components/TranscriptThinkingView.d.ts +7 -0
  14. package/dist/client/components/TranscriptToolRun.d.ts +9 -0
  15. package/dist/client/components/transcriptRenderModel.d.ts +25 -9
  16. package/dist/client/format.d.ts +45 -8
  17. package/dist/client/markdownExport.d.ts +3 -0
  18. package/dist/client/toolInvocations.d.ts +7 -0
  19. package/dist/client/types.d.ts +17 -100
  20. package/dist/client.js +85 -55998
  21. package/dist/dashboardLoader.d.ts +1 -0
  22. package/dist/handler.js +1288 -40
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.js +1807 -0
  25. package/dist/mock-conversations.d.ts +3 -0
  26. package/dist/mock-release-conversation.d.ts +3 -0
  27. package/dist/nitro.d.ts +7 -1
  28. package/dist/tailwind.css +1 -1
  29. package/dist/url.d.ts +13 -0
  30. package/package.json +9 -5
  31. package/src/app.ts +42 -18
  32. package/src/assets.ts +2 -0
  33. package/src/auth.ts +2 -35
  34. package/src/client/App.tsx +33 -74
  35. package/src/client/code.tsx +1 -1
  36. package/src/client/components/Button.tsx +75 -0
  37. package/src/client/components/ConversationRowStats.tsx +3 -2
  38. package/src/client/components/FilterTabs.tsx +10 -11
  39. package/src/client/components/LoadingView.tsx +7 -2
  40. package/src/client/components/Metric.tsx +159 -0
  41. package/src/client/components/StatusBadge.tsx +10 -1
  42. package/src/client/components/TelemetryMetrics.tsx +124 -0
  43. package/src/client/components/ToolFrame.tsx +57 -14
  44. package/src/client/components/Transcript.tsx +6 -2
  45. package/src/client/components/TranscriptHeader.tsx +18 -19
  46. package/src/client/components/TranscriptHeadingRow.tsx +64 -0
  47. package/src/client/components/TranscriptThinkingView.tsx +157 -0
  48. package/src/client/components/TranscriptToolRun.tsx +66 -0
  49. package/src/client/components/TranscriptToolView.tsx +16 -8
  50. package/src/client/components/TranscriptTurn.tsx +368 -132
  51. package/src/client/components/TurnDurationChart.tsx +236 -78
  52. package/src/client/components/transcriptRenderModel.ts +60 -20
  53. package/src/client/format.ts +329 -87
  54. package/src/client/markdownExport.ts +360 -0
  55. package/src/client/pages/CommandCenter.tsx +1 -1
  56. package/src/client/pages/ConversationPage.tsx +142 -45
  57. package/src/client/pages/ConversationsPage.tsx +1 -1
  58. package/src/client/toolInvocations.ts +16 -0
  59. package/src/client/types.ts +34 -90
  60. package/src/config.ts +4 -0
  61. package/src/dashboardLoader.ts +12 -0
  62. package/src/index.ts +78 -0
  63. package/src/mock-conversations.ts +726 -0
  64. package/src/mock-release-conversation.ts +605 -0
  65. package/src/nitro.ts +7 -1
  66. package/src/tailwind.css +11 -0
  67. package/src/url.ts +68 -0
@@ -0,0 +1,64 @@
1
+ import type { ReactNode } from "react";
2
+ import { Brain } from "lucide-react";
3
+
4
+ import { cn } from "../styles";
5
+
6
+ /** Align transcript labels and metadata as one centered heading row. */
7
+ export function TranscriptHeadingRow(props: {
8
+ className?: string;
9
+ left: ReactNode;
10
+ leftClassName?: string;
11
+ right?: ReactNode;
12
+ rightClassName?: string;
13
+ }) {
14
+ const hasRight =
15
+ props.right !== undefined && props.right !== null && props.right !== false;
16
+
17
+ return (
18
+ <div
19
+ className={cn(
20
+ "flex min-w-0 items-center justify-between gap-3",
21
+ props.className,
22
+ )}
23
+ >
24
+ <div
25
+ className={cn(
26
+ "flex min-w-0 items-center gap-2 overflow-hidden",
27
+ props.leftClassName,
28
+ )}
29
+ >
30
+ {props.left}
31
+ </div>
32
+ {hasRight ? (
33
+ <div className={cn("shrink-0 text-right", props.rightClassName)}>
34
+ {props.right}
35
+ </div>
36
+ ) : null}
37
+ </div>
38
+ );
39
+ }
40
+
41
+ /** Render compact transcript heading metadata without changing row alignment. */
42
+ export function TranscriptHeadingMeta(props: {
43
+ children: ReactNode;
44
+ className?: string;
45
+ }) {
46
+ return (
47
+ <span className={cn("font-mono leading-none", props.className)}>
48
+ {props.children}
49
+ </span>
50
+ );
51
+ }
52
+
53
+ /** Render the thinking-row label with stable optical vertical centering. */
54
+ export function TranscriptThoughtLabel() {
55
+ return (
56
+ <span
57
+ aria-label="Thinking"
58
+ className="mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-[#777]"
59
+ title="Thinking"
60
+ >
61
+ <Brain aria-hidden="true" size={14} strokeWidth={1.8} />
62
+ </span>
63
+ );
64
+ }
@@ -0,0 +1,157 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+
3
+ import {
4
+ formatMessageOffset,
5
+ formatMessageTimestamp,
6
+ stringifyPartValue,
7
+ } from "../format";
8
+ import { cn } from "../styles";
9
+ import type { ConversationTurn } from "../types";
10
+ import {
11
+ TranscriptHeadingMeta,
12
+ TranscriptHeadingRow,
13
+ TranscriptThoughtLabel,
14
+ } from "./TranscriptHeadingRow";
15
+ import { previewToolValue } from "./transcriptPreview";
16
+
17
+ const PREVIEW_OVERFLOW_EPSILON = 1;
18
+
19
+ function isString(value: string | undefined): value is string {
20
+ return typeof value === "string" && value.length > 0;
21
+ }
22
+
23
+ /** Render a thinking transcript event with layout-aware expansion. */
24
+ export function TranscriptThinkingView(props: {
25
+ timestamp?: number;
26
+ turn?: ConversationTurn;
27
+ value: unknown;
28
+ }) {
29
+ const [open, setOpen] = useState(false);
30
+ const [previewOverflows, setPreviewOverflows] = useState(false);
31
+ const previewMeasureRef = useRef<HTMLSpanElement>(null);
32
+ const rendered = stringifyPartValue(props.value);
33
+ const expandedText = rendered || "{}";
34
+ const preview = previewToolValue(props.value);
35
+ const contentChangesOnExpand =
36
+ preview !== expandedText || expandedText.includes("\n");
37
+ const shouldMeasurePreview = !contentChangesOnExpand;
38
+ const offset = props.turn
39
+ ? formatMessageOffset(props.turn, props.timestamp)
40
+ : undefined;
41
+ const meta = [
42
+ typeof props.timestamp === "number"
43
+ ? formatMessageTimestamp(props.timestamp)
44
+ : undefined,
45
+ offset,
46
+ ].filter(isString);
47
+ const metaText = meta.join(" · ");
48
+ const canExpand = contentChangesOnExpand || previewOverflows;
49
+
50
+ useEffect(() => {
51
+ if (!shouldMeasurePreview) {
52
+ setPreviewOverflows(false);
53
+ return;
54
+ }
55
+
56
+ const node = previewMeasureRef.current;
57
+ if (!node) return;
58
+
59
+ const measure = () => {
60
+ const next =
61
+ node.scrollWidth - node.clientWidth > PREVIEW_OVERFLOW_EPSILON;
62
+ setPreviewOverflows((current) => (current === next ? current : next));
63
+ };
64
+ measure();
65
+
66
+ const observer =
67
+ typeof ResizeObserver === "undefined"
68
+ ? undefined
69
+ : new ResizeObserver(measure);
70
+ observer?.observe(node);
71
+ if (node.parentElement) observer?.observe(node.parentElement);
72
+ window.addEventListener("resize", measure);
73
+
74
+ return () => {
75
+ observer?.disconnect();
76
+ window.removeEventListener("resize", measure);
77
+ };
78
+ }, [preview, shouldMeasurePreview]);
79
+
80
+ useEffect(() => {
81
+ if (!canExpand && open) setOpen(false);
82
+ }, [canExpand, open]);
83
+
84
+ const rowContent = (expanded: boolean) => (
85
+ <>
86
+ <TranscriptThoughtLabel />
87
+ <TranscriptHeadingRow
88
+ className={expanded ? "items-start" : undefined}
89
+ left={
90
+ <span className="relative min-w-0 flex-1 italic">
91
+ {shouldMeasurePreview ? (
92
+ <span
93
+ aria-hidden="true"
94
+ className="pointer-events-none invisible absolute inset-x-0 top-0 block truncate"
95
+ ref={previewMeasureRef}
96
+ >
97
+ {preview}
98
+ </span>
99
+ ) : null}
100
+ <span
101
+ className={cn(
102
+ "block min-w-0",
103
+ expanded
104
+ ? "whitespace-pre-wrap break-words text-[#9a9a9a]"
105
+ : "truncate",
106
+ )}
107
+ >
108
+ {expanded ? expandedText : preview}
109
+ </span>
110
+ </span>
111
+ }
112
+ leftClassName={expanded ? "items-start" : undefined}
113
+ right={
114
+ metaText ? (
115
+ <TranscriptHeadingMeta className="min-w-0 break-words text-[0.78rem] not-italic text-[#777] max-md:hidden">
116
+ {metaText}
117
+ </TranscriptHeadingMeta>
118
+ ) : undefined
119
+ }
120
+ rightClassName="min-w-0 max-md:hidden"
121
+ />
122
+ </>
123
+ );
124
+
125
+ if (!canExpand) {
126
+ return (
127
+ <div className="py-1.5 text-[0.84rem] leading-relaxed text-[#888]">
128
+ <div className="grid list-none grid-cols-[1rem_minmax(0,1fr)] items-start gap-2">
129
+ {rowContent(false)}
130
+ </div>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ return (
136
+ <details
137
+ className="py-1.5 text-[0.84rem] leading-relaxed text-[#888]"
138
+ onToggle={(event) => {
139
+ if (event.currentTarget !== event.target) return;
140
+ setOpen(event.currentTarget.open);
141
+ }}
142
+ open={open}
143
+ >
144
+ <summary className="grid cursor-pointer list-none grid-cols-[1rem_minmax(0,1fr)] items-start gap-2 transition-colors hover:text-[#b8b8b8] [&::-webkit-details-marker]:hidden">
145
+ {rowContent(open)}
146
+ </summary>
147
+ {metaText ? (
148
+ <div className="hidden min-w-0 grid-cols-[1rem_minmax(0,1fr)] gap-2 max-md:grid">
149
+ <span aria-hidden="true" />
150
+ <div className="min-w-0 break-words py-1 font-mono text-[0.78rem] not-italic leading-snug text-[#777]">
151
+ {metaText}
152
+ </div>
153
+ </div>
154
+ ) : null}
155
+ </details>
156
+ );
157
+ }
@@ -0,0 +1,66 @@
1
+ import { Fragment, useState, type ReactNode } from "react";
2
+
3
+ import type { RenderedToolEntry } from "./transcriptRenderModel";
4
+
5
+ const TOOL_RUN_REVEAL_THRESHOLD = 4;
6
+
7
+ /** Render a consecutive tool run with a one-way reveal for dense middle calls. */
8
+ export function TranscriptToolRun(props: {
9
+ entries: RenderedToolEntry[];
10
+ keyPrefix: string;
11
+ renderTool: (entry: RenderedToolEntry, index: number) => ReactNode;
12
+ startIndex: number;
13
+ }) {
14
+ const [revealed, setRevealed] = useState(false);
15
+
16
+ if (props.entries.length < TOOL_RUN_REVEAL_THRESHOLD || revealed) {
17
+ return (
18
+ <>
19
+ {renderToolEntries(
20
+ props.entries,
21
+ props.startIndex,
22
+ props.keyPrefix,
23
+ props.renderTool,
24
+ )}
25
+ </>
26
+ );
27
+ }
28
+
29
+ return (
30
+ <ToolRunReveal
31
+ hiddenCount={props.entries.length}
32
+ onClick={() => setRevealed(true)}
33
+ />
34
+ );
35
+ }
36
+
37
+ function renderToolEntries(
38
+ entries: RenderedToolEntry[],
39
+ startIndex: number,
40
+ keyPrefix: string,
41
+ renderTool: (entry: RenderedToolEntry, index: number) => ReactNode,
42
+ ): ReactNode[] {
43
+ return entries.map((entry, offset) => {
44
+ const index = startIndex + offset;
45
+ return (
46
+ <Fragment key={`${keyPrefix}:tool:${index}`}>
47
+ {renderTool(entry, index)}
48
+ </Fragment>
49
+ );
50
+ });
51
+ }
52
+
53
+ function ToolRunReveal(props: { hiddenCount: number; onClick: () => void }) {
54
+ return (
55
+ <button
56
+ aria-expanded={false}
57
+ className="group flex w-full cursor-pointer items-center gap-2 py-1.5 text-left font-mono text-[0.78rem] leading-tight text-[#888] transition-colors hover:text-[#d6d6d6] focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#beaaff]/55"
58
+ onClick={props.onClick}
59
+ type="button"
60
+ >
61
+ <span className="h-px min-w-4 flex-1 bg-white/10 transition-colors group-hover:bg-white/20" />
62
+ <span className="shrink-0">show {props.hiddenCount} tool calls</span>
63
+ <span className="h-px min-w-4 flex-1 bg-white/10 transition-colors group-hover:bg-white/20" />
64
+ </button>
65
+ );
66
+ }
@@ -38,20 +38,26 @@ export function TranscriptToolView(props: {
38
38
  ? formatMs(props.resultTimestamp - props.timestamp)
39
39
  : undefined;
40
40
  const meta = [
41
- props.timestamp ? formatMessageTimestamp(props.timestamp) : undefined,
41
+ typeof props.timestamp === "number"
42
+ ? formatMessageTimestamp(props.timestamp)
43
+ : undefined,
42
44
  duration,
43
45
  props.result ? formatBytes(outputBytes) : undefined,
44
46
  props.result ? undefined : "missing result",
45
47
  ].filter(isString);
46
48
  const args = <ToolArgumentsPreview input={input} />;
49
+ const hasExpandableContent = Boolean(props.call || props.result);
50
+ const mobileSummaryMeta =
51
+ duration ?? (props.call && !props.result ? "missing result" : undefined);
47
52
 
48
53
  if (props.view === "raw") {
49
54
  return (
50
55
  <ToolFrame
51
56
  meta={meta}
57
+ mobileSummaryMeta={mobileSummaryMeta}
52
58
  raw
53
59
  signature={
54
- <strong className="min-w-0 break-words font-bold text-white">
60
+ <strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
55
61
  {toolName}
56
62
  </strong>
57
63
  }
@@ -71,14 +77,16 @@ export function TranscriptToolView(props: {
71
77
 
72
78
  return (
73
79
  <ToolFrame
80
+ expandable={hasExpandableContent}
74
81
  meta={meta}
82
+ mobileSummaryMeta={mobileSummaryMeta}
75
83
  signature={
76
84
  <>
77
- <strong className="min-w-0 break-words font-bold text-white">
85
+ <strong className="min-w-0 break-words font-bold text-[#d6d6d6]">
78
86
  {toolName}
79
87
  </strong>
80
88
  {isPreviewableValue(input) ? (
81
- <code className="min-w-0 break-words font-[inherit] text-[#b8b8b8]">
89
+ <code className="min-w-0 break-words font-[inherit] text-[#b8b8b8] max-md:hidden">
82
90
  ({args})
83
91
  </code>
84
92
  ) : null}
@@ -113,12 +121,12 @@ function ToolBodySection(props: {
113
121
  return (
114
122
  <div
115
123
  className={cn(
116
- "border-t border-white/10 px-3",
117
- props.padded === false ? "" : "py-3",
124
+ "min-w-0 max-w-full overflow-hidden border-t border-white/10",
125
+ props.padded === false ? "" : "py-2",
118
126
  )}
119
127
  >
120
128
  {props.label ? (
121
- <div className="pb-2 font-mono text-[0.78rem] uppercase leading-none text-[#888]">
129
+ <div className="pb-2 font-mono text-[0.78rem] leading-none text-[#888]">
122
130
  {props.label}
123
131
  </div>
124
132
  ) : null}
@@ -173,7 +181,7 @@ function ToolArgEntry(props: { index: number; name: string; value: string }) {
173
181
  return (
174
182
  <span>
175
183
  {props.index > 0 ? <span className="text-[#888]">, </span> : null}
176
- <span className="text-white">{props.name}</span>
184
+ <span className="text-[#d6d6d6]">{props.name}</span>
177
185
  <span className="text-[#888]">: </span>
178
186
  <ToolArgValue value={props.value} />
179
187
  </span>