@sentry/junior-dashboard 0.265.0 → 0.267.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.
- package/dist/app.js +49 -12
- package/dist/client.js +94 -63
- package/dist/index.js +49 -12
- package/dist/tailwind.css +1 -1
- package/package.json +11 -5
- package/src/app.ts +0 -2
- package/src/auth.ts +4 -7
- package/src/client/App.tsx +35 -53
- package/src/client/components/ActorAvatar.tsx +10 -22
- package/src/client/components/ConnectionBanner.tsx +4 -3
- package/src/client/components/CopyButton.tsx +3 -4
- package/src/client/components/DetailsPanel.tsx +12 -13
- package/src/client/components/Drawer.tsx +46 -23
- package/src/client/components/EmptyTelemetry.tsx +5 -3
- package/src/client/components/ImageAttachment.tsx +49 -63
- package/src/client/components/InlineError.tsx +8 -4
- package/src/client/components/Notice.tsx +19 -26
- package/src/client/components/Pagination.tsx +26 -26
- package/src/client/components/ParticipantAvatarStack.tsx +13 -10
- package/src/client/components/README.md +28 -3
- package/src/client/components/SearchInput.tsx +51 -41
- package/src/client/components/VersionDriftBanner.tsx +9 -7
- package/src/client/components/charts/CacheWrites.tsx +52 -43
- package/src/client/components/ui/alert.tsx +64 -0
- package/src/client/components/ui/avatar.tsx +53 -0
- package/src/client/components/ui/button-group.tsx +59 -0
- package/src/client/components/ui/button.tsx +2 -1
- package/src/client/components/ui/dialog.tsx +60 -0
- package/src/client/components/ui/drawer.tsx +140 -0
- package/src/client/components/ui/empty.tsx +61 -0
- package/src/client/components/ui/input-group.tsx +96 -0
- package/src/client/components/ui/pagination.tsx +60 -0
- package/src/client/components/ui/separator.tsx +22 -0
- package/src/client/components/ui/spinner.tsx +21 -0
- package/src/client/components/ui/table.tsx +22 -1
- package/src/client/components/ui/tooltip.tsx +1 -1
- package/src/client/components/ui/useSheet.ts +5 -1
- package/src/client/conversations/ComposerImages.tsx +66 -14
- package/src/client/conversations/ConversationChildAgents.tsx +5 -5
- package/src/client/conversations/ConversationComposer.tsx +84 -12
- package/src/client/conversations/ConversationDetails.tsx +3 -27
- package/src/client/conversations/ConversationEventDetails.tsx +26 -10
- package/src/client/conversations/ConversationHeader.tsx +5 -4
- package/src/client/conversations/ConversationHeaderActions.tsx +7 -11
- package/src/client/conversations/ConversationPage.tsx +156 -2
- package/src/client/conversations/ConversationSidebar.tsx +5 -4
- package/src/client/conversations/ConversationTranscript.tsx +48 -16
- package/src/client/conversations/ConversationUsageBreakdown.tsx +108 -65
- package/src/client/conversations/ConversationWatches.tsx +67 -59
- package/src/client/conversations/DetailsList.tsx +77 -29
- package/src/client/conversations/EventDetails.tsx +46 -19
- package/src/client/conversations/EventUsage.tsx +99 -74
- package/src/client/conversations/ForkConversationButton.tsx +79 -73
- package/src/client/conversations/PendingMailboxStack.tsx +312 -119
- package/src/client/conversations/README.md +30 -10
- package/src/client/conversations/ShareConversationButton.tsx +3 -4
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +19 -22
- package/src/client/conversations/TranscriptActivityGroup.tsx +22 -19
- package/src/client/conversations/TranscriptAttachment.tsx +39 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +14 -19
- package/src/client/conversations/TranscriptEventView.tsx +35 -16
- package/src/client/conversations/TranscriptFailureView.tsx +10 -13
- package/src/client/conversations/TranscriptMarkdown.tsx +232 -436
- package/src/client/conversations/TranscriptMessageContextView.tsx +31 -20
- package/src/client/conversations/TranscriptMessageView.tsx +7 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +25 -15
- package/src/client/conversations/TranscriptSummary.tsx +13 -7
- package/src/client/conversations/TranscriptTurnContextView.tsx +78 -56
- package/src/client/conversations/TranscriptView.tsx +7 -3
- package/src/client/conversations/UserInputRequestCard.tsx +244 -0
- package/src/client/conversations/conversationOutbox.ts +46 -0
- package/src/client/conversations/eventLog.ts +1 -0
- package/src/client/conversations/eventTime.ts +13 -1
- package/src/client/conversations/eventTranscript.ts +12 -5
- package/src/client/conversations/queries.ts +127 -26
- package/src/client/conversations/transcriptMarkdownLinks.ts +5 -367
- package/src/client/conversations/transcriptMentions.tsx +18 -5
- package/src/client/conversations/userInputRequest.ts +56 -0
- package/src/client/format.ts +10 -3
- package/src/client/pages/automations/{AutomationLifecycle.tsx → AutomationActions.tsx} +60 -69
- package/src/client/pages/automations/AutomationDraftChanges.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +182 -156
- package/src/client/pages/automations/AutomationEventFields.tsx +119 -45
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +1 -1
- package/src/client/pages/automations/AutomationFormSection.tsx +20 -2
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +1 -1
- package/src/client/pages/automations/AutomationOverview.tsx +238 -0
- package/src/client/pages/automations/AutomationPage.tsx +343 -0
- package/src/client/pages/automations/AutomationRecentRuns.tsx +3 -2
- package/src/client/pages/automations/AutomationRow.tsx +11 -13
- package/src/client/pages/automations/AutomationRunDetails.tsx +4 -2
- package/src/client/pages/automations/{AutomationExecutionsPage.tsx → AutomationRunsTab.tsx} +94 -103
- package/src/client/pages/automations/AutomationStatus.tsx +164 -108
- package/src/client/pages/automations/AutomationVersionsTab.tsx +518 -0
- package/src/client/pages/automations/AutomationsPage.tsx +10 -98
- package/src/client/pages/automations/AutomationsPageLayout.tsx +5 -23
- package/src/client/pages/automations/README.md +56 -46
- package/src/client/types.ts +3 -0
- package/src/mock-reporting/fixtures.ts +44 -2
- package/src/mock-reporting/watches.ts +1 -1
- package/src/client/conversations/transcriptStyles.ts +0 -10
- package/src/client/pages/automations/AutomationDetails.tsx +0 -193
- package/src/client/pages/automations/AutomationEditPage.tsx +0 -176
- package/src/client/pages/automations/AutomationVersionsPage.tsx +0 -390
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Children, type ReactNode } from "react";
|
|
2
|
+
import Markdown, { type Components } from "react-markdown";
|
|
3
|
+
import remarkBreaks from "remark-breaks";
|
|
4
|
+
import remarkGfm from "remark-gfm";
|
|
2
5
|
|
|
3
6
|
import {
|
|
4
7
|
Table,
|
|
@@ -10,32 +13,24 @@ import {
|
|
|
10
13
|
} from "../components/ui/table";
|
|
11
14
|
import { cn } from "../styles";
|
|
12
15
|
import {
|
|
13
|
-
|
|
16
|
+
safeTranscriptHref,
|
|
14
17
|
TRANSCRIPT_ANCHOR_CLASS,
|
|
15
18
|
} from "./transcriptMarkdownLinks";
|
|
16
19
|
import { renderMentionText } from "./transcriptMentions";
|
|
17
20
|
import { HighlightText, useTranscriptSearch } from "./transcriptSearch";
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
alignments: TableAlignment[];
|
|
27
|
-
headers: string[];
|
|
28
|
-
rows: string[][];
|
|
29
|
-
}
|
|
30
|
-
| { type: "paragraph"; lines: string[] };
|
|
31
|
-
|
|
32
|
-
/** Render transcript markdown as readable prose with GFM-style hard breaks. */
|
|
22
|
+
/**
|
|
23
|
+
* Render transcript markdown as readable prose.
|
|
24
|
+
*
|
|
25
|
+
* The text is CommonMark plus GFM, with a hard break for each newline. Agent
|
|
26
|
+
* replies also use Slack-style `•` bullets and bold-only section labels, so
|
|
27
|
+
* those render as lists and headings too.
|
|
28
|
+
*/
|
|
33
29
|
export function TranscriptMarkdown(props: {
|
|
34
30
|
compact?: boolean;
|
|
35
31
|
text: string;
|
|
36
32
|
}): ReactNode {
|
|
37
|
-
|
|
38
|
-
if (blocks.length === 0) return null;
|
|
33
|
+
if (!props.text.trim()) return null;
|
|
39
34
|
|
|
40
35
|
return (
|
|
41
36
|
<div
|
|
@@ -47,436 +42,251 @@ export function TranscriptMarkdown(props: {
|
|
|
47
42
|
)}
|
|
48
43
|
>
|
|
49
44
|
<div className={cn("grid min-w-0", props.compact ? "gap-3" : "gap-2")}>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
switch (props.block.type) {
|
|
60
|
-
case "heading": {
|
|
61
|
-
const className = cn(
|
|
62
|
-
"m-0 min-w-0 font-semibold tracking-[-0.01em] text-dashboard-text [&:not(:first-child)]:mt-1 [&+*]:-mt-1",
|
|
63
|
-
props.block.level <= 2 ? "text-[1.05em]" : "text-[1em]",
|
|
64
|
-
);
|
|
65
|
-
const content = renderInlineMarkdown(props.block.text);
|
|
66
|
-
switch (props.block.level) {
|
|
67
|
-
case 1:
|
|
68
|
-
return <h1 className={className}>{content}</h1>;
|
|
69
|
-
case 2:
|
|
70
|
-
return <h2 className={className}>{content}</h2>;
|
|
71
|
-
case 3:
|
|
72
|
-
return <h3 className={className}>{content}</h3>;
|
|
73
|
-
case 4:
|
|
74
|
-
return <h4 className={className}>{content}</h4>;
|
|
75
|
-
case 5:
|
|
76
|
-
return <h5 className={className}>{content}</h5>;
|
|
77
|
-
default:
|
|
78
|
-
return <h6 className={className}>{content}</h6>;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
case "list": {
|
|
82
|
-
const ListTag = props.block.ordered ? "ol" : "ul";
|
|
83
|
-
return (
|
|
84
|
-
<ListTag
|
|
85
|
-
className={cn(
|
|
86
|
-
"m-0 grid min-w-0 list-outside gap-1.5 pl-6 text-dashboard-text marker:text-dashboard-text-muted",
|
|
87
|
-
props.block.ordered ? "list-decimal" : "list-disc",
|
|
88
|
-
)}
|
|
89
|
-
start={props.block.start}
|
|
45
|
+
<Markdown
|
|
46
|
+
components={COMPONENTS}
|
|
47
|
+
remarkPlugins={[
|
|
48
|
+
remarkGfm,
|
|
49
|
+
remarkBreaks,
|
|
50
|
+
remarkBareUrlStops,
|
|
51
|
+
remarkSectionLabels,
|
|
52
|
+
]}
|
|
53
|
+
urlTransform={safeTranscriptHref}
|
|
90
54
|
>
|
|
91
|
-
{props.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
</li>
|
|
95
|
-
))}
|
|
96
|
-
</ListTag>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
case "table":
|
|
100
|
-
return <MarkdownTable block={props.block} />;
|
|
101
|
-
case "paragraph":
|
|
102
|
-
return (
|
|
103
|
-
<p className="m-0 min-w-0 whitespace-normal text-dashboard-text">
|
|
104
|
-
{props.block.lines.map((line, index) => (
|
|
105
|
-
<span key={index}>
|
|
106
|
-
{index > 0 ? <br /> : null}
|
|
107
|
-
{renderInlineMarkdown(line)}
|
|
108
|
-
</span>
|
|
109
|
-
))}
|
|
110
|
-
</p>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function parseMarkdownProseBlocks(text: string): MarkdownBlock[] {
|
|
116
|
-
const normalized = text.replace(/\r\n?/g, "\n").replace(/^\n+|\n+$/g, "");
|
|
117
|
-
if (!normalized) return [];
|
|
118
|
-
|
|
119
|
-
const blocks: MarkdownBlock[] = [];
|
|
120
|
-
for (const chunk of normalized.split(/\n{2,}/)) {
|
|
121
|
-
const lines = chunk.split("\n");
|
|
122
|
-
let index = 0;
|
|
123
|
-
while (index < lines.length) {
|
|
124
|
-
const line = lines[index] ?? "";
|
|
125
|
-
const heading = matchHeading(line);
|
|
126
|
-
if (heading) {
|
|
127
|
-
blocks.push(heading);
|
|
128
|
-
index += 1;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const ordered = isOrderedListItem(line);
|
|
133
|
-
const unordered = isUnorderedListItem(line);
|
|
134
|
-
if (ordered || unordered) {
|
|
135
|
-
const start = ordered ? Number(/\d+/.exec(line)?.[0]) : undefined;
|
|
136
|
-
const items: string[] = [];
|
|
137
|
-
while (
|
|
138
|
-
index < lines.length &&
|
|
139
|
-
(ordered
|
|
140
|
-
? isOrderedListItem(lines[index] ?? "")
|
|
141
|
-
: isUnorderedListItem(lines[index] ?? ""))
|
|
142
|
-
) {
|
|
143
|
-
items.push(
|
|
144
|
-
(lines[index] ?? "").replace(
|
|
145
|
-
ordered ? ORDERED_LIST_MARKER : UNORDERED_LIST_MARKER,
|
|
146
|
-
"",
|
|
147
|
-
),
|
|
148
|
-
);
|
|
149
|
-
index += 1;
|
|
150
|
-
}
|
|
151
|
-
blocks.push({
|
|
152
|
-
items,
|
|
153
|
-
ordered,
|
|
154
|
-
start: start === 1 ? undefined : start,
|
|
155
|
-
type: "list",
|
|
156
|
-
});
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const table = matchTable(lines, index);
|
|
161
|
-
if (table) {
|
|
162
|
-
blocks.push(table.block);
|
|
163
|
-
index = table.nextIndex;
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const paragraphLines: string[] = [];
|
|
168
|
-
while (
|
|
169
|
-
index < lines.length &&
|
|
170
|
-
!matchHeading(lines[index] ?? "") &&
|
|
171
|
-
!isOrderedListItem(lines[index] ?? "") &&
|
|
172
|
-
!isUnorderedListItem(lines[index] ?? "") &&
|
|
173
|
-
!matchTable(lines, index)
|
|
174
|
-
) {
|
|
175
|
-
paragraphLines.push(lines[index] ?? "");
|
|
176
|
-
index += 1;
|
|
177
|
-
}
|
|
178
|
-
blocks.push({ lines: paragraphLines, type: "paragraph" });
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return blocks;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function MarkdownTable(props: {
|
|
186
|
-
block: Extract<MarkdownBlock, { type: "table" }>;
|
|
187
|
-
}) {
|
|
188
|
-
const columnCount = Math.max(
|
|
189
|
-
props.block.headers.length,
|
|
190
|
-
...props.block.rows.map((row) => row.length),
|
|
191
|
-
props.block.alignments.length,
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<div className="min-w-0 max-w-full overflow-hidden rounded-md border border-dashboard-border bg-dashboard-fill-faint">
|
|
196
|
-
<Table className="w-max min-w-full">
|
|
197
|
-
<TableHeader className="bg-dashboard-overlay-soft">
|
|
198
|
-
<TableRow className="border-dashboard-border">
|
|
199
|
-
{Array.from({ length: columnCount }, (_, index) => {
|
|
200
|
-
const alignment = props.block.alignments[index] ?? null;
|
|
201
|
-
return (
|
|
202
|
-
<TableHead
|
|
203
|
-
className={cn(
|
|
204
|
-
"px-2.5 py-1.5",
|
|
205
|
-
tableCellAlignClass(alignment),
|
|
206
|
-
)}
|
|
207
|
-
key={`h-${index}`}
|
|
208
|
-
scope="col"
|
|
209
|
-
>
|
|
210
|
-
{renderInlineMarkdown(props.block.headers[index] ?? "")}
|
|
211
|
-
</TableHead>
|
|
212
|
-
);
|
|
213
|
-
})}
|
|
214
|
-
</TableRow>
|
|
215
|
-
</TableHeader>
|
|
216
|
-
<TableBody>
|
|
217
|
-
{props.block.rows.map((row, rowIndex) => (
|
|
218
|
-
<TableRow key={`r-${rowIndex}`}>
|
|
219
|
-
{Array.from({ length: columnCount }, (_, index) => {
|
|
220
|
-
const alignment = props.block.alignments[index] ?? null;
|
|
221
|
-
return (
|
|
222
|
-
<TableCell
|
|
223
|
-
className={cn(
|
|
224
|
-
"px-2.5 py-1.5 align-top",
|
|
225
|
-
tableCellAlignClass(alignment),
|
|
226
|
-
)}
|
|
227
|
-
key={`c-${rowIndex}-${index}`}
|
|
228
|
-
>
|
|
229
|
-
{renderInlineMarkdown(row[index] ?? "")}
|
|
230
|
-
</TableCell>
|
|
231
|
-
);
|
|
232
|
-
})}
|
|
233
|
-
</TableRow>
|
|
234
|
-
))}
|
|
235
|
-
</TableBody>
|
|
236
|
-
</Table>
|
|
55
|
+
{normalizeSlackBullets(props.text)}
|
|
56
|
+
</Markdown>
|
|
57
|
+
</div>
|
|
237
58
|
</div>
|
|
238
59
|
);
|
|
239
60
|
}
|
|
240
61
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return undefined;
|
|
62
|
+
// Slack-style replies use "•" and "◦" bullets. CommonMark only knows -, *, +.
|
|
63
|
+
function normalizeSlackBullets(text: string): string {
|
|
64
|
+
return text.replace(/^([ \t]*)[•◦](?=[ \t])/gm, "$1-");
|
|
245
65
|
}
|
|
246
66
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const rows: string[][] = [];
|
|
266
|
-
let cursor = index + 2;
|
|
267
|
-
while (cursor < lines.length && isTableRowLine(lines[cursor] ?? "")) {
|
|
268
|
-
rows.push(splitTableRow(lines[cursor] ?? ""));
|
|
269
|
-
cursor += 1;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return {
|
|
273
|
-
block: {
|
|
274
|
-
alignments,
|
|
275
|
-
headers,
|
|
276
|
-
rows,
|
|
277
|
-
type: "table",
|
|
278
|
-
},
|
|
279
|
-
nextIndex: cursor,
|
|
67
|
+
type MarkdownNode = {
|
|
68
|
+
children?: MarkdownNode[];
|
|
69
|
+
depth?: number;
|
|
70
|
+
position?: { start: { offset?: number } };
|
|
71
|
+
type: string;
|
|
72
|
+
url?: string;
|
|
73
|
+
value?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* End a bare URL at `*`, as Slack output does (`readBareUrl` in
|
|
78
|
+
* `packages/junior/src/chat/slack/mrkdwn.ts`). GFM keeps `*` in the URL, so
|
|
79
|
+
* `https://example.com**bold**` would link to `https://example.com**bold`.
|
|
80
|
+
*/
|
|
81
|
+
function remarkBareUrlStops() {
|
|
82
|
+
return (tree: MarkdownNode, file: { value: unknown }) => {
|
|
83
|
+
splitBareUrls(tree, String(file.value));
|
|
280
84
|
};
|
|
281
85
|
}
|
|
282
86
|
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const right = token.endsWith(":");
|
|
301
|
-
if (left && right) return "center";
|
|
302
|
-
if (right) return "right";
|
|
303
|
-
if (left) return "left";
|
|
304
|
-
return null;
|
|
87
|
+
function splitBareUrls(parent: MarkdownNode, source: string): void {
|
|
88
|
+
parent.children = parent.children?.flatMap((node) => {
|
|
89
|
+
splitBareUrls(node, source);
|
|
90
|
+
const text = node.children?.[0];
|
|
91
|
+
const stop = node.url?.indexOf("*") ?? -1;
|
|
92
|
+
const bare =
|
|
93
|
+
node.type === "link" &&
|
|
94
|
+
node.children?.length === 1 &&
|
|
95
|
+
text?.value === node.url &&
|
|
96
|
+
source[node.position?.start.offset ?? -1] !== "<";
|
|
97
|
+
if (!bare || stop <= 0) return [node];
|
|
98
|
+
|
|
99
|
+
const url = node.url!.slice(0, stop);
|
|
100
|
+
return [
|
|
101
|
+
{ ...node, children: [{ type: "text", value: url }], url },
|
|
102
|
+
{ type: "text", value: node.url!.slice(stop) },
|
|
103
|
+
];
|
|
305
104
|
});
|
|
306
105
|
}
|
|
307
106
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
let escaped = false;
|
|
317
|
-
for (const char of trimmed) {
|
|
318
|
-
if (escaped) {
|
|
319
|
-
current += char;
|
|
320
|
-
escaped = false;
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
if (char === "\\") {
|
|
324
|
-
escaped = true;
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (char === "|") {
|
|
328
|
-
cells.push(current.trim());
|
|
329
|
-
current = "";
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
current += char;
|
|
333
|
-
}
|
|
334
|
-
cells.push(current.trim());
|
|
335
|
-
return cells;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function matchHeading(line: string): MarkdownBlock | undefined {
|
|
339
|
-
// Agents often write a bold-only line as a section label. Give it heading
|
|
340
|
-
// spacing so the section reads as a group.
|
|
341
|
-
if (/^\s*(?:\*\*[^*]+\*\*|__[^_]+__):?\s*$/.test(line)) {
|
|
342
|
-
return { level: 3, text: line.trim(), type: "heading" };
|
|
343
|
-
}
|
|
344
|
-
const match = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
345
|
-
if (!match) return undefined;
|
|
346
|
-
return {
|
|
347
|
-
level: match[1]!.length as 1 | 2 | 3 | 4 | 5 | 6,
|
|
348
|
-
text: match[2]!.trimEnd(),
|
|
349
|
-
type: "heading",
|
|
107
|
+
/**
|
|
108
|
+
* Agents often start a top-level paragraph with a bold-only line, such as
|
|
109
|
+
* `**Gaps**`, as a section label. Make that line a heading so the section
|
|
110
|
+
* reads as a group.
|
|
111
|
+
*/
|
|
112
|
+
function remarkSectionLabels() {
|
|
113
|
+
return (tree: MarkdownNode) => {
|
|
114
|
+
tree.children = tree.children?.flatMap(splitSectionLabel);
|
|
350
115
|
};
|
|
351
116
|
}
|
|
352
117
|
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
118
|
+
function splitSectionLabel(node: MarkdownNode): MarkdownNode[] {
|
|
119
|
+
const [label, ...rest] = node.children ?? [];
|
|
120
|
+
if (node.type !== "paragraph" || label?.type !== "strong") return [node];
|
|
356
121
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
122
|
+
const colon = rest[0]?.type === "text" && rest[0].value === ":";
|
|
123
|
+
const afterLabel = colon ? rest.slice(1) : rest;
|
|
124
|
+
if (afterLabel.length > 0 && afterLabel[0]?.type !== "break") return [node];
|
|
360
125
|
|
|
361
|
-
|
|
362
|
-
|
|
126
|
+
const heading: MarkdownNode = {
|
|
127
|
+
children: colon ? [label, rest[0]!] : [label],
|
|
128
|
+
depth: 3,
|
|
129
|
+
type: "heading",
|
|
130
|
+
};
|
|
131
|
+
const body = afterLabel.slice(1);
|
|
132
|
+
return body.length > 0
|
|
133
|
+
? [heading, { children: body, type: "paragraph" }]
|
|
134
|
+
: [heading];
|
|
363
135
|
}
|
|
364
136
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
137
|
+
const HEADING_CLASS =
|
|
138
|
+
"m-0 min-w-0 font-semibold tracking-[-0.01em] text-dashboard-text [&:not(:first-child)]:mt-1 [&+*]:-mt-1";
|
|
139
|
+
|
|
140
|
+
const COMPONENTS: Components = {
|
|
141
|
+
a: ({ children, href }) => (
|
|
142
|
+
<TranscriptAnchor href={href}>{children}</TranscriptAnchor>
|
|
143
|
+
),
|
|
144
|
+
blockquote: ({ children }) => (
|
|
145
|
+
<blockquote className="m-0 grid min-w-0 gap-2 border-l-2 border-dashboard-border pl-3 text-dashboard-text-muted">
|
|
146
|
+
{children}
|
|
147
|
+
</blockquote>
|
|
148
|
+
),
|
|
149
|
+
code: ({ children }) => (
|
|
150
|
+
<code className="rounded-[0.25rem] bg-dashboard-fill-strong px-1 py-0.5 font-mono text-[0.9em] text-dashboard-text">
|
|
151
|
+
{Children.map(children, (child) =>
|
|
152
|
+
typeof child === "string" ? <HighlightText text={child} /> : child,
|
|
153
|
+
)}
|
|
154
|
+
</code>
|
|
155
|
+
),
|
|
156
|
+
del: ({ children }) => (
|
|
157
|
+
<del className="text-dashboard-text-muted">{renderText(children)}</del>
|
|
158
|
+
),
|
|
159
|
+
em: ({ children }) => (
|
|
160
|
+
<em className="italic text-dashboard-text">{renderText(children)}</em>
|
|
161
|
+
),
|
|
162
|
+
h1: ({ children }) => (
|
|
163
|
+
<h1 className={cn(HEADING_CLASS, "text-[1.05em]")}>
|
|
164
|
+
{renderText(children)}
|
|
165
|
+
</h1>
|
|
166
|
+
),
|
|
167
|
+
h2: ({ children }) => (
|
|
168
|
+
<h2 className={cn(HEADING_CLASS, "text-[1.05em]")}>
|
|
169
|
+
{renderText(children)}
|
|
170
|
+
</h2>
|
|
171
|
+
),
|
|
172
|
+
h3: ({ children }) => (
|
|
173
|
+
<h3 className={cn(HEADING_CLASS, "text-[1em]")}>{renderText(children)}</h3>
|
|
174
|
+
),
|
|
175
|
+
h4: ({ children }) => (
|
|
176
|
+
<h4 className={cn(HEADING_CLASS, "text-[1em]")}>{renderText(children)}</h4>
|
|
177
|
+
),
|
|
178
|
+
h5: ({ children }) => (
|
|
179
|
+
<h5 className={cn(HEADING_CLASS, "text-[1em]")}>{renderText(children)}</h5>
|
|
180
|
+
),
|
|
181
|
+
h6: ({ children }) => (
|
|
182
|
+
<h6 className={cn(HEADING_CLASS, "text-[1em]")}>{renderText(children)}</h6>
|
|
183
|
+
),
|
|
184
|
+
hr: () => <hr className="m-0 w-full border-dashboard-border" />,
|
|
185
|
+
// Do not load remote images from transcript text. Link to them instead.
|
|
186
|
+
img: ({ alt, src }) => (
|
|
187
|
+
<TranscriptAnchor href={typeof src === "string" ? src : undefined}>
|
|
188
|
+
{alt || src}
|
|
189
|
+
</TranscriptAnchor>
|
|
190
|
+
),
|
|
191
|
+
li: ({ children, className }) => (
|
|
192
|
+
<li
|
|
193
|
+
className={cn(
|
|
194
|
+
"min-w-0 pl-1 [&>*+*]:mt-1.5",
|
|
195
|
+
className?.includes("task-list-item") && "-ml-5 list-none",
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{renderText(children)}
|
|
199
|
+
</li>
|
|
200
|
+
),
|
|
201
|
+
ol: ({ children, start }) => (
|
|
202
|
+
<ol
|
|
203
|
+
className="m-0 grid min-w-0 list-outside list-decimal gap-1.5 pl-6 text-dashboard-text marker:text-dashboard-text-muted"
|
|
204
|
+
start={start}
|
|
205
|
+
>
|
|
206
|
+
{children}
|
|
207
|
+
</ol>
|
|
208
|
+
),
|
|
209
|
+
p: ({ children }) => (
|
|
210
|
+
<p className="m-0 min-w-0 whitespace-normal">{renderText(children)}</p>
|
|
211
|
+
),
|
|
212
|
+
pre: ({ children }) => (
|
|
213
|
+
<pre className="m-0 min-w-0 overflow-x-auto rounded-md border border-dashboard-border-subtle bg-dashboard-surface-panel px-3 py-2.5 font-mono text-sm [&>code]:bg-transparent [&>code]:p-0">
|
|
214
|
+
{children}
|
|
215
|
+
</pre>
|
|
216
|
+
),
|
|
217
|
+
strong: ({ children }) => (
|
|
218
|
+
<strong className="font-semibold text-dashboard-text">
|
|
219
|
+
{renderText(children)}
|
|
220
|
+
</strong>
|
|
221
|
+
),
|
|
222
|
+
table: ({ children }) => (
|
|
223
|
+
<div className="min-w-0 max-w-full overflow-hidden rounded-md border border-dashboard-border bg-dashboard-fill-faint">
|
|
224
|
+
<Table className="w-max min-w-full">{children}</Table>
|
|
225
|
+
</div>
|
|
226
|
+
),
|
|
227
|
+
tbody: ({ children }) => <TableBody>{children}</TableBody>,
|
|
228
|
+
td: ({ children, style }) => (
|
|
229
|
+
<TableCell
|
|
230
|
+
className={cn("px-2.5 py-1.5 align-top", alignClass(style?.textAlign))}
|
|
231
|
+
>
|
|
232
|
+
{renderText(children)}
|
|
233
|
+
</TableCell>
|
|
234
|
+
),
|
|
235
|
+
th: ({ children, style }) => (
|
|
236
|
+
<TableHead
|
|
237
|
+
className={cn("px-2.5 py-1.5", alignClass(style?.textAlign))}
|
|
238
|
+
scope="col"
|
|
239
|
+
>
|
|
240
|
+
{renderText(children)}
|
|
241
|
+
</TableHead>
|
|
242
|
+
),
|
|
243
|
+
thead: ({ children }) => (
|
|
244
|
+
<TableHeader className="bg-dashboard-overlay-soft">{children}</TableHeader>
|
|
245
|
+
),
|
|
246
|
+
tr: ({ children }) => (
|
|
247
|
+
<TableRow className="[thead_&]:border-dashboard-border">
|
|
248
|
+
{children}
|
|
249
|
+
</TableRow>
|
|
250
|
+
),
|
|
251
|
+
ul: ({ children }) => (
|
|
252
|
+
<ul className="m-0 grid min-w-0 list-outside list-disc gap-1.5 pl-6 text-dashboard-text marker:text-dashboard-text-muted">
|
|
253
|
+
{children}
|
|
254
|
+
</ul>
|
|
255
|
+
),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Text nodes can hold Slack mention tokens and transcript search matches.
|
|
259
|
+
function renderText(children: ReactNode): ReactNode {
|
|
260
|
+
return Children.map(children, (child, index) =>
|
|
261
|
+
typeof child === "string" ? renderMentionText(child, `t-${index}`) : child,
|
|
262
|
+
);
|
|
368
263
|
}
|
|
369
264
|
|
|
370
|
-
function
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const codePattern = /(`+)([^`]+?)\1/g;
|
|
375
|
-
let cursor = 0;
|
|
376
|
-
let match: RegExpExecArray | null;
|
|
377
|
-
let part = 0;
|
|
378
|
-
|
|
379
|
-
while ((match = codePattern.exec(text))) {
|
|
380
|
-
if (match.index > cursor) {
|
|
381
|
-
nodes.push(
|
|
382
|
-
...renderEmphasisText(
|
|
383
|
-
text.slice(cursor, match.index),
|
|
384
|
-
`${keyBase}-${part++}`,
|
|
385
|
-
),
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
nodes.push(
|
|
389
|
-
<code
|
|
390
|
-
className="rounded-[0.25rem] bg-dashboard-fill-strong px-1 py-0.5 font-mono text-[0.9em] text-dashboard-text"
|
|
391
|
-
key={`code-${keyBase}-${part++}`}
|
|
392
|
-
>
|
|
393
|
-
<HighlightText text={match[2] ?? ""} />
|
|
394
|
-
</code>,
|
|
395
|
-
);
|
|
396
|
-
cursor = match.index + match[0].length;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (cursor < text.length) {
|
|
400
|
-
nodes.push(...renderEmphasisText(text.slice(cursor), `${keyBase}-${part}`));
|
|
401
|
-
}
|
|
402
|
-
return nodes;
|
|
265
|
+
function alignClass(align: unknown): string | undefined {
|
|
266
|
+
if (align === "center") return "text-center";
|
|
267
|
+
if (align === "right") return "text-right";
|
|
268
|
+
return undefined;
|
|
403
269
|
}
|
|
404
270
|
|
|
405
|
-
function
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const nodes: ReactNode[] = [];
|
|
409
|
-
const pattern = /(\*\*|__)(.+?)\1|\*([^*]+?)\*/g;
|
|
410
|
-
let cursor = 0;
|
|
411
|
-
let match: RegExpExecArray | null;
|
|
412
|
-
let part = 0;
|
|
271
|
+
function TranscriptAnchor(props: { children: ReactNode; href?: string }) {
|
|
272
|
+
// `safeTranscriptHref` removes unsafe destinations. Keep their label text.
|
|
273
|
+
if (!props.href) return <>{renderText(props.children)}</>;
|
|
413
274
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
match[1] ? (
|
|
425
|
-
<strong
|
|
426
|
-
className="font-semibold text-dashboard-text"
|
|
427
|
-
key={`${keyBase}-b-${part++}`}
|
|
428
|
-
>
|
|
429
|
-
{renderLinkText(match[2] ?? "", `${keyBase}-bc-${part}`)}
|
|
430
|
-
</strong>
|
|
275
|
+
const opensNewTab = /^https?:/i.test(props.href);
|
|
276
|
+
return (
|
|
277
|
+
<a
|
|
278
|
+
className={TRANSCRIPT_ANCHOR_CLASS}
|
|
279
|
+
href={props.href}
|
|
280
|
+
rel={opensNewTab ? "noreferrer" : undefined}
|
|
281
|
+
target={opensNewTab ? "_blank" : undefined}
|
|
282
|
+
>
|
|
283
|
+
{typeof props.children === "string" ? (
|
|
284
|
+
<SearchAwareLinkLabel href={props.href} label={props.children} />
|
|
431
285
|
) : (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
{renderLinkText(match[3] ?? "", `${keyBase}-ic-${part}`)}
|
|
437
|
-
</em>
|
|
438
|
-
),
|
|
439
|
-
);
|
|
440
|
-
cursor = match.index + match[0].length;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (cursor < text.length) {
|
|
444
|
-
nodes.push(...renderLinkText(text.slice(cursor), `${keyBase}-t-${part}`));
|
|
445
|
-
}
|
|
446
|
-
return nodes;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function renderLinkText(text: string, keyBase: string): ReactNode[] {
|
|
450
|
-
if (!text) return [];
|
|
451
|
-
|
|
452
|
-
const links = findTranscriptMarkdownLinks(text);
|
|
453
|
-
const nodes: ReactNode[] = [];
|
|
454
|
-
let cursor = 0;
|
|
455
|
-
let part = 0;
|
|
456
|
-
|
|
457
|
-
for (const link of links) {
|
|
458
|
-
if (link.start > cursor) {
|
|
459
|
-
nodes.push(
|
|
460
|
-
...renderMentionText(
|
|
461
|
-
text.slice(cursor, link.start),
|
|
462
|
-
`${keyBase}-s-${part++}`,
|
|
463
|
-
),
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
nodes.push(
|
|
467
|
-
<TranscriptAnchor href={link.href} key={`${keyBase}-a-${link.start}`}>
|
|
468
|
-
<SearchAwareLinkLabel href={link.href} label={link.label} />
|
|
469
|
-
</TranscriptAnchor>,
|
|
470
|
-
);
|
|
471
|
-
cursor = link.end;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (cursor < text.length) {
|
|
475
|
-
nodes.push(
|
|
476
|
-
...renderMentionText(text.slice(cursor), `${keyBase}-s-${part}`),
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
return nodes;
|
|
286
|
+
props.children
|
|
287
|
+
)}
|
|
288
|
+
</a>
|
|
289
|
+
);
|
|
480
290
|
}
|
|
481
291
|
|
|
482
292
|
function SearchAwareLinkLabel(props: { href: string; label: string }) {
|
|
@@ -500,17 +310,3 @@ function SearchAwareLinkLabel(props: { href: string; label: string }) {
|
|
|
500
310
|
|
|
501
311
|
return <HighlightText text={props.label} />;
|
|
502
312
|
}
|
|
503
|
-
|
|
504
|
-
function TranscriptAnchor(props: { children: ReactNode; href: string }) {
|
|
505
|
-
const opensNewTab = /^https?:/i.test(props.href);
|
|
506
|
-
return (
|
|
507
|
-
<a
|
|
508
|
-
className={TRANSCRIPT_ANCHOR_CLASS}
|
|
509
|
-
href={props.href}
|
|
510
|
-
rel={opensNewTab ? "noreferrer" : undefined}
|
|
511
|
-
target={opensNewTab ? "_blank" : undefined}
|
|
512
|
-
>
|
|
513
|
-
{props.children}
|
|
514
|
-
</a>
|
|
515
|
-
);
|
|
516
|
-
}
|