@sentry/junior-dashboard 0.267.0 → 0.269.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 (59) hide show
  1. package/README.md +7 -105
  2. package/dist/app.js +47 -8
  3. package/dist/client.js +74 -78
  4. package/dist/index.js +47 -8
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +4 -4
  7. package/src/api/schema.ts +2 -1
  8. package/src/app.ts +11 -7
  9. package/src/client/App.tsx +20 -4
  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/ParticipantAvatarStack.tsx +3 -1
  14. package/src/client/components/ProfileMenu.tsx +88 -29
  15. package/src/client/components/UserInputForm.tsx +283 -0
  16. package/src/client/components/charts/AGENTS.md +15 -0
  17. package/src/client/components/layout/DashboardHeader.tsx +13 -0
  18. package/src/client/components/ui/button.tsx +11 -1
  19. package/src/client/conversations/AGENTS.md +42 -0
  20. package/src/client/conversations/ConversationHomeList.tsx +6 -0
  21. package/src/client/conversations/ConversationListStatusIcon.tsx +15 -1
  22. package/src/client/conversations/ConversationPage.tsx +4 -27
  23. package/src/client/conversations/ConversationSidebar.tsx +6 -0
  24. package/src/client/conversations/ConversationWorkspace.tsx +4 -0
  25. package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
  26. package/src/client/conversations/TranscriptMessageContextView.tsx +12 -1
  27. package/src/client/conversations/TranscriptMessageView.tsx +3 -0
  28. package/src/client/conversations/TranscriptToolView.tsx +13 -1
  29. package/src/client/conversations/TranscriptTurnContextView.tsx +142 -144
  30. package/src/client/conversations/turnContext.ts +15 -0
  31. package/src/client/format.ts +28 -28
  32. package/src/client/pages/SettingsPage.tsx +103 -3
  33. package/src/client/pages/automations/AGENTS.md +34 -0
  34. package/src/client/pages/automations/AutomationActions.tsx +18 -20
  35. package/src/client/pages/automations/AutomationEditor.tsx +3 -4
  36. package/src/client/pages/automations/AutomationOverview.tsx +3 -3
  37. package/src/client/pages/automations/AutomationStatus.tsx +2 -2
  38. package/src/client/pages/code/RepositorySections.tsx +1 -0
  39. package/src/client/pages/code/RepositoryTabs.tsx +3 -0
  40. package/src/client/pages/system/AGENTS.md +12 -0
  41. package/src/client/primaryNav.ts +15 -2
  42. package/src/client/review-desk/ConversationReviewBar.tsx +68 -0
  43. package/src/client/review-desk/ReviewDeskPage.tsx +297 -0
  44. package/src/client/review-desk/ReviewItemActions.tsx +165 -0
  45. package/src/client/review-desk/ReviewItemCard.tsx +47 -0
  46. package/src/client/review-desk/ReviewItemDetail.tsx +123 -0
  47. package/src/client/review-desk/queries.ts +173 -0
  48. package/src/client/review-desk/reviewItemFormat.ts +61 -0
  49. package/src/mock-reporting/fixtures.ts +10 -0
  50. package/src/mock-reporting/routes.ts +20 -0
  51. package/src/shell.ts +3 -0
  52. package/src/tailwind.css +1 -1
  53. package/src/client/components/README.md +0 -215
  54. package/src/client/components/charts/README.md +0 -38
  55. package/src/client/conversations/README.md +0 -159
  56. package/src/client/conversations/UserInputRequestCard.tsx +0 -244
  57. package/src/client/conversations/userInputRequest.ts +0 -56
  58. package/src/client/pages/automations/README.md +0 -97
  59. package/src/client/pages/system/README.md +0 -23
@@ -1,21 +1,28 @@
1
- import { useMutation, useQueryClient } from "@tanstack/react-query";
1
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
2
  import { useEffect, useState, type FormEvent } from "react";
3
3
 
4
+ import {
5
+ USER_CUSTOMIZATION_MAX_CHARS,
6
+ userCustomizationSchema,
7
+ } from "@sentry/junior/api/schema";
4
8
  import {
5
9
  dashboardIdentitySchema,
6
10
  type DashboardIdentity,
7
11
  } from "../../api/schema";
8
12
  import { Button } from "../components/ui/button";
9
13
  import { Input } from "../components/ui/input";
14
+ import { Textarea } from "../components/ui/textarea";
10
15
  import { Field, FieldDescription, FieldLabel } from "../components/ui/field";
11
16
  import { InlineError } from "../components/InlineError";
12
17
  import { Card } from "../components/layout/Card";
13
18
  import { useDocumentTitle } from "../documentTitle";
14
- import { patch } from "../http";
19
+ import { DashboardApiError, fetchDashboardJson, patch, put } from "../http";
15
20
  import { dashboardContainerClass } from "../styles";
16
21
  import type { DashboardCoreData } from "../types";
17
22
 
18
23
  const dashboardCoreQueryKey = ["dashboard", "core"] as const;
24
+ const userCustomizationQueryKey = ["dashboard", "user-customization"] as const;
25
+ const USER_CUSTOMIZATION_PATH = "/api/user-customization";
19
26
 
20
27
  type SettingsPageProps = {
21
28
  identity: DashboardIdentity;
@@ -58,7 +65,7 @@ export function SettingsPage({ identity }: SettingsPageProps) {
58
65
  <section className="mx-auto w-full max-w-3xl">
59
66
  <h1 className="m-0 text-2xl font-bold">Settings</h1>
60
67
  <p className="mt-2 mb-0 max-w-2xl text-sm text-dashboard-text-muted">
61
- Manage how your account appears in the dashboard.
68
+ Manage your profile and preferences.
62
69
  </p>
63
70
 
64
71
  <form className="mt-6" onSubmit={submit}>
@@ -97,7 +104,100 @@ export function SettingsPage({ identity }: SettingsPageProps) {
97
104
  </div>
98
105
  </Card>
99
106
  </form>
107
+ <CustomizationForm />
100
108
  </section>
101
109
  </div>
102
110
  );
103
111
  }
112
+
113
+ /**
114
+ * Let the user save text that changes how the agent replies to them. The
115
+ * server returns 404 when the app does not enable this experiment.
116
+ */
117
+ function CustomizationForm() {
118
+ const queryClient = useQueryClient();
119
+ const [text, setText] = useState("");
120
+ const customizationQuery = useQuery({
121
+ queryKey: userCustomizationQueryKey,
122
+ queryFn: ({ signal }) =>
123
+ fetchDashboardJson(
124
+ userCustomizationSchema,
125
+ USER_CUSTOMIZATION_PATH,
126
+ signal,
127
+ ),
128
+ retry: false,
129
+ });
130
+ const savedText = customizationQuery.data?.customization;
131
+ useEffect(() => {
132
+ if (savedText !== undefined) setText(savedText);
133
+ }, [savedText]);
134
+ const save = useMutation({
135
+ mutationFn: (customization: string) =>
136
+ put(userCustomizationSchema, USER_CUSTOMIZATION_PATH, { customization }),
137
+ onSuccess: (updated) => {
138
+ queryClient.setQueryData(userCustomizationQueryKey, updated);
139
+ },
140
+ });
141
+ if (
142
+ customizationQuery.error instanceof DashboardApiError &&
143
+ customizationQuery.error.status === 404
144
+ ) {
145
+ return null;
146
+ }
147
+ if (savedText === undefined) {
148
+ return customizationQuery.isError ? (
149
+ <InlineError className="mt-6">
150
+ Could not load your customization.
151
+ </InlineError>
152
+ ) : null;
153
+ }
154
+ const canSave =
155
+ text.trim() !== savedText &&
156
+ text.trim().length <= USER_CUSTOMIZATION_MAX_CHARS &&
157
+ !save.isPending;
158
+
159
+ function submit(event: FormEvent<HTMLFormElement>) {
160
+ event.preventDefault();
161
+ if (canSave) save.mutate(text.trim());
162
+ }
163
+
164
+ return (
165
+ <form className="mt-6" onSubmit={submit}>
166
+ <Card className="mb-0" padding="md" variant="raised">
167
+ <h2 className="m-0 text-lg font-bold">Customization</h2>
168
+ <Field className="mt-5">
169
+ <FieldLabel htmlFor="user-customization">
170
+ How should replies to you change?
171
+ </FieldLabel>
172
+ <Textarea
173
+ id="user-customization"
174
+ maxLength={USER_CUSTOMIZATION_MAX_CHARS}
175
+ onChange={(event) => {
176
+ setText(event.target.value);
177
+ if (save.isError || save.isSuccess) save.reset();
178
+ }}
179
+ placeholder="Keep replies short and simple."
180
+ value={text}
181
+ />
182
+ <FieldDescription>
183
+ Added to each message you send. Do not put secrets here. (
184
+ {text.trim().length}/{USER_CUSTOMIZATION_MAX_CHARS})
185
+ </FieldDescription>
186
+ </Field>
187
+ <div className="mt-5 flex items-center gap-3">
188
+ <Button disabled={!canSave} type="submit">
189
+ {save.isPending ? "Saving…" : "Save customization"}
190
+ </Button>
191
+ {save.isSuccess ? (
192
+ <p className="m-0 text-sm text-emerald-300">Changes saved.</p>
193
+ ) : null}
194
+ {save.isError ? (
195
+ <InlineError>
196
+ Could not save your customization. Try again.
197
+ </InlineError>
198
+ ) : null}
199
+ </div>
200
+ </Card>
201
+ </form>
202
+ );
203
+ }
@@ -0,0 +1,34 @@
1
+ # Automation pages
2
+
3
+ ## Boundaries
4
+
5
+ - Core owns validation, edit access, creator-only rules, revision checks,
6
+ Schedule previews, and the Event catalog. The page shows core errors. It
7
+ does not repeat these rules.
8
+ - Preview requests never save or run work.
9
+ - Completed scheduled Automations are read-only.
10
+
11
+ ## Editor rules
12
+
13
+ - Send only changed fields. Keep unsupported triggers and stored
14
+ Destinations unless the user replaces them. Keep outcome order.
15
+ - New messages can target only the current Destination or the creator.
16
+ - A public reader sees outcomes as read-only and cannot turn on creator
17
+ credentials. Warn that an instruction or event selector change switches to
18
+ system credentials.
19
+ - Pause, Resume, and Delete are creator-only. Hide them from other viewers.
20
+ - Query refreshes must not replace an unsaved draft. After a stale save, let
21
+ the user keep their changes or load the latest version.
22
+ - Block navigation with unsaved changes.
23
+ - Lifecycle actions and "Make active" read the latest revision first.
24
+
25
+ ## Page rules
26
+
27
+ - Each Automation has one page at `/automations/:id`. Keep the list query
28
+ string on the way back. Drop tab-only params: `result`, `run`, `version`.
29
+ - `AutomationRecentRuns` shows terminal runs only. Do not infer queued or
30
+ running work from terminal records.
31
+ - Delete keeps errors in the dialog and blocks dismissal while pending.
32
+
33
+ Browser journeys: `e2e/automation-editor.spec.ts` and
34
+ `e2e/automations.spec.ts`.
@@ -43,7 +43,7 @@ export function AutomationActions({
43
43
  const completed = automation.status === "completed";
44
44
  const paused = automation.status === "paused";
45
45
  const blocked = automation.status === "blocked";
46
- const canPause = automation.ownedByViewer && !completed;
46
+ const canPause = !completed;
47
47
  const action = paused || blocked ? "resume" : "pause";
48
48
  const mutation = useMutation({
49
49
  mutationFn: async () => {
@@ -135,25 +135,23 @@ export function AutomationActions({
135
135
  </Popover>
136
136
  </>
137
137
  ) : null}
138
- {automation.ownedByViewer ? (
139
- <Tooltip>
140
- <TooltipTrigger
141
- render={
142
- <Button
143
- focusableWhenDisabled
144
- aria-label="Delete automation"
145
- size="icon"
146
- variant="danger"
147
- onClick={(event) => onDelete(event.currentTarget)}
148
- disabled={mutation.isPending}
149
- >
150
- <Trash2 aria-hidden="true" size={16} />
151
- </Button>
152
- }
153
- />
154
- <TooltipContent size="compact">Delete automation</TooltipContent>
155
- </Tooltip>
156
- ) : null}
138
+ <Tooltip>
139
+ <TooltipTrigger
140
+ render={
141
+ <Button
142
+ focusableWhenDisabled
143
+ aria-label="Delete automation"
144
+ size="icon"
145
+ variant="danger"
146
+ onClick={(event) => onDelete(event.currentTarget)}
147
+ disabled={mutation.isPending}
148
+ >
149
+ <Trash2 aria-hidden="true" size={16} />
150
+ </Button>
151
+ }
152
+ />
153
+ <TooltipContent size="compact">Delete automation</TooltipContent>
154
+ </Tooltip>
157
155
  </div>
158
156
  {mutation.error ? (
159
157
  <div className="max-w-sm">
@@ -135,8 +135,7 @@ export function AutomationEditor(props: {
135
135
  const dropsCreatorCredentials =
136
136
  !owned &&
137
137
  original.credentialMode === "creator" &&
138
- (changes.instruction !== undefined ||
139
- (original.kind === "event" && changes.trigger !== undefined));
138
+ (changes.instruction !== undefined || changes[triggerField] !== undefined);
140
139
  const triggerError = fieldError(triggerField);
141
140
  const inputState = (key: string) => ({
142
141
  "aria-invalid": Boolean(fieldError(key)) || undefined,
@@ -426,8 +425,8 @@ export function AutomationEditor(props: {
426
425
  {dropsCreatorCredentials ? (
427
426
  <FormNotice title="Saving switches this automation to system credentials.">
428
427
  Only {creator} can let a changed instruction
429
- {original.kind === "event" ? " or trigger" : ""} use their
430
- connected accounts.
428
+ {original.kind === "event" ? " or trigger" : " or schedule"} use
429
+ their connected accounts.
431
430
  </FormNotice>
432
431
  ) : null}
433
432
  {fieldError("credentialMode") ? (
@@ -197,9 +197,9 @@ function AutomationAbout(props: { automation: AutomationSummary }) {
197
197
  <p className="mt-3 mb-0 text-xs leading-relaxed text-dashboard-text-muted">
198
198
  {automation.destination.visibility === "private"
199
199
  ? null
200
- : "You can edit its settings because it is in a public channel. "}
201
- Only {automation.createdBy} can pause, resume, or delete it, change
202
- where results go, or turn on their connected accounts.
200
+ : "You can edit, pause, or delete it because it is in a public channel. "}
201
+ Only {automation.createdBy} can change where results go, turn on their
202
+ connected accounts, or resume or run it while it uses them.
203
203
  </p>
204
204
  )}
205
205
  </RailCard>
@@ -140,8 +140,8 @@ export function AutomationAttention({
140
140
  <p className="m-0 break-words">
141
141
  {automation.statusReason ??
142
142
  "Check the last run to see what needs fixing."}{" "}
143
- {!automation.ownedByViewer
144
- ? `Ask ${automation.createdBy} to resume after this is fixed.`
143
+ {!automation.ownedByViewer && automation.credentialMode === "creator"
144
+ ? `It uses accounts connected by ${automation.createdBy}. Ask them to resume it, or switch it to Junior's credentials first.`
145
145
  : null}
146
146
  </p>
147
147
  ) : null}
@@ -69,6 +69,7 @@ export function RecentConversationRow(props: { conversation: Conversation }) {
69
69
  finishedSinceSeen={false}
70
70
  isPrivate={conversation.visibility === "private"}
71
71
  status={visualStatusForConversation(conversation)}
72
+ waitingOnViewer={false}
72
73
  />
73
74
  </span>
74
75
  <div className="min-w-0 truncate font-display text-sm text-dashboard-text">
@@ -18,6 +18,7 @@ import { Card } from "../../components/layout/Card";
18
18
  import { ConversationHomeList } from "../../conversations/ConversationHomeList";
19
19
  import { buildConversations, getDashboardTimeZone } from "../../format";
20
20
  import { useDebouncedValue } from "../../debouncedValue";
21
+ import { useWaitingConversationIds } from "../../review-desk/queries";
21
22
  import { CodeActivityChart } from "./CodeActivityChart";
22
23
  import { CodeChangeRow } from "./CodeChangeRow";
23
24
  import { repositoryActivity } from "./RepositorySections";
@@ -32,6 +33,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
32
33
  const search = useDebouncedValue(query.trim());
33
34
  const feed = useCodeRepositoryConversationsData(props.repositoryId, search);
34
35
  const conversations = buildConversations(feed.data?.conversations ?? []);
36
+ const waitingConversationIds = useWaitingConversationIds();
35
37
  useEffect(() => {
36
38
  setPage(1);
37
39
  }, [search]);
@@ -59,6 +61,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
59
61
  finishedConversationIds={noFinishedConversations}
60
62
  loading={!feed.data}
61
63
  timeZone={getDashboardTimeZone()}
64
+ waitingConversationIds={waitingConversationIds}
62
65
  />
63
66
  )}
64
67
  <PagePagination
@@ -0,0 +1,12 @@
1
+ # System pages
2
+
3
+ - The URL selects a Workspace recipe: `/system/workspaces/:id` opens
4
+ `WorkspaceDetails` beside the list.
5
+ - The list report has no snapshots. The detail read adds the current
6
+ snapshot. Keep snapshot loading and errors inside the Snapshot row.
7
+ - Edit and Delete live in the panel header. List rows have no actions.
8
+ - `WorkspaceDeleteDialog` keeps errors in the dialog and blocks dismissal
9
+ while the request runs.
10
+ - Close and Escape return focus to the selected row.
11
+
12
+ Browser coverage: `e2e/system.spec.ts`.
@@ -9,6 +9,7 @@ const AUTH_PRIMARY_NAV_PREFIXES = [
9
9
  "/memories",
10
10
  "/settings",
11
11
  "/plugins/",
12
+ "/review",
12
13
  ] as const;
13
14
 
14
15
  /** True when the path is only available to a signed-in viewer. */
@@ -24,6 +25,8 @@ export function buildPrimaryNavItems(input: {
24
25
  loggedIn: boolean;
25
26
  pathname: string;
26
27
  primaryUserPages: PluginUserPageLink[];
28
+ /** Open Review items assigned to the viewer. */
29
+ reviewCount?: number;
27
30
  userPagesPending: boolean;
28
31
  }): DashboardHeaderNavItem[] {
29
32
  const memoryNavPage = input.primaryUserPages.find(
@@ -32,8 +35,8 @@ export function buildPrimaryNavItems(input: {
32
35
  const otherPrimaryUserPages = input.primaryUserPages.filter(
33
36
  (page) => !(page.pluginName === "memory" && page.id === "memories"),
34
37
  );
35
- // Auth-only routes already imply the signed-in primary nav. Hold Tasks and
36
- // Memories slots while core/user-pages catch up so the header does not jump.
38
+ // Auth-only routes already imply the signed-in primary nav. Hold Review
39
+ // desk, Tasks, and Memories slots while core/user-pages catch up so the header does not jump.
37
40
  const reserveAuthPrimaryNav =
38
41
  input.loading && isAuthPrimaryNavPath(input.pathname);
39
42
  const showTasksNav = input.loggedIn || reserveAuthPrimaryNav;
@@ -43,6 +46,16 @@ export function buildPrimaryNavItems(input: {
43
46
  (input.loggedIn && input.userPagesPending);
44
47
 
45
48
  return [
49
+ ...(showTasksNav
50
+ ? [
51
+ {
52
+ count: input.reviewCount,
53
+ key: "review",
54
+ label: "Review desk",
55
+ to: "/review",
56
+ },
57
+ ]
58
+ : []),
46
59
  { key: "code", label: "Code", to: "/code" },
47
60
  ...(showTasksNav
48
61
  ? [{ key: "automations", label: "Automations", to: "/automations" }]
@@ -0,0 +1,68 @@
1
+ import { ChevronRight } from "lucide-react";
2
+ import { useState } from "react";
3
+
4
+ import { Drawer } from "../components/Drawer";
5
+ import { LoadMorePagination } from "../components/Pagination";
6
+ import { ReviewItemCard } from "./ReviewItemCard";
7
+ import { useReviewItemsData } from "./queries";
8
+
9
+ /** `more` marks a count of the loaded pages only, such as "50+". */
10
+ function itemCountLabel(count: number, more: boolean): string {
11
+ if (more) return `${count}+ open review items`;
12
+ return count === 1 ? "1 open review item" : `${count} open review items`;
13
+ }
14
+
15
+ /**
16
+ * Show how many open Review items are linked to a Conversation, above its
17
+ * message box. The bar opens a side panel where the viewer can act on each.
18
+ */
19
+ export function ConversationReviewBar(props: { conversationId: string }) {
20
+ const [open, setOpen] = useState(false);
21
+ const { items, query } = useReviewItemsData({
22
+ conversationId: props.conversationId,
23
+ });
24
+ if (items.length === 0) return null;
25
+ const more = Boolean(query.hasNextPage);
26
+ const label = itemCountLabel(items.length, more);
27
+ return (
28
+ <>
29
+ <button
30
+ className="mb-2 flex w-full cursor-pointer items-center justify-between gap-3 rounded-lg border border-violet-300/25 bg-violet-300/[0.06] px-3 py-2 text-left font-sans text-sm text-dashboard-text transition-colors hover:bg-violet-300/[0.1]"
31
+ onClick={() => setOpen(true)}
32
+ type="button"
33
+ >
34
+ <span className="flex items-center gap-2">
35
+ <span
36
+ aria-hidden="true"
37
+ className="size-2 shrink-0 rounded-full bg-violet-300"
38
+ />
39
+ {label}
40
+ </span>
41
+ <ChevronRight aria-hidden="true" size={16} />
42
+ </button>
43
+ {open ? (
44
+ <Drawer
45
+ closeLabel="Close review items"
46
+ header={
47
+ <h2 className="m-0 font-sans text-base font-semibold">{label}</h2>
48
+ }
49
+ label="Review items"
50
+ onClose={() => setOpen(false)}
51
+ openKey={props.conversationId}
52
+ width="narrow"
53
+ >
54
+ <div className="grid gap-3">
55
+ {items.map((item) => (
56
+ <ReviewItemCard item={item} key={item.id} />
57
+ ))}
58
+ <LoadMorePagination
59
+ hasMore={more}
60
+ loading={query.isFetchingNextPage}
61
+ onLoadMore={() => void query.fetchNextPage()}
62
+ />
63
+ </div>
64
+ </Drawer>
65
+ ) : null}
66
+ </>
67
+ );
68
+ }