@sentry/junior-dashboard 0.268.0 → 0.270.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 (81) hide show
  1. package/README.md +7 -105
  2. package/dist/app.js +12 -9
  3. package/dist/client.js +81 -81
  4. package/dist/index.js +12 -9
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +5 -4
  7. package/src/api/schema.ts +2 -1
  8. package/src/app.ts +11 -7
  9. package/src/client/App.tsx +15 -9
  10. package/src/client/api.ts +4 -19
  11. package/src/client/components/AGENTS.md +65 -0
  12. package/src/client/components/ActorAvatar.tsx +1 -1
  13. package/src/client/components/AutomationCard.tsx +2 -1
  14. package/src/client/components/ParticipantAvatarStack.tsx +1 -0
  15. package/src/client/components/ProfileMenu.tsx +88 -29
  16. package/src/client/components/UserInputForm.tsx +110 -37
  17. package/src/client/components/charts/AGENTS.md +15 -0
  18. package/src/client/conversations/AGENTS.md +38 -0
  19. package/src/client/conversations/ConversationBrief.tsx +55 -36
  20. package/src/client/conversations/ConversationComposer.tsx +27 -8
  21. package/src/client/conversations/ConversationEventLog.tsx +1 -0
  22. package/src/client/conversations/ConversationHomeList.tsx +43 -26
  23. package/src/client/conversations/ConversationPage.tsx +90 -56
  24. package/src/client/conversations/ConversationSectionHeading.tsx +44 -0
  25. package/src/client/conversations/ConversationSidebar.tsx +55 -25
  26. package/src/client/conversations/ConversationTranscript.tsx +3 -2
  27. package/src/client/conversations/ConversationWatches.tsx +4 -1
  28. package/src/client/conversations/ConversationWorkspace.tsx +43 -25
  29. package/src/client/conversations/EventDetails.tsx +6 -9
  30. package/src/client/conversations/PendingMailboxStack.tsx +5 -2
  31. package/src/client/conversations/TranscriptContextEventView.tsx +3 -4
  32. package/src/client/conversations/TranscriptEventView.tsx +9 -3
  33. package/src/client/conversations/TranscriptMessageView.tsx +6 -5
  34. package/src/client/conversations/TranscriptReasoningView.tsx +4 -5
  35. package/src/client/conversations/TranscriptRows.tsx +6 -1
  36. package/src/client/conversations/TranscriptStructuredEventView.tsx +5 -4
  37. package/src/client/conversations/TranscriptToolView.tsx +2 -1
  38. package/src/client/conversations/TranscriptView.tsx +6 -6
  39. package/src/client/conversations/conversationOutbox.ts +13 -3
  40. package/src/client/conversations/conversationSections.ts +36 -8
  41. package/src/client/conversations/eventTranscript.ts +1 -0
  42. package/src/client/conversations/transcriptBottomPinning.ts +171 -81
  43. package/src/client/conversations/transcriptScrollAnchor.ts +101 -0
  44. package/src/client/conversations/useCollapsedConversationSections.ts +48 -0
  45. package/src/client/display-text/DisplayText.tsx +68 -0
  46. package/src/client/display-text/emoji.ts +54 -0
  47. package/src/client/{conversations/transcriptMarkdownLinks.ts → display-text/links.ts} +16 -3
  48. package/src/client/{conversations/TranscriptMarkdown.tsx → display-text/markdown.tsx} +83 -19
  49. package/src/client/display-text/mentions.tsx +148 -0
  50. package/src/client/display-text/plainText.ts +29 -0
  51. package/src/client/format.ts +7 -23
  52. package/src/client/pages/automations/AGENTS.md +34 -0
  53. package/src/client/pages/automations/AutomationActions.tsx +18 -20
  54. package/src/client/pages/automations/AutomationEditor.tsx +3 -4
  55. package/src/client/pages/automations/AutomationFilters.tsx +6 -6
  56. package/src/client/pages/automations/AutomationOverview.tsx +5 -5
  57. package/src/client/pages/automations/AutomationStatus.tsx +2 -2
  58. package/src/client/pages/automations/AutomationVersionsTab.tsx +12 -8
  59. package/src/client/pages/code/RepositoryTabs.tsx +6 -1
  60. package/src/client/pages/memory/MemoryDetails.tsx +5 -13
  61. package/src/client/pages/system/AGENTS.md +12 -0
  62. package/src/client/review-desk/ConversationReviewBar.tsx +14 -9
  63. package/src/client/review-desk/ReviewDeskPage.tsx +99 -79
  64. package/src/client/review-desk/ReviewItemActions.tsx +6 -2
  65. package/src/client/review-desk/ReviewItemCard.tsx +58 -17
  66. package/src/client/review-desk/ReviewItemDetail.tsx +17 -9
  67. package/src/client/review-desk/UnsentAnswers.tsx +148 -0
  68. package/src/client/review-desk/queries.ts +25 -16
  69. package/src/client/review-desk/reviewItemFormat.ts +29 -1
  70. package/src/mock-reporting/routes.ts +1 -1
  71. package/src/tailwind.css +1 -1
  72. package/src/client/components/README.md +0 -219
  73. package/src/client/components/charts/README.md +0 -38
  74. package/src/client/conversations/README.md +0 -159
  75. package/src/client/conversations/TranscriptText.tsx +0 -47
  76. package/src/client/conversations/UserInputRequestCard.tsx +0 -39
  77. package/src/client/conversations/conversationActivityPreview.ts +0 -16
  78. package/src/client/conversations/transcriptMentions.tsx +0 -134
  79. package/src/client/conversations/userInputRequest.ts +0 -56
  80. package/src/client/pages/automations/README.md +0 -97
  81. package/src/client/pages/system/README.md +0 -23
@@ -0,0 +1,148 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ReactNode,
11
+ } from "react";
12
+ import type { ReviewItemPayload } from "@sentry/junior/api/schema";
13
+
14
+ import {
15
+ AlertDialog,
16
+ AlertDialogAction,
17
+ AlertDialogCancel,
18
+ AlertDialogContent,
19
+ AlertDialogDescription,
20
+ AlertDialogFooter,
21
+ AlertDialogTitle,
22
+ } from "../components/ui/alert-dialog";
23
+
24
+ type UnsentAnswers = {
25
+ conversationId: string;
26
+ /** Item title for each mounted answer form that has unsent answers. */
27
+ titles: Map<string, string>;
28
+ };
29
+
30
+ const UnsentAnswersContext = createContext<UnsentAnswers | undefined>(
31
+ undefined,
32
+ );
33
+
34
+ /**
35
+ * Track the answer forms in one Conversation that have unsent answers. A
36
+ * message in the Conversation closes some open action items, and the
37
+ * answers on those forms are then lost. The reply footer reads this to ask
38
+ * first.
39
+ */
40
+ export function UnsentAnswersProvider(props: {
41
+ children: ReactNode;
42
+ conversationId: string;
43
+ }) {
44
+ const value = useMemo(
45
+ () => ({ conversationId: props.conversationId, titles: new Map() }),
46
+ [props.conversationId],
47
+ );
48
+ return (
49
+ <UnsentAnswersContext.Provider value={value}>
50
+ {props.children}
51
+ </UnsentAnswersContext.Provider>
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Report that the answer form of an item has unsent answers. It counts only
57
+ * when a message from the viewer closes the item. This follows
58
+ * `closeReviewItemsAnsweredByReply`: an open action item that the agent
59
+ * added in this Conversation, not assigned to Everyone, that the viewer can
60
+ * act on. Outside a Conversation it does nothing.
61
+ */
62
+ export function useReportUnsentAnswer(
63
+ item: ReviewItemPayload,
64
+ unsent: boolean,
65
+ ) {
66
+ const context = useContext(UnsentAnswersContext);
67
+ const key = useId();
68
+ const closesOnReply =
69
+ item.kind === "action" &&
70
+ item.status === "open" &&
71
+ item.canAct &&
72
+ !item.assignedToEveryone &&
73
+ item.addedByConversationId === context?.conversationId;
74
+ const title = item.title;
75
+ useEffect(() => {
76
+ if (!context || !closesOnReply || !unsent) return;
77
+ context.titles.set(key, title);
78
+ return () => {
79
+ context.titles.delete(key);
80
+ };
81
+ }, [closesOnReply, context, key, title, unsent]);
82
+ }
83
+
84
+ /**
85
+ * Ask the reader before a message drops unsent answers. `confirmSubmit`
86
+ * returns undefined when nothing would be lost, so the send goes at once.
87
+ * Otherwise it opens `dialog` and resolves true when the reader sends anyway.
88
+ */
89
+ export function useConfirmUnsentAnswers() {
90
+ const context = useContext(UnsentAnswersContext);
91
+ const [open, setOpen] = useState(false);
92
+ // Keep the titles through the close animation.
93
+ const [titles, setTitles] = useState<string[]>([]);
94
+ const resolveRef = useRef<(send: boolean) => void>(undefined);
95
+
96
+ const confirmSubmit = useCallback(() => {
97
+ // The transcript card and the side panel can show the same item.
98
+ const unsent = [...new Set(context?.titles.values())];
99
+ if (unsent.length === 0) return undefined;
100
+ setTitles(unsent);
101
+ setOpen(true);
102
+ return new Promise<boolean>((resolve) => {
103
+ resolveRef.current = resolve;
104
+ });
105
+ }, [context]);
106
+
107
+ // Resolve on close, so a dismissal by Escape also keeps the draft.
108
+ const close = (send: boolean) => {
109
+ resolveRef.current?.(send);
110
+ resolveRef.current = undefined;
111
+ setOpen(false);
112
+ };
113
+
114
+ const dialog = (
115
+ <AlertDialog
116
+ onOpenChange={(open) => {
117
+ if (!open) close(false);
118
+ }}
119
+ open={open}
120
+ >
121
+ <AlertDialogContent>
122
+ <AlertDialogTitle>Send without your answers?</AlertDialogTitle>
123
+ <AlertDialogDescription>
124
+ {titles.length === 1 ? (
125
+ <>
126
+ Your answers to{" "}
127
+ <strong className="font-medium text-dashboard-text">
128
+ {titles[0]}
129
+ </strong>{" "}
130
+ are not sent. This message closes that review item, and the
131
+ answers are lost.
132
+ </>
133
+ ) : (
134
+ `Your answers to ${titles.length} review items are not sent. This message closes those items, and the answers are lost.`
135
+ )}
136
+ </AlertDialogDescription>
137
+ <AlertDialogFooter>
138
+ <AlertDialogCancel>Go back</AlertDialogCancel>
139
+ <AlertDialogAction onClick={() => close(true)} variant="danger">
140
+ Send message
141
+ </AlertDialogAction>
142
+ </AlertDialogFooter>
143
+ </AlertDialogContent>
144
+ </AlertDialog>
145
+ );
146
+
147
+ return { confirmSubmit, dialog };
148
+ }
@@ -1,6 +1,7 @@
1
1
  import { useMemo } from "react";
2
2
  import {
3
3
  keepPreviousData,
4
+ useInfiniteQuery,
4
5
  useMutation,
5
6
  useQueries,
6
7
  useQuery,
@@ -19,38 +20,46 @@ import {
19
20
  import { conversationDetailTitle } from "../format";
20
21
  import { fetchDashboardJson, post } from "../http";
21
22
 
22
- /** Review desk views. Each shows open items, or archived ones. */
23
- export const REVIEW_DESK_VIEWS = ["mine", "everyone", "all"] as const;
24
- export type ReviewDeskView = (typeof REVIEW_DESK_VIEWS)[number];
25
-
26
23
  const REVIEW_ITEMS_QUERY_KEY = ["dashboard", "review-items"] as const;
27
24
  const COUNT_REFRESH_MS = 60_000;
28
25
 
29
- /** List the Review items that the viewer can see in one view. */
26
+ /**
27
+ * List the open Review items that the viewer can see, one page at a time.
28
+ * A search also finds archived items, with the first page. `items` holds
29
+ * the items of every loaded page, and `query` loads more.
30
+ */
30
31
  export function useReviewItemsData(filters: {
31
- archived: boolean;
32
32
  conversationId?: string;
33
33
  q?: string;
34
- view: ReviewDeskView;
35
34
  }) {
36
- const params = new URLSearchParams({ view: filters.view });
37
- if (filters.archived) params.set("archived", "true");
35
+ const params = new URLSearchParams();
38
36
  if (filters.conversationId) {
39
37
  params.set("conversationId", filters.conversationId);
40
38
  }
41
39
  if (filters.q) params.set("q", filters.q);
42
40
  const search = params.toString();
43
- return useQuery({
41
+ const query = useInfiniteQuery({
42
+ initialPageParam: undefined as string | undefined,
44
43
  queryKey: [...REVIEW_ITEMS_QUERY_KEY, "list", search],
45
- queryFn: ({ signal }) =>
46
- fetchDashboardJson(
44
+ queryFn: ({ pageParam, signal }) => {
45
+ const pageParams = new URLSearchParams(search);
46
+ if (pageParam) pageParams.set("cursor", pageParam);
47
+ return fetchDashboardJson(
47
48
  reviewItemListSchema,
48
- `/api/review-items?${search}`,
49
+ `/api/review-items?${pageParams.toString()}`,
49
50
  signal,
50
- ),
51
+ );
52
+ },
53
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
51
54
  placeholderData: keepPreviousData,
52
55
  retry: false,
53
56
  });
57
+ const pages = query.data?.pages;
58
+ const items = useMemo(
59
+ () => (pages ?? []).flatMap((page) => page.items),
60
+ [pages],
61
+ );
62
+ return { items, moreArchived: Boolean(pages?.[0]?.moreArchived), query };
54
63
  }
55
64
 
56
65
  /** Read one Review item, for a link to an item outside the current list. */
@@ -92,7 +101,7 @@ export function useLinkedConversationTitles(
92
101
  }).map((query) => query.data ?? undefined);
93
102
  }
94
103
 
95
- /** Count the open Review items assigned to the viewer, for the nav. */
104
+ /** Count the open action items assigned to the viewer, for the nav. */
96
105
  export function useReviewItemCountData(enabled: boolean) {
97
106
  return useQuery({
98
107
  enabled,
@@ -145,7 +154,7 @@ const NO_CONVERSATIONS: ReadonlySet<string> = new Set();
145
154
 
146
155
  /**
147
156
  * Return the Conversations that wait on the viewer. Today only the review
148
- * desk sets this: an open linked item assigned to the viewer or Everyone.
157
+ * desk sets this: a linked open action item assigned to the viewer.
149
158
  */
150
159
  export function useWaitingConversationIds(): ReadonlySet<string> {
151
160
  const query = useQuery({
@@ -1,5 +1,8 @@
1
1
  import { z } from "zod";
2
- import type { ReviewItemPayload } from "@sentry/junior/api/schema";
2
+ import type {
3
+ ConversationReportEvent,
4
+ ReviewItemPayload,
5
+ } from "@sentry/junior/api/schema";
3
6
 
4
7
  /** Return the review desk route that selects one item. */
5
8
  export function reviewItemPath(id: string): string {
@@ -59,3 +62,28 @@ export function reviewItemIdFromToolCall(call: {
59
62
  if (toolName !== ADD_REVIEW_ITEM_TOOL_NAME) return undefined;
60
63
  return addedItemId(call.output);
61
64
  }
65
+
66
+ /** Return the id of the last Review item that a tool call in the events added. */
67
+ export function latestReviewItemId(
68
+ events: readonly ConversationReportEvent[] | undefined,
69
+ ): string | undefined {
70
+ for (let index = (events?.length ?? 0) - 1; index >= 0; index -= 1) {
71
+ const data = events![index]!.data;
72
+ if (data.type !== "tool_calls") continue;
73
+ for (let call = data.calls.length - 1; call >= 0; call -= 1) {
74
+ const id = reviewItemIdFromToolCall(data.calls[call]!);
75
+ if (id) return id;
76
+ }
77
+ }
78
+ return undefined;
79
+ }
80
+
81
+ /** True when an open item asks a question that the viewer can answer. */
82
+ export function reviewItemWaitsForAnswer(item: ReviewItemPayload): boolean {
83
+ return (
84
+ item.status === "open" &&
85
+ item.kind === "action" &&
86
+ Boolean(item.request) &&
87
+ item.canAct
88
+ );
89
+ }
@@ -303,7 +303,7 @@ export function createMockReportingApi(): Hono<{
303
303
  jsonResponse(reviewItemListSchema, { items: [] }),
304
304
  );
305
305
  app.get("/review-items/count", () =>
306
- jsonResponse(reviewItemCountSchema, { mine: 0 }),
306
+ jsonResponse(reviewItemCountSchema, { needsViewer: 0 }),
307
307
  );
308
308
  app.get("/review-items/waiting-conversations", () =>
309
309
  jsonResponse(reviewItemWaitingConversationsSchema, { conversationIds: [] }),
package/src/tailwind.css CHANGED
@@ -4,7 +4,7 @@
4
4
  @import "shadcn/tailwind.css";
5
5
 
6
6
  /*
7
- * Dashboard design tokens. components/README.md explains each role.
7
+ * Dashboard design tokens. components/AGENTS.md has the token rules.
8
8
  * Keep structural UI colors here so a second theme can redefine them.
9
9
  * Prefer roles over opacity ladders. Status accents stay as Tailwind palette utilities.
10
10
  */
@@ -1,219 +0,0 @@
1
- # Dashboard UI kit
2
-
3
- Shared dashboard components live here. Feature pages compose these surfaces
4
- instead of copying Tailwind blocks. See `policies/frontend-components.md` for
5
- the policy. `pnpm dashboard-style:check` enforces the token rules below.
6
-
7
- ## shadcn/ui
8
-
9
- `packages/junior-dashboard/components.json` configures shadcn/ui for this package.
10
- It uses Base UI (`base-nova`), Tailwind 4, and Lucide. Add components from the
11
- repository root:
12
-
13
- ```sh
14
- pnpm --filter @sentry/junior-dashboard exec shadcn add <component>
15
- ```
16
-
17
- The CLI writes components to `src/client/components/ui/`. Its `@dashboard/*`
18
- alias points to `src/client/*`. The existing `@/*` alias still points to core.
19
- The `ui/` files join classes with the merging `cn` from
20
- `src/client/lib/utils.ts`, so a caller's `className` wins over a base class.
21
-
22
- The shadcn color tokens in `src/tailwind.css` use the dashboard tokens in both
23
- themes. The `dark:` variant follows `data-theme`. Keep the existing Tailwind
24
- radius scale. Do not apply the CLI's global border or body resets. Check each
25
- added component against the rules below before use.
26
-
27
- Each `ui/` file is shadcn source that uses dashboard tokens. Remove exports
28
- that no app code uses. Use `ui/` components directly. Do not add an app
29
- component that only renames a `ui/` component or its props. Change the `ui/`
30
- file when every caller needs the same style or variant. App components compose
31
- `ui/` source only when they add structure or behavior:
32
-
33
- | App component | shadcn source |
34
- | ------------------------------ | ----------------- |
35
- | `Drawer` | `ui/drawer` |
36
- | `BottomSheet` | `ui/sheet` |
37
- | `layout/DashboardHeader` (nav) | `ui/sheet` |
38
- | `ProfileMenu` (desktop) | `ui/popover` |
39
- | `ActorAvatar` | `ui/avatar` |
40
- | `ParticipantAvatarStack` | `ui/avatar` |
41
- | `SearchInput` | `ui/input-group` |
42
- | `PagePagination` | `ui/pagination` |
43
- | `EmptyTelemetry` | `ui/empty` |
44
- | `TimeRangeSelector` | `ui/toggle-group` |
45
- | `ThemeSwitcher` | `ui/toggle-group` |
46
- | `TranscriptViewToggle` | `ui/toggle-group` |
47
- | `FilterTabList` | `ui/tabs` |
48
- | `charts/ActivityChartTooltip` | `ui/tooltip` |
49
- | `conversations/ToolFrame` | `ui/trace` |
50
- | `TranscriptReasoningView` | `ui/trace` |
51
- | `ImageAttachment` (preview) | `ui/dialog` |
52
- | `Notice`, `InlineError` | `ui/alert` |
53
- | App banners | `ui/alert` |
54
- | Recharts activity charts | `ui/chart` |
55
-
56
- Rules for adopted components:
57
-
58
- - Use `ui/trace` for transcript activity rows. It composes `ui/collapsible`.
59
- A closed tool row unmounts its panel, so it does not run Shiki. Reasoning
60
- sets `hiddenUntilFound`, so browser find reaches closed text. Transcript
61
- search holds rows open and turns off the row button. Put only inline
62
- elements, such as `span`, inside the row button.
63
- - Use `ui/alert-dialog` for every confirmation. Do not use `window.confirm`.
64
- Keep the confirmed item in state after close, so the closing dialog keeps its
65
- text.
66
- - Use `ui/dialog` for a short task form, such as the fork message. Do not put
67
- a form in a `Drawer` or a `DetailsPanel`. On small screens the dialog sits
68
- near the top, so the on-screen keyboard does not cover its fields.
69
- - Use `useSheet` for each app `ui/dialog`, as for sheets. It shares the
70
- document scroll lock. Only the front modal closes on Escape or a backdrop
71
- tap. Mount the dialog only while it is open.
72
- - `ui/alert` sets `role="alert"`. Pass `role="status"` for a message that is
73
- not an error.
74
- - `ui/toast` has no global notification manager. Features call
75
- `useToastManager` inside the mounted `Toaster` provider.
76
- - Put a checkbox or radio inside its `<label>`. Give each `RadioGroup` an
77
- `aria-label`.
78
- - Give an icon-only control its label with `TooltipContent size="compact"`.
79
- Do not use the native `title` attribute. Tooltips open on mouse hover and
80
- keyboard focus only, so a tap still runs the control.
81
- - When a `ui/button` tooltip trigger can be disabled, set
82
- `focusableWhenDisabled`. A native disabled button can drop hover and focus,
83
- so Firefox closes the tooltip and hides pending or result labels.
84
- `ui/button` styles `data-disabled`, so both disabled modes look the same.
85
- - Build a form field from `ui/field`. Give `FieldError` the id
86
- `<control id>-error` and `FieldDescription` the id `<control id>-help`. The
87
- control sets `aria-invalid` and `aria-describedby`.
88
- - Use `Button size="menu"` for a full-width icon and label row in a mobile
89
- overflow menu. A pressed row adds `bg-dashboard-fill-strong`.
90
- - For a link that looks like a button, put `buttonVariants()` on the router
91
- `Link`. A form page has one `Button variant="primary"` for its main save
92
- action.
93
- - Use `Button variant="quiet"` for a secondary action beside the primary one,
94
- such as Dismiss. It has no border, so the primary action stays dominant.
95
- - In a form, show each choice as a whole bordered row around `RadioGroupItem`
96
- or `Checkbox`. The row shows hover and selected state. See `UserInputForm`.
97
-
98
- ## Foundations
99
-
100
- Tokens live in the `@theme` block of `src/tailwind.css`. Keep every color in a
101
- token so each theme can redefine it.
102
-
103
- - **Themes.** Dark is the default. The light theme redefines every token under
104
- `:root[data-theme="light"]`. It also mirrors the light status and chart
105
- shades (50-400) to their dark pair, so `text-rose-300` stays readable on the
106
- light canvas. The head script in `src/theme.ts` sets `data-theme` before first
107
- paint from the saved choice or the system color scheme. `ThemeSwitcher` saves
108
- the choice for this browser. Use the `light:` variant only for one-off values
109
- that no token covers, such as provider brand colors.
110
- - **Charts.** Use `var(--color-*)` for series colors, not hex values, so they
111
- follow the theme. Name series colors in the `ChartContainer` config and fill
112
- each series with `var(--color-<key>)`.
113
- - **Surfaces.** Use the surface ladder, darkest to lightest: `bg`,
114
- `bg-elevated` (drawers), `surface-panel` (sidebar, cards, bubbles),
115
- `surface-raised` (buttons, tooltips), `control`, `surface-hover`,
116
- `surface-active` (selected rows). Use `fill-*` for translucent fills and
117
- `overlay-*` for scrims. Do not use `white/*` or `black/*` fills, borders, or
118
- rings, or hex surface colors. Shadows may use black.
119
- - **Borders.** Use `border-subtle` for hairlines inside a surface,
120
- `border` for card edges, `border-strong` for shell edges, and
121
- `border-emphasis` or `border-interactive` for controls and hover.
122
- - **Text.** Brightest to dimmest: `text-solid`, `text`, `text-subtle`,
123
- `text-muted`, `text-faint`.
124
- - **Accent.** Lavender is the one brand accent. Use `accent` for icons,
125
- active markers, and tinted fills, such as `bg-dashboard-accent/10`. Use
126
- `accent-text` for links and accent text. Keep Tailwind palette colors for
127
- status (info, success, warning, error) and for categorical chart, event, and
128
- subagent colors.
129
- - **Focus.** Use `outline-dashboard-focus` for keyboard focus. Do not use other
130
- colors for focus rings.
131
- - **Type faces.** `font-sans` is the UI face for prose, controls, navigation,
132
- labels, and names. `font-display` is for headings, record titles, and large
133
- metric values. `font-mono` is for machine values: code, ids, paths, file
134
- names, tool names, and raw payloads. Show counts, times, and prices in
135
- `font-sans tabular-nums`.
136
- - **Type scale.** Use the named steps `text-2xs` through `text-4xl`. Keep
137
- `text-2xs` for compact badges and chart annotations.
138
- - **Labels.** Section, card, field, and column labels use
139
- `font-sans text-xs font-medium text-dashboard-text-muted` in sentence case.
140
- Do not use uppercase or letter spacing for labels.
141
- - **Radius.** Controls and chips use `rounded-md`. Cards, list rows, and panels
142
- use `rounded-lg`. The composer, menus, and transcript object cards use
143
- `rounded-xl`. Avatars and count badges use `rounded-full`.
144
-
145
- Shared class strings live in `src/client/styles.ts`.
146
-
147
- ## App components
148
-
149
- - **Status.** Use `StatusDot` for dense table or list markers. Use `StatusChip`
150
- when the label must stay visible. `StatusChip` is not a control.
151
- - **Messages.** Use `Notice` for compact transient messages and `FormNotice`
152
- for wrapping form recovery text.
153
- - **Disclosure.** Use `ui/collapsible` for one show-or-hide section. Put a
154
- `ChevronRight` with `dashboardDisclosureChevronClass` in a `group` trigger.
155
- Set `hiddenUntilFound` on the content when browser find must reach closed
156
- text. Closed content then keeps its box, so put margins and padding on a
157
- child. Use `ui/accordion` for a set of sections with headings.
158
- - **Details.** `DetailsPanel` docks an aside at 1280px and above, and uses a
159
- `BottomSheet` below that width. It owns Escape and the scroll frame for
160
- docked panels. Docked actions sit beside the close control, so their
161
- position does not change with the title width.
162
- - **Drawer.** `Drawer` is a right-side `ui/drawer`. A `Drawer` opened from
163
- inside another `Drawer` stacks in front of it. The parent scales back,
164
- shows a thin edge behind the child, and hides its content. Set `flush` when
165
- the content owns its insets and sticky headers, such as the event log.
166
- - **Identity.** `ObjectIcon` owns the native Ticket, Code change, Automation,
167
- Deployment, and Item icons. Do not replace it with a provider logo or a
168
- status-only icon.
169
- - **Loading.** Use `ui/skeleton` for placeholder leaves and `PageContentSkeleton`
170
- variants for page bodies. Use `PageRouteLoading` when a page keeps its real
171
- header above a body skeleton. Use a feature-owned loading component for
172
- distinct geometry, such as conversations. Use `ui/spinner` for an inline
173
- pending icon.
174
-
175
- ## Navigation
176
-
177
- The dashboard has three navigation levels. Each has its own look, so the
178
- levels stay distinct.
179
-
180
- - **App navigation** in `layout/DashboardHeader` uses pills.
181
- - **Page links** use `SegmentedNav`, through `layout/SecondaryNavigation`. They
182
- are underline tabs that sit on the header border.
183
- - **Panel switches** use `ui/tabs`. Give `TabsList` an `aria-label`. Set
184
- `activateOnFocus` so arrow keys select the tab.
185
-
186
- Keep collection filters in `FilterTabList`. It uses `TabsList variant="line"`
187
- and adds the count badges.
188
-
189
- ## Patterns
190
-
191
- - **Layout.** `layout/` holds the shell chrome, page headers, cards, and
192
- section titles. `DashboardChrome` owns the sticky header block and its one
193
- bottom border.
194
- - **Charts.** Put `role="img"` and the chart `aria-label` on the Recharts
195
- chart, so the tooltip live region stays outside the image. Day and hour
196
- charts compose the `charts/ActivityChart` axis, average line, and tooltip
197
- helpers. They do not format UTC bucket keys inline.
198
- - **Conversation mobile geometry** is a hard ownership edge:
199
- - `layout/VisualViewportShell` owns the fixed shell, visualViewport CSS
200
- variables, and body lock.
201
- - `conversations/ChatLayout` owns the scroll-above-dock frame for reply
202
- threads.
203
- - `conversations/ComposerDock` owns the reply bottom pad and dock chrome.
204
-
205
- Pages pass children only. Home and create are one landing surface, not a
206
- dock, and must not use `ComposerDock`. Do not add viewport height, offset, or
207
- bottom safe-area math in page modules.
208
-
209
- Feature surfaces stay under `conversations/` or `pages/` until two real callers
210
- share them. See [Transcript components](../conversations/README.md) for
211
- transcript ownership.
212
-
213
- ## Rules
214
-
215
- - Prefer a named component on the second real use.
216
- - Keep Tailwind classes on the owning component.
217
- - Check new or changed shared components on a real page with mock data and in
218
- the dashboard E2E screenshots.
219
- - One-off page glue may stay inline when extraction only adds indirection.
@@ -1,38 +0,0 @@
1
- # System cache reporting
2
-
3
- `InputCacheChart` owns the cache trend, summary, and largest writes.
4
- `SystemActivity` selects its time buckets with the page reporting control.
5
- `SystemMetricCharts` keeps spend and runtime beside the cache view.
6
-
7
- ## Input counters
8
-
9
- `ConversationMetricDay` comes from stored Run metrics, not Sentry spans.
10
- Its ordinary input, cached input, and cache-creation counters are disjoint.
11
- Add these three counters once to get total input. Do not include output.
12
- Sentry span input totals use a different rule and must not use this transform.
13
-
14
- `cache-input.ts` owns this calculation. Period shares use summed token counts,
15
- not the mean of bucket shares. A missing counter is not a reported zero.
16
- If an active bucket lacks a counter, omit its share and the whole-period share.
17
- Token mode can still show reported counts. Gray marks identify incomplete
18
- buckets. Empty buckets do not lower shares.
19
-
20
- Field presence is not call-level coverage. SQL sums can combine complete and
21
- incomplete Runs in one bucket. Do not describe these values as a cache hit rate
22
- or a cache failure rate.
23
-
24
- ## Investigation
25
-
26
- `CacheWrites` lists the three largest write counts in the page time window.
27
- The list and chart consume the same selected buckets. There is no local time
28
- selector or comparison window. The list is collapsed until needed.
29
-
30
- The chart fills the available width. It keeps a fixed, compact height rather
31
- than growing taller on wide screens.
32
-
33
- This report has no per-call model, release, environment, trace, or write-cost
34
- fields. Add a reporting contract before adding those filters or drilldowns.
35
- A large write alone cannot identify cache expiry, a cold start, or prefix loss.
36
-
37
- The System browser scenario covers display modes and verifies that the page
38
- time control also filters the list.