@sentry/junior-dashboard 0.279.0 → 0.280.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 (147) hide show
  1. package/dist/app.js +100 -31
  2. package/dist/client-manifest.json +1 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +100 -31
  5. package/dist/public/chunks/AdminPage-YEKZOKH6.js +1 -0
  6. package/dist/public/chunks/AutomationPage-5GS3IUAK.js +3 -0
  7. package/dist/public/chunks/AutomationRunsPage-IPYEZU4L.js +1 -0
  8. package/dist/public/chunks/AutomationsPage-HSAYPC3C.js +1 -0
  9. package/dist/public/chunks/CodePage-IPFKXWFB.js +1 -0
  10. package/dist/public/chunks/ConnectionsPage-5XUEK6FS.js +1 -0
  11. package/dist/public/chunks/DrawerContent-RYYS57ZH.js +1 -0
  12. package/dist/public/chunks/LocationDetailPage-27ETUGR6.js +1 -0
  13. package/dist/public/chunks/LocationsPage-WOKMQAGM.js +1 -0
  14. package/dist/public/chunks/McpSettingsPage-3RT7JPEK.js +1 -0
  15. package/dist/public/chunks/PeoplePage-6YQLDOUL.js +1 -0
  16. package/dist/public/chunks/PersonProfilePage-GYMAZFIK.js +1 -0
  17. package/dist/public/chunks/PersonalTokensPage-4BCR744B.js +1 -0
  18. package/dist/public/chunks/PluginUserPage-IP6WL7MD.js +1 -0
  19. package/dist/public/chunks/RepositoryPage-TRD44LQ3.js +1 -0
  20. package/dist/public/chunks/SettingsPage-4E3SLQ36.js +1 -0
  21. package/dist/public/chunks/SystemPage-C3XBKXKW.js +1 -0
  22. package/dist/public/chunks/WorkspaceFormPage-2LS2X67Q.js +2 -0
  23. package/dist/public/chunks/WorkspacesPage-5WB3CUTX.js +1 -0
  24. package/dist/public/chunks/chunk-2BCS5AQL.js +1 -0
  25. package/dist/public/chunks/chunk-3Y3ICT6K.js +1 -0
  26. package/dist/public/chunks/chunk-4AKUZW6R.js +1 -0
  27. package/dist/public/chunks/chunk-4UGZNVJM.js +1 -0
  28. package/dist/public/chunks/chunk-564V7YHG.js +1 -0
  29. package/dist/public/chunks/chunk-5GZDBGAR.js +1 -0
  30. package/dist/public/chunks/chunk-5ISD3QSC.js +1 -0
  31. package/dist/public/chunks/chunk-5LNGNJ3P.js +1 -0
  32. package/dist/public/chunks/chunk-5ZA674OU.js +1 -0
  33. package/dist/public/chunks/chunk-6FSJ3W2W.js +1 -0
  34. package/dist/public/chunks/chunk-6GQL3EVJ.js +1 -0
  35. package/dist/public/chunks/chunk-6PHJZPWD.js +19 -0
  36. package/dist/public/chunks/chunk-A2QWWA7O.js +41 -0
  37. package/dist/public/chunks/chunk-AYXSBMWA.js +1 -0
  38. package/dist/public/chunks/chunk-B5ZN3SWQ.js +14 -0
  39. package/dist/public/chunks/chunk-B6HGRBBX.js +1 -0
  40. package/dist/public/chunks/chunk-BKOACADP.js +1 -0
  41. package/dist/public/chunks/chunk-CQY6BYUB.js +1 -0
  42. package/dist/public/chunks/chunk-CR2ICLSH.js +1 -0
  43. package/dist/public/chunks/chunk-DZB4LPI6.js +1 -0
  44. package/dist/public/chunks/chunk-EFEREKX4.js +1 -0
  45. package/dist/public/chunks/chunk-FE522UEA.js +1 -0
  46. package/dist/public/chunks/chunk-FVX42XAD.js +1 -0
  47. package/dist/public/chunks/chunk-G5NWYJTV.js +1 -0
  48. package/dist/public/chunks/chunk-GMZIELFU.js +1 -0
  49. package/dist/public/chunks/chunk-GO45RQDL.js +1 -0
  50. package/dist/public/chunks/chunk-GOTQ4D6W.js +1 -0
  51. package/dist/public/chunks/chunk-GRMX2HX6.js +1 -0
  52. package/dist/public/chunks/chunk-GYRM5ZIV.js +1 -0
  53. package/dist/public/chunks/chunk-H2ZU34CP.js +1 -0
  54. package/dist/public/chunks/chunk-H76B43OQ.js +1 -0
  55. package/dist/public/chunks/chunk-HCLRG2EC.js +1 -0
  56. package/dist/public/chunks/chunk-IA6BJDKN.js +1 -0
  57. package/dist/public/chunks/chunk-IZWTFLCC.js +1 -0
  58. package/dist/public/chunks/chunk-J37MXUED.js +1 -0
  59. package/dist/public/chunks/chunk-JAZABOBA.js +0 -0
  60. package/dist/public/chunks/chunk-L2H3ZWZN.js +1 -0
  61. package/dist/public/chunks/chunk-LBELRO5P.js +1 -0
  62. package/dist/public/chunks/chunk-LKW7MULF.js +1 -0
  63. package/dist/public/chunks/chunk-MEHLQZDG.js +3 -0
  64. package/dist/public/chunks/chunk-MTBUV2GJ.js +1 -0
  65. package/dist/public/chunks/chunk-N4AONKD3.js +1 -0
  66. package/dist/public/chunks/chunk-NQMDED2R.js +2 -0
  67. package/dist/public/chunks/chunk-NQOJAZFQ.js +1 -0
  68. package/dist/public/chunks/chunk-PEQNT5MO.js +1 -0
  69. package/dist/public/chunks/chunk-PQPAPPEI.js +1 -0
  70. package/dist/public/chunks/chunk-PXHLJIOT.js +1 -0
  71. package/dist/public/chunks/chunk-QNA7F7QB.js +1 -0
  72. package/dist/public/chunks/chunk-QOX5RMLM.js +1 -0
  73. package/dist/public/chunks/chunk-QPBLZSNX.js +1 -0
  74. package/dist/public/chunks/chunk-R4ORRE77.js +1 -0
  75. package/dist/public/chunks/chunk-RHIXHBS5.js +1 -0
  76. package/dist/public/chunks/chunk-RHMZNAJX.js +1 -0
  77. package/dist/public/chunks/chunk-SDKV3OID.js +1 -0
  78. package/dist/public/chunks/chunk-SIOW7OOW.js +1 -0
  79. package/dist/public/chunks/chunk-SUX4QBVF.js +1 -0
  80. package/dist/public/chunks/chunk-T4M53QQT.js +1 -0
  81. package/dist/public/chunks/chunk-TFJOEF3Q.js +1 -0
  82. package/dist/public/chunks/chunk-UAH6DWNS.js +1 -0
  83. package/dist/public/chunks/chunk-UJD3MBMB.js +1 -0
  84. package/dist/public/chunks/chunk-UK2V4UBT.js +1 -0
  85. package/dist/public/chunks/chunk-UT6HZPGL.js +1 -0
  86. package/dist/public/chunks/chunk-UZ44J2LH.js +1 -0
  87. package/dist/public/chunks/chunk-V5UDSIRA.js +63 -0
  88. package/dist/public/chunks/chunk-VC6IS564.js +1 -0
  89. package/dist/public/chunks/chunk-VFEBNOPX.js +1 -0
  90. package/dist/public/chunks/chunk-VXT3FIVL.js +55 -0
  91. package/dist/public/chunks/chunk-WJ3OXMVX.js +1 -0
  92. package/dist/public/chunks/chunk-WRXMQB7X.js +25 -0
  93. package/dist/public/chunks/chunk-WZVO3HG4.js +1 -0
  94. package/dist/public/chunks/chunk-XNR4RZEC.js +1 -0
  95. package/dist/public/chunks/chunk-YPMUHXYT.js +147 -0
  96. package/dist/public/chunks/chunk-YYNMZ4WC.js +1 -0
  97. package/dist/public/chunks/chunk-Z2JPRGFT.js +1 -0
  98. package/dist/public/chunks/chunk-ZIJ6SJDN.js +1 -0
  99. package/dist/public/chunks/code-highlighter-Q7WC7AEF.js +12 -0
  100. package/dist/public/client-TYC7JKRE.js +91 -0
  101. package/dist/tailwind.css +1 -1
  102. package/package.json +5 -4
  103. package/src/api/schema.ts +1 -1
  104. package/src/assets.ts +4 -1
  105. package/src/client/App.tsx +100 -23
  106. package/src/client/TracedRoutes.tsx +8 -4
  107. package/src/client/code-highlighter.ts +26 -0
  108. package/src/client/code.tsx +2 -22
  109. package/src/client/components/Drawer.tsx +12 -71
  110. package/src/client/components/DrawerContent.tsx +76 -0
  111. package/src/client/components/ProfileMenu.tsx +12 -3
  112. package/src/client/components/UserInputForm.tsx +28 -5
  113. package/src/client/conversations/ConversationCatchUp.tsx +107 -0
  114. package/src/client/conversations/ConversationContextTags.tsx +110 -0
  115. package/src/client/conversations/ConversationEventLog.tsx +213 -71
  116. package/src/client/conversations/ConversationHeader.tsx +35 -41
  117. package/src/client/conversations/ConversationHeaderActions.tsx +34 -38
  118. package/src/client/conversations/ConversationHomeList.tsx +41 -3
  119. package/src/client/conversations/ConversationMeta.tsx +6 -12
  120. package/src/client/conversations/ConversationOverflowMenu.tsx +15 -4
  121. package/src/client/conversations/ConversationPage.tsx +188 -150
  122. package/src/client/conversations/ShareConversationButton.tsx +5 -50
  123. package/src/client/conversations/SlackMark.tsx +16 -4
  124. package/src/client/conversations/TranscriptRows.tsx +24 -11
  125. package/src/client/conversations/TranscriptView.tsx +21 -13
  126. package/src/client/conversations/conversationArchive.ts +22 -0
  127. package/src/client/conversations/conversationCatchUpState.ts +96 -0
  128. package/src/client/conversations/conversationMemory.ts +8 -1
  129. package/src/client/conversations/queries.ts +11 -3
  130. package/src/client/conversations/{transcriptUnreadHistory.ts → transcriptAroundHistory.ts} +48 -42
  131. package/src/client/conversations/transcriptBottomPinning.ts +27 -3
  132. package/src/client/conversations/transcriptReadPosition.ts +24 -2
  133. package/src/client/conversations/turnContext.ts +1 -1
  134. package/src/client/pages/admin/AdminPage.tsx +1 -1
  135. package/src/client/pages/admin/ServiceAccountsSection.tsx +1 -1
  136. package/src/client/pages/memory/memoryDashboard.ts +1 -1
  137. package/src/client/pages/memory/memoryRecord.ts +1 -1
  138. package/src/client/pages/settings/ConnectionsPage.tsx +1 -1
  139. package/src/client/pages/user/PluginUserPage.tsx +0 -6
  140. package/src/client/pages/user/pluginUserPageRoutes.ts +5 -0
  141. package/src/client/primaryNav.ts +1 -1
  142. package/src/client/review-desk/reviewItemFormat.ts +1 -1
  143. package/src/mock-reporting/fixtures.ts +43 -0
  144. package/src/mock-reporting/routes.ts +40 -3
  145. package/src/public-assets.ts +28 -20
  146. package/src/shell.ts +12 -2
  147. package/dist/public/client-ZCZXZBTL.js +0 -460
@@ -1,5 +1,5 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
- import { z } from "zod";
2
+ import * as z from "zod";
3
3
 
4
4
  import { fetchDashboardJson } from "../../http";
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
- import { z } from "zod";
2
+ import * as z from "zod";
3
3
 
4
4
  import { fetchDashboardJson } from "../../http";
5
5
  import type { PluginUserPageRecord } from "../user/pluginUserPageData";
@@ -1,6 +1,6 @@
1
1
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
2
  import { Plug } from "lucide-react";
3
- import type { z } from "zod";
3
+ import type * as z from "zod";
4
4
  import {
5
5
  connectionListSchema,
6
6
  disconnectConnectionResponseSchema,
@@ -27,12 +27,6 @@ import {
27
27
  type PluginUserPageRecordAction,
28
28
  } from "./pluginUserPageData";
29
29
 
30
- /** Build the canonical dashboard path for a plugin-owned page. */
31
- export function pluginUserPagePath(pluginName: string, pageId: string): string {
32
- if (pluginName === "memory" && pageId === "memories") return "/memories";
33
- return `/plugins/${encodeURIComponent(pluginName)}/${encodeURIComponent(pageId)}`;
34
- }
35
-
36
30
  /** Render the memory library for its first-class permalink route. */
37
31
  export function MemoryPermalinkRoute(props: { pages: PluginUserPageLink[] }) {
38
32
  const page = props.pages.find(
@@ -0,0 +1,5 @@
1
+ /** Build the canonical dashboard path for a plugin-owned page. */
2
+ export function pluginUserPagePath(pluginName: string, pageId: string): string {
3
+ if (pluginName === "memory" && pageId === "memories") return "/memories";
4
+ return `/plugins/${encodeURIComponent(pluginName)}/${encodeURIComponent(pageId)}`;
5
+ }
@@ -1,7 +1,7 @@
1
1
  import type { PluginUserPageLink } from "@sentry/junior-plugin-api";
2
2
 
3
3
  import type { DashboardHeaderNavItem } from "./components/layout/DashboardHeader";
4
- import { pluginUserPagePath } from "./pages/user/PluginUserPage";
4
+ import { pluginUserPagePath } from "./pages/user/pluginUserPageRoutes";
5
5
 
6
6
  const AUTH_PRIMARY_NAV_PREFIXES = [
7
7
  "/admin",
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  import type {
3
3
  ConversationReportEvent,
4
4
  ReviewItemPayload,
@@ -393,6 +393,7 @@ function activeConversation(nowMs: number): ConversationDetailReport {
393
393
  kind: "resource_link",
394
394
  key: "getsentry/payments#42",
395
395
  label: "getsentry/payments#42",
396
+ objectType: "code_change",
396
397
  plugin: "github",
397
398
  status: "open",
398
399
  url: "https://github.com/getsentry/payments/pull/42",
@@ -685,6 +686,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
685
686
  kind: "resource_link",
686
687
  key: "getsentry/getsentry#21571",
687
688
  label: "getsentry/getsentry#21571",
689
+ objectType: "code_change",
688
690
  plugin: "github",
689
691
  status: "merged",
690
692
  url: "https://github.com/getsentry/getsentry/pull/21571",
@@ -695,6 +697,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
695
697
  kind: "resource_link",
696
698
  key: "getsentry/getsentry#21572",
697
699
  label: "getsentry/getsentry#21572",
700
+ objectType: "code_change",
698
701
  plugin: "github",
699
702
  status: "open",
700
703
  url: "https://github.com/getsentry/getsentry/pull/21572",
@@ -705,6 +708,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
705
708
  kind: "resource_link",
706
709
  key: "getsentry/getsentry#21569",
707
710
  label: "getsentry/getsentry#21569",
711
+ objectType: "code_change",
708
712
  plugin: "github",
709
713
  status: "draft",
710
714
  url: "https://github.com/getsentry/getsentry/pull/21569",
@@ -715,6 +719,7 @@ function dashboardQaConversation(nowMs: number): ConversationDetailReport {
715
719
  kind: "resource_link",
716
720
  key: "getsentry/sentry#121727",
717
721
  label: "getsentry/sentry#121727",
722
+ objectType: "code_change",
718
723
  plugin: "github",
719
724
  status: "merged",
720
725
  url: "https://github.com/getsentry/sentry/pull/121727",
@@ -1180,6 +1185,7 @@ function longConversation(nowMs: number): ConversationDetailReport {
1180
1185
  kind: "resource_link",
1181
1186
  key: "getsentry/junior#2201",
1182
1187
  label: "getsentry/junior#2201",
1188
+ objectType: "code_change",
1183
1189
  plugin: "github",
1184
1190
  status: "draft",
1185
1191
  url: "https://github.com/getsentry/junior/pull/2201",
@@ -1190,6 +1196,7 @@ function longConversation(nowMs: number): ConversationDetailReport {
1190
1196
  kind: "resource_link",
1191
1197
  key: "getsentry/payments#91",
1192
1198
  label: "getsentry/payments#91",
1199
+ objectType: "code_change",
1193
1200
  plugin: "github",
1194
1201
  status: "open",
1195
1202
  url: "https://github.com/getsentry/payments/pull/91",
@@ -1200,6 +1207,7 @@ function longConversation(nowMs: number): ConversationDetailReport {
1200
1207
  kind: "resource_link",
1201
1208
  key: "getsentry/relay#44",
1202
1209
  label: "getsentry/relay#44",
1210
+ objectType: "code_change",
1203
1211
  plugin: "github",
1204
1212
  status: "merged",
1205
1213
  url: "https://github.com/getsentry/relay/pull/44",
@@ -1268,6 +1276,7 @@ function incidentConversation(nowMs: number): ConversationDetailReport {
1268
1276
  kind: "resource_link",
1269
1277
  key: "getsentry/payments#77",
1270
1278
  label: "getsentry/payments#77",
1279
+ objectType: "code_change",
1271
1280
  plugin: "github",
1272
1281
  status: "merged",
1273
1282
  url: "https://github.com/getsentry/payments/pull/77",
@@ -1278,6 +1287,7 @@ function incidentConversation(nowMs: number): ConversationDetailReport {
1278
1287
  kind: "resource_link",
1279
1288
  key: "getsentry/payments#61",
1280
1289
  label: "getsentry/payments#61",
1290
+ objectType: "task",
1281
1291
  plugin: "github",
1282
1292
  status: "closed",
1283
1293
  url: "https://github.com/getsentry/payments/issues/61",
@@ -2281,6 +2291,39 @@ export function readMockConversationEvents(
2281
2291
  };
2282
2292
  }
2283
2293
 
2294
+ /**
2295
+ * Return the page around a linked event or Turn. Mock transcripts fit one
2296
+ * page, so it holds every event of the Conversation.
2297
+ */
2298
+ export function readMockConversationEventsAround(
2299
+ conversationId: string,
2300
+ link: { event: number } | { turn: string },
2301
+ ): ConversationEventPage | "turn-not-found" | undefined {
2302
+ const conversation = mockConversations(NOW_MS).find(
2303
+ (candidate) => candidate.conversationId === conversationId,
2304
+ );
2305
+ if (!conversation) return undefined;
2306
+ if (
2307
+ "turn" in link &&
2308
+ !conversation.events.some(
2309
+ ({ data }) =>
2310
+ data.type === "turn_lifecycle" &&
2311
+ data.state === "started" &&
2312
+ data.turnId === link.turn,
2313
+ )
2314
+ ) {
2315
+ return "turn-not-found";
2316
+ }
2317
+ return {
2318
+ events: conversation.events,
2319
+ eventHistory: conversation.eventHistory,
2320
+ generatedAt: NOW,
2321
+ ...(conversation.previousCursor
2322
+ ? { previousCursor: conversation.previousCursor }
2323
+ : undefined),
2324
+ };
2325
+ }
2326
+
2284
2327
  function mockBeforeCursor(conversationId: string, seq: number): string {
2285
2328
  return `mock:before:${encodeURIComponent(conversationId)}:${seq}`;
2286
2329
  }
@@ -42,6 +42,8 @@ import {
42
42
  automationParamsSchema,
43
43
  automationSummarySchema,
44
44
  automationRunListSchema,
45
+ conversationCatchUpBodySchema,
46
+ conversationCatchUpSchema,
45
47
  } from "@sentry/junior/api/schema";
46
48
  import { mockChartPng } from "./chart-png";
47
49
  import {
@@ -56,6 +58,7 @@ import {
56
58
  readMockConversationChildAgents,
57
59
  readMockConversationDetail,
58
60
  readMockConversationEvents,
61
+ readMockConversationEventsAround,
59
62
  readMockConversationFeed,
60
63
  readMockConversationPendingMessages,
61
64
  readMockConversationStats,
@@ -198,14 +201,30 @@ export function createMockReportingApi(): Hono<{
198
201
  if (!params.success) {
199
202
  return errorResponse("Invalid route parameters.", 400);
200
203
  }
201
- // Mock details fit one page and have no unread cursor, so mock history
202
- // only pages backward.
203
- if (!query.success || !("before" in query.data)) {
204
+ if (!query.success) {
204
205
  return errorResponse("Invalid query parameters.", 400);
205
206
  }
206
207
  if (!readMockConversationDetail(params.data.conversationId)) {
207
208
  return errorResponse("Conversation not found.", 404);
208
209
  }
210
+ if ("event" in query.data || "turn" in query.data) {
211
+ const around = readMockConversationEventsAround(
212
+ params.data.conversationId,
213
+ "event" in query.data
214
+ ? { event: query.data.event }
215
+ : { turn: query.data.turn },
216
+ );
217
+ return around === "turn-not-found"
218
+ ? errorResponse("Turn not found.", 404)
219
+ : around
220
+ ? jsonResponse(conversationEventPageSchema, around)
221
+ : errorResponse("Conversation not found.", 404);
222
+ }
223
+ // Mock details fit one page and have no unread cursor, so mock history
224
+ // pages only backward from cursors.
225
+ if (!("before" in query.data)) {
226
+ return errorResponse("Invalid query parameters.", 400);
227
+ }
209
228
  const report = readMockConversationEvents(
210
229
  params.data.conversationId,
211
230
  query.data.before,
@@ -287,6 +306,24 @@ export function createMockReportingApi(): Hono<{
287
306
  ? jsonResponse(conversationReadPositionSchema, result)
288
307
  : errorResponse("Conversation not found.", 404);
289
308
  });
309
+ app.post("/conversations/:conversationId/catch-up", async (c) => {
310
+ const params = conversationParamsSchema.safeParse(c.req.param());
311
+ if (!params.success) {
312
+ return errorResponse("Invalid route parameters.", 400);
313
+ }
314
+ const body = conversationCatchUpBodySchema.safeParse(await c.req.json());
315
+ if (!body.success) {
316
+ return errorResponse("Invalid request body.", 400);
317
+ }
318
+ if (!readMockConversationDetail(params.data.conversationId)) {
319
+ return errorResponse("Conversation not found.", 404);
320
+ }
321
+ return jsonResponse(conversationCatchUpSchema, {
322
+ needsYou: "Review the Safari fix on PR #12.",
323
+ summary:
324
+ "CI failed on Safari after you left. Junior pushed a fix to PR #12, and the checks pass now.",
325
+ });
326
+ });
290
327
  app.patch("/conversations/:conversationId/archive", async (c) => {
291
328
  const params = conversationParamsSchema.safeParse(c.req.param());
292
329
  if (!params.success) {
@@ -1,7 +1,8 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { dashboardClientFile, dashboardPublicAssets } from "./assets";
4
+ import * as z from "zod";
5
+ import { dashboardClientManifest, dashboardPublicAssets } from "./assets";
5
6
 
6
7
  /**
7
8
  * URL prefix for the content-hashed dashboard browser files. `juniorNitro()`
@@ -33,28 +34,35 @@ function publicDir(): string | undefined {
33
34
  ].find((dir) => existsSync(dir));
34
35
  }
35
36
 
37
+ /** File under `dist` where the client build names its entry and imports. */
38
+ export const DASHBOARD_CLIENT_MANIFEST_FILE = "client-manifest.json";
39
+
40
+ /** The client entry and every file it imports statically. */
41
+ export const dashboardClientManifestSchema = z.object({
42
+ entry: z.string().regex(DASHBOARD_CLIENT_ENTRY_NAME),
43
+ imports: z.array(z.string().regex(DASHBOARD_PUBLIC_ASSET_NAME)),
44
+ });
45
+
46
+ /** The dashboard client entry and every file it imports statically. */
47
+ export type DashboardClientManifest = z.infer<
48
+ typeof dashboardClientManifestSchema
49
+ >;
50
+
36
51
  /**
37
- * Name the dashboard client entry. Without embedded assets, read the build
38
- * output on each call, so a client rebuild never leaves the page on a deleted
39
- * file.
52
+ * Read the client manifest. Without embedded assets, read the build output on
53
+ * each call, so a client rebuild never leaves the page on a deleted file.
40
54
  */
41
- export function dashboardClientFileName(): string {
42
- if (dashboardClientFile) return dashboardClientFile;
55
+ export function readDashboardClientManifest(): DashboardClientManifest {
56
+ if (dashboardClientManifest) return dashboardClientManifest;
43
57
  const dir = publicDir();
44
- // A watch rebuild can leave older entries next to the new one.
45
- const newest = dir
46
- ? readdirSync(dir)
47
- .filter((file) => DASHBOARD_CLIENT_ENTRY_NAME.test(file))
48
- .map((file) => ({
49
- file,
50
- mtimeMs: statSync(path.join(dir, file)).mtimeMs,
51
- }))
52
- .sort((a, b) => b.mtimeMs - a.mtimeMs)[0]
53
- : undefined;
54
- if (!newest) {
58
+ const manifestPath =
59
+ dir && path.join(dir, "..", DASHBOARD_CLIENT_MANIFEST_FILE);
60
+ if (!manifestPath || !existsSync(manifestPath)) {
55
61
  throw new Error("Junior dashboard client bundle was not found");
56
62
  }
57
- return newest.file;
63
+ return dashboardClientManifestSchema.parse(
64
+ JSON.parse(readFileSync(manifestPath, "utf8")),
65
+ );
58
66
  }
59
67
 
60
68
  function readPublicAsset(file: string): string | undefined {
@@ -63,7 +71,7 @@ function readPublicAsset(file: string): string | undefined {
63
71
  return dashboardPublicAssets[file];
64
72
  }
65
73
  // An embedded build has every file it can serve.
66
- if (dashboardClientFile) return undefined;
74
+ if (dashboardClientManifest) return undefined;
67
75
  const dir = publicDir();
68
76
  const assetPath = dir ? path.join(dir, file) : undefined;
69
77
  return assetPath && existsSync(assetPath)
package/src/shell.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  import { dashboardRainbowProgressClass } from "./dashboardLoader";
12
12
  import {
13
13
  DASHBOARD_ASSETS_PATH,
14
- dashboardClientFileName,
14
+ readDashboardClientManifest,
15
15
  } from "./public-assets";
16
16
  import { DASHBOARD_THEME_COLORS, DASHBOARD_THEME_SCRIPT } from "./theme";
17
17
 
@@ -217,6 +217,15 @@ export function renderDashboard(
217
217
  const bootstrapScript = bootstrap
218
218
  ? `window.__JUNIOR_DASHBOARD_BOOTSTRAP__ = ${scriptJson(bootstrap)};`
219
219
  : "";
220
+ const client = readDashboardClientManifest();
221
+ // Without these hints, the browser finds the chunks only after the entry
222
+ // has downloaded and parsed, one round trip later.
223
+ const clientPreloads = client.imports
224
+ .map(
225
+ (file) =>
226
+ `<link rel="modulepreload" href="${DASHBOARD_ASSETS_PATH}/${file}" />`,
227
+ )
228
+ .join("\n ");
220
229
  return new Response(
221
230
  `<!DOCTYPE html>
222
231
  <html lang="en">
@@ -231,6 +240,7 @@ export function renderDashboard(
231
240
  <meta name="apple-mobile-web-app-title" content="${escapeHtml(agentName)}" />
232
241
  <link rel="manifest" href="${DASHBOARD_MANIFEST_PATH}" />
233
242
  <link rel="apple-touch-icon" href="${DASHBOARD_INSTALL_ICON_PATH}" />
243
+ ${clientPreloads}
234
244
  <title>${escapeHtml(agentName)}</title>
235
245
  ${renderSentryHead(sentry)}
236
246
  <style>
@@ -297,7 +307,7 @@ export function renderDashboard(
297
307
  });
298
308
  })();
299
309
  </script>
300
- <script type="module" src="${DASHBOARD_ASSETS_PATH}/${encodeURIComponent(dashboardClientFileName())}"></script>
310
+ <script type="module" src="${DASHBOARD_ASSETS_PATH}/${client.entry}"></script>
301
311
  </body>
302
312
  </html>`,
303
313
  {