@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.
- package/dist/app.d.ts +2 -0
- package/dist/app.js +1285 -39
- package/dist/assets.d.ts +2 -0
- package/dist/client/api.d.ts +1 -1
- package/dist/client/components/Button.d.ts +15 -0
- package/dist/client/components/Metric.d.ts +22 -0
- package/dist/client/components/StatusBadge.d.ts +2 -1
- package/dist/client/components/TelemetryMetrics.d.ts +24 -0
- package/dist/client/components/ToolFrame.d.ts +3 -1
- package/dist/client/components/Transcript.d.ts +2 -0
- package/dist/client/components/TranscriptHeader.d.ts +2 -0
- package/dist/client/components/TranscriptHeadingRow.d.ts +16 -0
- package/dist/client/components/TranscriptThinkingView.d.ts +7 -0
- package/dist/client/components/TranscriptToolRun.d.ts +9 -0
- package/dist/client/components/transcriptRenderModel.d.ts +25 -9
- package/dist/client/format.d.ts +45 -8
- package/dist/client/markdownExport.d.ts +3 -0
- package/dist/client/toolInvocations.d.ts +7 -0
- package/dist/client/types.d.ts +17 -100
- package/dist/client.js +85 -55998
- package/dist/dashboardLoader.d.ts +1 -0
- package/dist/handler.js +1288 -40
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1807 -0
- package/dist/mock-conversations.d.ts +3 -0
- package/dist/mock-release-conversation.d.ts +3 -0
- package/dist/nitro.d.ts +7 -1
- package/dist/tailwind.css +1 -1
- package/dist/url.d.ts +13 -0
- package/package.json +9 -5
- package/src/app.ts +42 -18
- package/src/assets.ts +2 -0
- package/src/auth.ts +2 -35
- package/src/client/App.tsx +33 -74
- package/src/client/code.tsx +1 -1
- package/src/client/components/Button.tsx +75 -0
- package/src/client/components/ConversationRowStats.tsx +3 -2
- package/src/client/components/FilterTabs.tsx +10 -11
- package/src/client/components/LoadingView.tsx +7 -2
- package/src/client/components/Metric.tsx +159 -0
- package/src/client/components/StatusBadge.tsx +10 -1
- package/src/client/components/TelemetryMetrics.tsx +124 -0
- package/src/client/components/ToolFrame.tsx +57 -14
- package/src/client/components/Transcript.tsx +6 -2
- package/src/client/components/TranscriptHeader.tsx +18 -19
- package/src/client/components/TranscriptHeadingRow.tsx +64 -0
- package/src/client/components/TranscriptThinkingView.tsx +157 -0
- package/src/client/components/TranscriptToolRun.tsx +66 -0
- package/src/client/components/TranscriptToolView.tsx +16 -8
- package/src/client/components/TranscriptTurn.tsx +368 -132
- package/src/client/components/TurnDurationChart.tsx +236 -78
- package/src/client/components/transcriptRenderModel.ts +60 -20
- package/src/client/format.ts +329 -87
- package/src/client/markdownExport.ts +360 -0
- package/src/client/pages/CommandCenter.tsx +1 -1
- package/src/client/pages/ConversationPage.tsx +142 -45
- package/src/client/pages/ConversationsPage.tsx +1 -1
- package/src/client/toolInvocations.ts +16 -0
- package/src/client/types.ts +34 -90
- package/src/config.ts +4 -0
- package/src/dashboardLoader.ts +12 -0
- package/src/index.ts +78 -0
- package/src/mock-conversations.ts +726 -0
- package/src/mock-release-conversation.ts +605 -0
- package/src/nitro.ts +7 -1
- package/src/tailwind.css +11 -0
- package/src/url.ts +68 -0
package/src/client/format.ts
CHANGED
|
@@ -8,9 +8,12 @@ import type {
|
|
|
8
8
|
RequesterIdentity,
|
|
9
9
|
Session,
|
|
10
10
|
SessionFilter,
|
|
11
|
+
TranscriptMessage,
|
|
12
|
+
TranscriptPart,
|
|
11
13
|
TurnUsage,
|
|
12
14
|
VisualStatus,
|
|
13
15
|
} from "./types";
|
|
16
|
+
import { sameToolInvocation } from "./toolInvocations";
|
|
14
17
|
|
|
15
18
|
let dashboardTimeZone = "America/Los_Angeles";
|
|
16
19
|
|
|
@@ -24,7 +27,7 @@ function displayTimeZone(): string {
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
function isActiveSession(session: Session): boolean {
|
|
27
|
-
return session.status === "active"
|
|
30
|
+
return session.status === "active";
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/** Identify turn summaries that should appear in failed conversation filters. */
|
|
@@ -99,11 +102,22 @@ export function formatMs(value: number | undefined): string {
|
|
|
99
102
|
if (ms < 1000) return `${ms}ms`;
|
|
100
103
|
const seconds = ms / 1000;
|
|
101
104
|
if (seconds < 60) return `${seconds.toFixed(seconds < 10 ? 1 : 0)}s`;
|
|
102
|
-
const
|
|
103
|
-
const
|
|
105
|
+
const roundedSeconds = Math.round(seconds);
|
|
106
|
+
const minutes = Math.floor(roundedSeconds / 60);
|
|
107
|
+
const remainingSeconds = roundedSeconds % 60;
|
|
104
108
|
return `${minutes}m ${remainingSeconds}s`;
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
/** Format chart duration ticks without long labels wrapping on the Y axis. */
|
|
112
|
+
export function formatDurationTick(value: number | undefined): string {
|
|
113
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "none";
|
|
114
|
+
const ms = Math.max(0, Math.floor(value));
|
|
115
|
+
if (Math.round(ms / 1000) >= 10 * 60) {
|
|
116
|
+
return `${Math.round(ms / (60 * 1000))}m`;
|
|
117
|
+
}
|
|
118
|
+
return formatMs(ms);
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
/** Format aggregate runtime across turn summaries when duration data exists. */
|
|
108
122
|
export function formatDurationTotal(
|
|
109
123
|
durations: Array<number | undefined>,
|
|
@@ -250,66 +264,261 @@ export function turnMessageCount(turn: ConversationTurn): number {
|
|
|
250
264
|
return turn.transcriptMessageCount ?? 0;
|
|
251
265
|
}
|
|
252
266
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
267
|
+
export type ToolCallSummaryItem = {
|
|
268
|
+
count: number;
|
|
269
|
+
name: string;
|
|
270
|
+
totalDurationMs?: number;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export type ToolCallSummary = {
|
|
274
|
+
items: ToolCallSummaryItem[];
|
|
275
|
+
total: number;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
type PendingToolCall = {
|
|
279
|
+
id?: string;
|
|
280
|
+
name: string;
|
|
281
|
+
timestamp?: number;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
function toolCallName(part: TranscriptPart): string {
|
|
285
|
+
return part.name ?? part.id ?? "unknown";
|
|
260
286
|
}
|
|
261
287
|
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
)
|
|
288
|
+
function findPendingToolCallIndex(
|
|
289
|
+
calls: PendingToolCall[],
|
|
290
|
+
result: TranscriptPart,
|
|
291
|
+
): number {
|
|
292
|
+
for (let index = calls.length - 1; index >= 0; index -= 1) {
|
|
293
|
+
if (sameToolInvocation(calls[index]!, result)) {
|
|
294
|
+
return index;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return -1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Summarize tool calls and matched result durations from transcript metadata. */
|
|
301
|
+
export function summarizeToolCalls(
|
|
302
|
+
turns: ConversationTurn[],
|
|
303
|
+
limit = 5,
|
|
304
|
+
): ToolCallSummary {
|
|
305
|
+
const byName = new Map<string, ToolCallSummaryItem>();
|
|
306
|
+
let total = 0;
|
|
307
|
+
|
|
308
|
+
for (const turn of turns) {
|
|
309
|
+
const pending: PendingToolCall[] = [];
|
|
310
|
+
for (const message of transcriptSource(turn)) {
|
|
311
|
+
for (const part of message.parts) {
|
|
312
|
+
if (part.type === "tool_call") {
|
|
313
|
+
const name = toolCallName(part);
|
|
314
|
+
const item = byName.get(name) ?? { count: 0, name };
|
|
315
|
+
item.count += 1;
|
|
316
|
+
byName.set(name, item);
|
|
317
|
+
pending.push({
|
|
318
|
+
...(part.id ? { id: part.id } : {}),
|
|
319
|
+
name,
|
|
320
|
+
...(typeof message.timestamp === "number"
|
|
321
|
+
? { timestamp: message.timestamp }
|
|
322
|
+
: {}),
|
|
323
|
+
});
|
|
324
|
+
total += 1;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (part.type !== "tool_result") continue;
|
|
329
|
+
const pendingIndex = findPendingToolCallIndex(pending, part);
|
|
330
|
+
if (pendingIndex < 0) continue;
|
|
331
|
+
const [call] = pending.splice(pendingIndex, 1);
|
|
332
|
+
if (
|
|
333
|
+
!call ||
|
|
334
|
+
typeof call.timestamp !== "number" ||
|
|
335
|
+
typeof message.timestamp !== "number" ||
|
|
336
|
+
message.timestamp < call.timestamp
|
|
337
|
+
) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const item = byName.get(call.name);
|
|
341
|
+
if (!item) continue;
|
|
342
|
+
item.totalDurationMs =
|
|
343
|
+
(item.totalDurationMs ?? 0) + (message.timestamp - call.timestamp);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const items = [...byName.values()]
|
|
349
|
+
.sort(
|
|
350
|
+
(left, right) =>
|
|
351
|
+
right.count - left.count ||
|
|
352
|
+
(right.totalDurationMs ?? 0) - (left.totalDurationMs ?? 0) ||
|
|
353
|
+
left.name.localeCompare(right.name),
|
|
354
|
+
)
|
|
355
|
+
.slice(0, limit);
|
|
356
|
+
|
|
357
|
+
return { items, total };
|
|
267
358
|
}
|
|
268
359
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
360
|
+
export type MessageSummaryItem = {
|
|
361
|
+
author: string;
|
|
362
|
+
bytes: number;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export type MessageSummary = {
|
|
366
|
+
items: MessageSummaryItem[];
|
|
367
|
+
total: number;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
function transcriptMessageAuthor(
|
|
371
|
+
turn: ConversationTurn,
|
|
372
|
+
message: TranscriptMessage,
|
|
373
|
+
): string {
|
|
374
|
+
const kind = transcriptRoleKind(message.role);
|
|
375
|
+
if (kind === "assistant") return "Junior";
|
|
376
|
+
if (kind === "user") {
|
|
377
|
+
return requesterLabel(turn.requesterIdentity) ?? "User";
|
|
378
|
+
}
|
|
379
|
+
if (kind === "system") return "System";
|
|
380
|
+
if (kind === "tool") return "Tool";
|
|
381
|
+
return message.role || "Unknown";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function transcriptPartBytes(part: TranscriptPart): number {
|
|
385
|
+
if (typeof part.bytes === "number" && Number.isFinite(part.bytes)) {
|
|
386
|
+
return Math.max(0, Math.floor(part.bytes));
|
|
387
|
+
}
|
|
388
|
+
if (
|
|
389
|
+
typeof part.inputSizeBytes === "number" &&
|
|
390
|
+
Number.isFinite(part.inputSizeBytes)
|
|
391
|
+
) {
|
|
392
|
+
return Math.max(0, Math.floor(part.inputSizeBytes));
|
|
393
|
+
}
|
|
394
|
+
if (
|
|
395
|
+
typeof part.outputSizeBytes === "number" &&
|
|
396
|
+
Number.isFinite(part.outputSizeBytes)
|
|
397
|
+
) {
|
|
398
|
+
return Math.max(0, Math.floor(part.outputSizeBytes));
|
|
399
|
+
}
|
|
400
|
+
return new TextEncoder().encode(
|
|
401
|
+
stringifyPartValue(part.text ?? part.input ?? part.output ?? part),
|
|
402
|
+
).byteLength;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Summarize conversational messages by author and serialized size. */
|
|
406
|
+
export function summarizeMessages(turns: ConversationTurn[]): MessageSummary {
|
|
407
|
+
const items: MessageSummaryItem[] = [];
|
|
408
|
+
|
|
409
|
+
for (const turn of turns) {
|
|
410
|
+
for (const message of transcriptSource(turn)) {
|
|
411
|
+
if (!isConversationMessage(message)) continue;
|
|
412
|
+
items.push({
|
|
413
|
+
author: transcriptMessageAuthor(turn, message),
|
|
414
|
+
bytes: message.parts.reduce(
|
|
415
|
+
(sum, part) => sum + transcriptPartBytes(part),
|
|
416
|
+
0,
|
|
417
|
+
),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return { items, total: items.length };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Format raw counts with the dashboard's compact number rules. */
|
|
426
|
+
export function formatCompactNumber(value: number | undefined): string {
|
|
427
|
+
return formatNumber(value);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export type TokenUsageSummary = {
|
|
431
|
+
cachedInputTokens?: number;
|
|
432
|
+
cacheCreationTokens?: number;
|
|
433
|
+
inputTokens?: number;
|
|
434
|
+
outputTokens?: number;
|
|
435
|
+
providerTotalTokens?: number;
|
|
436
|
+
totalTokens: number;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
function addOptionalCount(
|
|
440
|
+
left: number | undefined,
|
|
441
|
+
right: number | undefined,
|
|
442
|
+
): number | undefined {
|
|
443
|
+
return right === undefined ? left : (left ?? 0) + right;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Summarize token usage without double-counting provider total fields. */
|
|
447
|
+
export function summarizeUsage(
|
|
448
|
+
usages: Array<TurnUsage | undefined>,
|
|
449
|
+
): TokenUsageSummary | undefined {
|
|
450
|
+
const summary: TokenUsageSummary = { totalTokens: 0 };
|
|
451
|
+
|
|
452
|
+
for (const usage of usages) {
|
|
453
|
+
if (!usage) continue;
|
|
454
|
+
|
|
455
|
+
const componentTotal = getUsageComponentTotal(usage);
|
|
456
|
+
if (componentTotal !== undefined) {
|
|
457
|
+
summary.totalTokens += componentTotal;
|
|
458
|
+
summary.inputTokens = addOptionalCount(
|
|
459
|
+
summary.inputTokens,
|
|
460
|
+
getFiniteTokenCount(usage.inputTokens),
|
|
461
|
+
);
|
|
462
|
+
summary.outputTokens = addOptionalCount(
|
|
463
|
+
summary.outputTokens,
|
|
464
|
+
getFiniteTokenCount(usage.outputTokens),
|
|
465
|
+
);
|
|
466
|
+
summary.cachedInputTokens = addOptionalCount(
|
|
467
|
+
summary.cachedInputTokens,
|
|
468
|
+
getFiniteTokenCount(usage.cachedInputTokens),
|
|
469
|
+
);
|
|
470
|
+
summary.cacheCreationTokens = addOptionalCount(
|
|
471
|
+
summary.cacheCreationTokens,
|
|
472
|
+
getFiniteTokenCount(usage.cacheCreationTokens),
|
|
473
|
+
);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const providerTotal = getFiniteTokenCount(usage.totalTokens);
|
|
478
|
+
if (providerTotal !== undefined) {
|
|
479
|
+
summary.totalTokens += providerTotal;
|
|
480
|
+
summary.providerTotalTokens =
|
|
481
|
+
(summary.providerTotalTokens ?? 0) + providerTotal;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return summary.totalTokens > 0 ? summary : undefined;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Format a summarized token counter for compact metadata. */
|
|
489
|
+
export function formatTokenSummary(
|
|
490
|
+
summary: TokenUsageSummary | undefined,
|
|
491
|
+
): string {
|
|
492
|
+
return summary ? `${formatNumber(summary.totalTokens)} tokens` : "";
|
|
273
493
|
}
|
|
274
494
|
|
|
275
495
|
/** Format the aggregate token count across conversation turns. */
|
|
276
496
|
export function formatUsageTotal(usages: Array<TurnUsage | undefined>): string {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
: undefined,
|
|
296
|
-
usage?.cachedInputTokens !== undefined
|
|
297
|
-
? `${formatNumber(usage.cachedInputTokens)} cached`
|
|
298
|
-
: undefined,
|
|
299
|
-
usage?.cacheCreationTokens !== undefined
|
|
300
|
-
? `${formatNumber(usage.cacheCreationTokens)} cache-write`
|
|
301
|
-
: undefined,
|
|
302
|
-
].filter(Boolean);
|
|
303
|
-
return pieces.length > 0
|
|
304
|
-
? `${formatNumber(total)} tokens (${pieces.join(" / ")})`
|
|
305
|
-
: `${formatNumber(total)} tokens`;
|
|
497
|
+
return formatTokenSummary(summarizeUsage(usages));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Keep turn duration displays aligned on elapsed transcript time. */
|
|
501
|
+
export function turnElapsedDurationMs(
|
|
502
|
+
turn: Pick<ConversationTurn, "completedAt" | "lastSeenAt" | "startedAt">,
|
|
503
|
+
): number | undefined {
|
|
504
|
+
const start = parseTime(turn.startedAt);
|
|
505
|
+
const end = parseTime(turn.completedAt ?? turn.lastSeenAt);
|
|
506
|
+
if (start == null || end == null || end < start) return undefined;
|
|
507
|
+
return Math.max(0, end - start);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Format elapsed turn time for chart dots and transcript metadata. */
|
|
511
|
+
export function formatTurnDuration(
|
|
512
|
+
turn: Pick<ConversationTurn, "completedAt" | "lastSeenAt" | "startedAt">,
|
|
513
|
+
): string {
|
|
514
|
+
return formatMs(turnElapsedDurationMs(turn));
|
|
306
515
|
}
|
|
307
516
|
|
|
308
517
|
/** Format a conversation span from first turn start to latest activity. */
|
|
309
518
|
export function formatConversationDuration(conversation: Conversation): string {
|
|
310
519
|
const start = parseTime(conversation.startedAt);
|
|
311
|
-
const end = parseTime(conversation.lastSeenAt)
|
|
312
|
-
if (start == null || end < start) return "none";
|
|
520
|
+
const end = parseTime(conversation.lastSeenAt);
|
|
521
|
+
if (start == null || end == null || end < start) return "none";
|
|
313
522
|
const seconds = Math.max(1, Math.round((end - start) / 1000));
|
|
314
523
|
if (seconds < 60) return `${seconds}s`;
|
|
315
524
|
const minutes = Math.round(seconds / 60);
|
|
@@ -319,7 +528,7 @@ export function formatConversationDuration(conversation: Conversation): string {
|
|
|
319
528
|
|
|
320
529
|
/** Resolve the owning conversation id for a turn/session summary. */
|
|
321
530
|
export function conversationIdForSession(session: Session): string {
|
|
322
|
-
return session.conversationId
|
|
531
|
+
return session.conversationId;
|
|
323
532
|
}
|
|
324
533
|
|
|
325
534
|
function compareTimeDesc(a: string | undefined, b: string | undefined): number {
|
|
@@ -330,7 +539,27 @@ function compareTimeAsc(a: string | undefined, b: string | undefined): number {
|
|
|
330
539
|
return (parseTime(a) ?? 0) - (parseTime(b) ?? 0);
|
|
331
540
|
}
|
|
332
541
|
|
|
542
|
+
function isGenericTurnTitle(title: string, conversationId: string): boolean {
|
|
543
|
+
const normalized = title.trim();
|
|
544
|
+
return (
|
|
545
|
+
normalized.length === 0 ||
|
|
546
|
+
normalized === conversationId ||
|
|
547
|
+
/^Turn\s+\S+$/i.test(normalized) ||
|
|
548
|
+
/^Awaiting\s+\w+\s+resume$/i.test(normalized)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function meaningfulConversationTitle(
|
|
553
|
+
conversation: Conversation,
|
|
554
|
+
): string | undefined {
|
|
555
|
+
return isGenericTurnTitle(conversation.title, conversation.id)
|
|
556
|
+
? undefined
|
|
557
|
+
: conversation.title;
|
|
558
|
+
}
|
|
559
|
+
|
|
333
560
|
function getConversationTitle(conversation: Conversation): string {
|
|
561
|
+
const title = meaningfulConversationTitle(conversation);
|
|
562
|
+
if (title) return title;
|
|
334
563
|
if (conversation.surface === "slack") {
|
|
335
564
|
return (
|
|
336
565
|
slackLocationLabel(conversation, { includeId: false }) ??
|
|
@@ -351,15 +580,18 @@ export function conversationDisplayTitle(
|
|
|
351
580
|
/** Prefer stable requester identifiers while keeping Slack ids as a last resort. */
|
|
352
581
|
export function requesterLabel(
|
|
353
582
|
requester: RequesterIdentity | undefined,
|
|
354
|
-
fallback: string | undefined,
|
|
355
583
|
): string | undefined {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
584
|
+
const email = requester?.email?.trim() || undefined;
|
|
585
|
+
const fullName = requester?.fullName?.trim() || undefined;
|
|
586
|
+
const slackUserName = requester?.slackUserName?.trim() || undefined;
|
|
587
|
+
return email ?? fullName ?? slackUserName ?? requester?.slackUserId;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Derive the conversation owner label from structured requester identity. */
|
|
591
|
+
export function conversationRequesterLabel(
|
|
592
|
+
conversation: Conversation | undefined,
|
|
593
|
+
): string | undefined {
|
|
594
|
+
return requesterLabel(conversation?.requesterIdentity);
|
|
363
595
|
}
|
|
364
596
|
|
|
365
597
|
/** Format the owner and permalink id line shared by conversation rows and headers. */
|
|
@@ -367,20 +599,15 @@ export function conversationIdentityMeta(
|
|
|
367
599
|
conversation: Conversation | undefined,
|
|
368
600
|
conversationId: string | undefined,
|
|
369
601
|
): string {
|
|
370
|
-
const id = conversationId ??
|
|
371
|
-
const owner =
|
|
372
|
-
|
|
373
|
-
conversation?.requester,
|
|
374
|
-
);
|
|
602
|
+
const id = conversationId ?? conversation?.id;
|
|
603
|
+
const owner = conversationRequesterLabel(conversation);
|
|
604
|
+
if (!id) return owner ?? "";
|
|
375
605
|
return owner ? `${owner} · ${id}` : id;
|
|
376
606
|
}
|
|
377
607
|
|
|
378
608
|
/** Convert Slack channel ids and names into user-facing location labels. */
|
|
379
609
|
export function slackLocationLabel(
|
|
380
|
-
input: Pick<
|
|
381
|
-
Session,
|
|
382
|
-
"channel" | "channelName" | "requester" | "requesterIdentity"
|
|
383
|
-
>,
|
|
610
|
+
input: Pick<Session, "channel" | "channelName">,
|
|
384
611
|
options: { includeId?: boolean } = {},
|
|
385
612
|
): string | undefined {
|
|
386
613
|
const channelId = input.channel;
|
|
@@ -479,6 +706,18 @@ export function detectLanguage(text: string): BundledLanguage {
|
|
|
479
706
|
if (/^<[\s\S]+>$/.test(trimmed) && /<\/?[a-zA-Z][^>]*>/.test(trimmed)) {
|
|
480
707
|
return "xml";
|
|
481
708
|
}
|
|
709
|
+
// Mixed prose + block-level XML: detect when a complete open/close element pair
|
|
710
|
+
// appears on its own lines. Handles system prompts and runtime context blocks
|
|
711
|
+
// that start with plain text but contain structured XML sections.
|
|
712
|
+
const blockOpen = trimmed.match(
|
|
713
|
+
/(?:^|\n)[ \t]*<([A-Za-z_][\w:.-]*)(?:[ \t][^<>]*)?>[ \t]*(?=\n|$)/,
|
|
714
|
+
);
|
|
715
|
+
if (blockOpen?.[1]) {
|
|
716
|
+
const tag = blockOpen[1].replace(/[$()*+.?[\\^{|}]/g, "\\$&");
|
|
717
|
+
if (new RegExp(`(?:^|\\n)[ \\t]*</${tag}>[ \\t]*(?=\\n|$)`).test(trimmed)) {
|
|
718
|
+
return "xml";
|
|
719
|
+
}
|
|
720
|
+
}
|
|
482
721
|
if (/```|^#{1,6}\s|\n[-*]\s|\n\d+\.\s|\[[^\]]+\]\([^)]+\)/m.test(trimmed)) {
|
|
483
722
|
return "markdown";
|
|
484
723
|
}
|
|
@@ -575,7 +814,11 @@ export function parseMarkdownBlocks(
|
|
|
575
814
|
const prose = text.slice(cursor, match.index).trim();
|
|
576
815
|
if (prose) {
|
|
577
816
|
const language = detectProse(prose);
|
|
578
|
-
blocks.push({
|
|
817
|
+
blocks.push({
|
|
818
|
+
code: formatCodeBlock(prose, language),
|
|
819
|
+
fenced: false,
|
|
820
|
+
language,
|
|
821
|
+
});
|
|
579
822
|
}
|
|
580
823
|
const language = normalizeLanguage(match[1]);
|
|
581
824
|
blocks.push({
|
|
@@ -588,7 +831,11 @@ export function parseMarkdownBlocks(
|
|
|
588
831
|
const rest = text.slice(cursor).trim();
|
|
589
832
|
if (rest) {
|
|
590
833
|
const language = detectProse(rest);
|
|
591
|
-
blocks.push({
|
|
834
|
+
blocks.push({
|
|
835
|
+
code: formatCodeBlock(rest, language),
|
|
836
|
+
fenced: false,
|
|
837
|
+
language,
|
|
838
|
+
});
|
|
592
839
|
}
|
|
593
840
|
if (blocks.length > 0) return blocks;
|
|
594
841
|
const language = detectProse(text);
|
|
@@ -655,12 +902,10 @@ export function buildConversations(sessions: Session[]): Conversation[] {
|
|
|
655
902
|
const sortedTurns = [...turns].sort((a, b) =>
|
|
656
903
|
compareTimeAsc(a.startedAt, b.startedAt),
|
|
657
904
|
);
|
|
658
|
-
const
|
|
659
|
-
compareTimeDesc(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
),
|
|
663
|
-
)[0]!;
|
|
905
|
+
const recentTurns = [...turns].sort((a, b) =>
|
|
906
|
+
compareTimeDesc(a.lastSeenAt, b.lastSeenAt),
|
|
907
|
+
);
|
|
908
|
+
const newest = recentTurns[0]!;
|
|
664
909
|
const oldest = sortedTurns.reduce((current, next) =>
|
|
665
910
|
(parseTime(next.startedAt) ?? Number.MAX_SAFE_INTEGER) <
|
|
666
911
|
(parseTime(current.startedAt) ?? Number.MAX_SAFE_INTEGER)
|
|
@@ -674,28 +919,25 @@ export function buildConversations(sessions: Session[]): Conversation[] {
|
|
|
674
919
|
: sortedTurns.some(isFailedSession)
|
|
675
920
|
? "failed"
|
|
676
921
|
: newest.status;
|
|
677
|
-
const requesterTurn =
|
|
678
|
-
|
|
679
|
-
|
|
922
|
+
const requesterTurn = sortedTurns.find((turn) => turn.requesterIdentity);
|
|
923
|
+
const conversationTitle = recentTurns.find(
|
|
924
|
+
(turn) => turn.conversationTitle,
|
|
925
|
+
)?.conversationTitle;
|
|
680
926
|
|
|
681
927
|
return {
|
|
682
928
|
channel: newest.channel,
|
|
683
|
-
channelName:
|
|
684
|
-
conversationTitle
|
|
685
|
-
?.conversationTitle,
|
|
929
|
+
channelName: recentTurns.find((turn) => turn.channelName)?.channelName,
|
|
930
|
+
conversationTitle,
|
|
686
931
|
id,
|
|
932
|
+
lastProgressAt: newest.lastProgressAt,
|
|
687
933
|
lastSeenAt: newest.lastSeenAt,
|
|
688
|
-
requester: requesterLabel(
|
|
689
|
-
requesterTurn?.requesterIdentity,
|
|
690
|
-
requesterTurn?.requester,
|
|
691
|
-
),
|
|
692
934
|
requesterIdentity: requesterTurn?.requesterIdentity,
|
|
693
935
|
sentryConversationUrl: newest.sentryConversationUrl,
|
|
694
936
|
sentryTraceUrl: newest.sentryTraceUrl,
|
|
695
937
|
startedAt: oldest.startedAt,
|
|
696
938
|
status,
|
|
697
939
|
surface: newest.surface,
|
|
698
|
-
title: newest.title
|
|
940
|
+
title: newest.title,
|
|
699
941
|
traceId: newest.traceId,
|
|
700
942
|
turns: sortedTurns,
|
|
701
943
|
};
|