@sentry/junior-dashboard 0.271.0 → 0.273.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +44 -3
- package/dist/client.js +69 -67
- package/dist/index.js +40 -3
- package/dist/tailwind.css +1 -1
- package/package.json +4 -4
- package/src/app.ts +2 -0
- package/src/client/App.tsx +440 -430
- package/src/client/components/JuniorLogo.tsx +1 -1
- package/src/client/components/ProfileMenu.tsx +4 -41
- package/src/client/components/layout/DashboardChrome.tsx +9 -4
- package/src/client/components/layout/DashboardHeader.tsx +11 -51
- package/src/client/components/layout/NavRail.tsx +156 -0
- package/src/client/components/ui/sidebar.tsx +336 -0
- package/src/client/conversations/ConversationPage.tsx +14 -0
- package/src/client/conversations/ConversationTranscript.tsx +74 -5
- package/src/client/conversations/PendingMailboxStack.tsx +22 -9
- package/src/client/conversations/TranscriptMessageShell.tsx +19 -8
- package/src/client/conversations/TranscriptMessageView.tsx +8 -1
- package/src/client/conversations/TranscriptRows.tsx +23 -0
- package/src/client/conversations/TranscriptTurnContextView.tsx +1 -1
- package/src/client/conversations/TranscriptView.tsx +22 -0
- package/src/client/conversations/conversationOutbox.ts +19 -0
- package/src/client/conversations/eventTranscript.ts +6 -0
- package/src/client/conversations/queries.ts +29 -20
- package/src/client/conversations/transcript.ts +12 -2
- package/src/client/conversations/transcriptBottomPinning.ts +84 -13
- package/src/client/conversations/transcriptReadPosition.ts +267 -0
- package/src/client/conversations/transcriptRenderModel.ts +16 -2
- package/src/client/format.ts +8 -1
- package/src/client/pages/automations/AutomationActions.tsx +2 -3
- package/src/client/pages/automations/AutomationEditor.tsx +18 -30
- package/src/client/pages/automations/AutomationOverview.tsx +2 -2
- package/src/client/pages/automations/AutomationVersionsTab.tsx +1 -1
- package/src/client/pages/settings/SettingsPage.tsx +62 -0
- package/src/client/review-desk/ReviewDeskPage.tsx +26 -40
- package/src/client/review-desk/ReviewDeskSidebar.tsx +29 -23
- package/src/client/review-desk/ReviewItemActions.tsx +1 -10
- package/src/client/review-desk/queries.ts +6 -5
- package/src/client/review-desk/reviewItemFormat.ts +3 -5
- package/src/client/types.ts +2 -0
- package/src/mock-reporting/read-positions.ts +39 -0
- package/src/mock-reporting/routes.ts +26 -1
- package/src/tailwind.css +1 -1
- package/src/client/components/AGENTS.md +0 -36
- package/src/client/conversations/AGENTS.md +0 -17
- package/src/client/pages/automations/AGENTS.md +0 -15
|
@@ -5,7 +5,6 @@ import type { ReviewItemPayload } from "@sentry/junior/api/schema";
|
|
|
5
5
|
import { InlineError } from "../components/InlineError";
|
|
6
6
|
import { LoadMorePagination } from "../components/Pagination";
|
|
7
7
|
import { useDetailsPanel } from "../components/DetailsPanel";
|
|
8
|
-
import { Button } from "../components/ui/button";
|
|
9
8
|
import { Skeleton } from "../components/ui/skeleton";
|
|
10
9
|
import { useDocumentTitle } from "../documentTitle";
|
|
11
10
|
import { pathWithSearch, useDebouncedSearchParam } from "../searchParams";
|
|
@@ -24,10 +23,7 @@ import { inboxGroups, nextInboxItem, reviewItemPath } from "./reviewItemFormat";
|
|
|
24
23
|
const shellClass =
|
|
25
24
|
"grid h-full min-h-0 w-full overflow-hidden bg-dashboard-bg md:grid-cols-[20rem_minmax(0,1fr)]";
|
|
26
25
|
|
|
27
|
-
function InboxEmpty(props: {
|
|
28
|
-
className?: string;
|
|
29
|
-
onShowArchived?: () => void;
|
|
30
|
-
}) {
|
|
26
|
+
function InboxEmpty(props: { className?: string }) {
|
|
31
27
|
return (
|
|
32
28
|
<div
|
|
33
29
|
className={cn(
|
|
@@ -38,11 +34,6 @@ function InboxEmpty(props: {
|
|
|
38
34
|
<p className="m-0 font-display text-lg font-medium text-dashboard-text">
|
|
39
35
|
Nothing needs you
|
|
40
36
|
</p>
|
|
41
|
-
{props.onShowArchived ? (
|
|
42
|
-
<Button onClick={props.onShowArchived} variant="quiet">
|
|
43
|
-
See archived items
|
|
44
|
-
</Button>
|
|
45
|
-
) : null}
|
|
46
37
|
</div>
|
|
47
38
|
);
|
|
48
39
|
}
|
|
@@ -82,18 +73,29 @@ export function ReviewDeskPage() {
|
|
|
82
73
|
items: list,
|
|
83
74
|
moreArchived,
|
|
84
75
|
query: items,
|
|
85
|
-
} = useReviewItemsData({
|
|
76
|
+
} = useReviewItemsData({ forViewer: true, q });
|
|
86
77
|
const showBlocked = !q;
|
|
87
78
|
const blocked = useBlockedAutomationsData(showBlocked);
|
|
88
79
|
const blockedAutomations = showBlocked
|
|
89
80
|
? (blocked.data?.automations ?? [])
|
|
90
81
|
: [];
|
|
91
82
|
const groups = useMemo(() => inboxGroups(list), [list]);
|
|
83
|
+
// A search lists every match in one list. Later pages add open items, so
|
|
84
|
+
// keep open matches before archived ones.
|
|
85
|
+
const results = useMemo(
|
|
86
|
+
() =>
|
|
87
|
+
q
|
|
88
|
+
? [
|
|
89
|
+
...list.filter((item) => item.status === "open"),
|
|
90
|
+
...list.filter((item) => item.status !== "open"),
|
|
91
|
+
]
|
|
92
|
+
: undefined,
|
|
93
|
+
[list, q],
|
|
94
|
+
);
|
|
92
95
|
const open = [...groups.needsYou, ...groups.fyi];
|
|
93
96
|
|
|
94
|
-
// Archived items stay out of the way until a search or the reader asks.
|
|
95
97
|
const [collapsed, setCollapsed] = useState<ReadonlySet<InboxGroupKey>>(
|
|
96
|
-
() => new Set(
|
|
98
|
+
() => new Set(),
|
|
97
99
|
);
|
|
98
100
|
const toggle = (key: InboxGroupKey) =>
|
|
99
101
|
setCollapsed((current) => {
|
|
@@ -102,16 +104,6 @@ export function ReviewDeskPage() {
|
|
|
102
104
|
else next.add(key);
|
|
103
105
|
return next;
|
|
104
106
|
});
|
|
105
|
-
const shownCollapsed = useMemo(() => {
|
|
106
|
-
if (!q || !collapsed.has("archived")) return collapsed;
|
|
107
|
-
const next = new Set(collapsed);
|
|
108
|
-
next.delete("archived");
|
|
109
|
-
return next;
|
|
110
|
-
}, [collapsed, q]);
|
|
111
|
-
const showArchived =
|
|
112
|
-
groups.archived.length > 0 && shownCollapsed.has("archived")
|
|
113
|
-
? () => toggle("archived")
|
|
114
|
-
: undefined;
|
|
115
107
|
|
|
116
108
|
const itemParam = searchParams.get("item") ?? undefined;
|
|
117
109
|
// Wide screens select the item that waited longest. Small screens show
|
|
@@ -125,12 +117,11 @@ export function ReviewDeskPage() {
|
|
|
125
117
|
|
|
126
118
|
// Read source titles only for the rows on screen and the selected item.
|
|
127
119
|
const sourceIds = useMemo(() => {
|
|
128
|
-
const
|
|
129
|
-
...(
|
|
130
|
-
...(
|
|
131
|
-
...(shownCollapsed.has("archived") ? [] : groups.archived),
|
|
132
|
-
...(selected ? [selected] : []),
|
|
120
|
+
const rows = results ?? [
|
|
121
|
+
...(collapsed.has("needs-you") ? [] : groups.needsYou),
|
|
122
|
+
...(collapsed.has("fyi") ? [] : groups.fyi),
|
|
133
123
|
];
|
|
124
|
+
const shown = selected ? [...rows, selected] : rows;
|
|
134
125
|
return [
|
|
135
126
|
...new Set(
|
|
136
127
|
shown
|
|
@@ -138,7 +129,7 @@ export function ReviewDeskPage() {
|
|
|
138
129
|
.map((item) => item.addedByConversationId),
|
|
139
130
|
),
|
|
140
131
|
];
|
|
141
|
-
}, [groups,
|
|
132
|
+
}, [collapsed, groups, results, selected]);
|
|
142
133
|
const sourceTitles = useLinkedConversationTitles(sourceIds);
|
|
143
134
|
const sourceTitle = (item: ReviewItemPayload) =>
|
|
144
135
|
sourceTitles[sourceIds.indexOf(item.addedByConversationId)];
|
|
@@ -162,9 +153,8 @@ export function ReviewDeskPage() {
|
|
|
162
153
|
)}
|
|
163
154
|
>
|
|
164
155
|
<ReviewDeskSidebar
|
|
165
|
-
archived={groups.archived}
|
|
166
156
|
blocked={blockedAutomations}
|
|
167
|
-
collapsed={
|
|
157
|
+
collapsed={collapsed}
|
|
168
158
|
error={Boolean(items.error && !items.data)}
|
|
169
159
|
footer={
|
|
170
160
|
<>
|
|
@@ -179,9 +169,9 @@ export function ReviewDeskPage() {
|
|
|
179
169
|
Could not load more items. Try again.
|
|
180
170
|
</InlineError>
|
|
181
171
|
) : null}
|
|
182
|
-
{moreArchived
|
|
172
|
+
{moreArchived ? (
|
|
183
173
|
<p className="m-0 px-3 py-2 text-xs text-dashboard-text-muted">
|
|
184
|
-
|
|
174
|
+
Narrow the search to find older archived items.
|
|
185
175
|
</p>
|
|
186
176
|
) : null}
|
|
187
177
|
{loaded && q && list.length === 0 ? (
|
|
@@ -189,12 +179,7 @@ export function ReviewDeskPage() {
|
|
|
189
179
|
No items match "{q}".
|
|
190
180
|
</p>
|
|
191
181
|
) : null}
|
|
192
|
-
{empty ?
|
|
193
|
-
<InboxEmpty
|
|
194
|
-
className="md:hidden"
|
|
195
|
-
onShowArchived={showArchived}
|
|
196
|
-
/>
|
|
197
|
-
) : null}
|
|
182
|
+
{empty ? <InboxEmpty className="md:hidden" /> : null}
|
|
198
183
|
</>
|
|
199
184
|
}
|
|
200
185
|
fyi={groups.fyi}
|
|
@@ -202,6 +187,7 @@ export function ReviewDeskPage() {
|
|
|
202
187
|
needsYou={groups.needsYou}
|
|
203
188
|
onSearchChange={setSearch}
|
|
204
189
|
onToggle={toggle}
|
|
190
|
+
results={results}
|
|
205
191
|
search={search}
|
|
206
192
|
searchParams={listParams}
|
|
207
193
|
selectedId={selected?.id}
|
|
@@ -250,7 +236,7 @@ export function ReviewDeskPage() {
|
|
|
250
236
|
) : direct.error ? (
|
|
251
237
|
<InlineError className="p-7">Review item not found.</InlineError>
|
|
252
238
|
) : empty ? (
|
|
253
|
-
<InboxEmpty
|
|
239
|
+
<InboxEmpty />
|
|
254
240
|
) : null}
|
|
255
241
|
</section>
|
|
256
242
|
</div>
|
|
@@ -12,10 +12,10 @@ import { ConversationSectionHeading } from "../conversations/ConversationSection
|
|
|
12
12
|
import { automationPath, formatRelativeTime } from "../format";
|
|
13
13
|
import { pathWithSearch } from "../searchParams";
|
|
14
14
|
import { cn } from "../styles";
|
|
15
|
-
import { reviewItemPath } from "./reviewItemFormat";
|
|
15
|
+
import { reviewItemPath, reviewItemStatusLabel } from "./reviewItemFormat";
|
|
16
16
|
|
|
17
17
|
/** A sidebar group key. The page owns which groups are collapsed. */
|
|
18
|
-
export type InboxGroupKey = "
|
|
18
|
+
export type InboxGroupKey = "blocked" | "fyi" | "needs-you";
|
|
19
19
|
|
|
20
20
|
type Dot = "accent" | "muted" | "warning" | "closed";
|
|
21
21
|
|
|
@@ -120,11 +120,11 @@ function InboxRowsLoading() {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* List the inbox: open items that need the viewer, notices, blocked
|
|
124
|
-
* Automations,
|
|
123
|
+
* List the inbox: open items that need the viewer, notices, and blocked
|
|
124
|
+
* Automations, each in a group that collapses. A search shows one list of
|
|
125
|
+
* open and archived matches instead.
|
|
125
126
|
*/
|
|
126
127
|
export function ReviewDeskSidebar(props: {
|
|
127
|
-
archived: ReviewItemPayload[];
|
|
128
128
|
blocked: AutomationSummary[];
|
|
129
129
|
collapsed: ReadonlySet<InboxGroupKey>;
|
|
130
130
|
/** Shown under the groups, such as an empty state or load more. */
|
|
@@ -135,6 +135,8 @@ export function ReviewDeskSidebar(props: {
|
|
|
135
135
|
needsYou: ReviewItemPayload[];
|
|
136
136
|
onSearchChange(value: string): void;
|
|
137
137
|
onToggle(key: InboxGroupKey): void;
|
|
138
|
+
/** Search matches in API order. Set while a search is active. */
|
|
139
|
+
results?: ReviewItemPayload[];
|
|
138
140
|
search: string;
|
|
139
141
|
searchParams: URLSearchParams;
|
|
140
142
|
selectedId?: string;
|
|
@@ -153,16 +155,19 @@ export function ReviewDeskSidebar(props: {
|
|
|
153
155
|
];
|
|
154
156
|
const firstKey = (index: number) =>
|
|
155
157
|
shown.slice(0, index).every((count) => count === 0);
|
|
156
|
-
const itemRow = (item: ReviewItemPayload
|
|
158
|
+
const itemRow = (item: ReviewItemPayload) => {
|
|
159
|
+
const open = item.status === "open";
|
|
157
160
|
const source = item.automationId ? "Automation" : props.sourceTitle(item);
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
const meta = [
|
|
162
|
+
open ? undefined : reviewItemStatusLabel(item),
|
|
163
|
+
source,
|
|
164
|
+
formatRelativeTime(open ? item.createdAt : item.closedAt),
|
|
165
|
+
];
|
|
161
166
|
return (
|
|
162
167
|
<InboxRow
|
|
163
|
-
dot={
|
|
168
|
+
dot={open ? (item.kind === "action" ? "accent" : "muted") : "closed"}
|
|
164
169
|
key={item.id}
|
|
165
|
-
meta={
|
|
170
|
+
meta={meta.filter(Boolean).join(" · ")}
|
|
166
171
|
selected={
|
|
167
172
|
item.id !== props.selectedId
|
|
168
173
|
? undefined
|
|
@@ -202,6 +207,18 @@ export function ReviewDeskSidebar(props: {
|
|
|
202
207
|
</InlineError>
|
|
203
208
|
) : props.loading ? (
|
|
204
209
|
<InboxRowsLoading />
|
|
210
|
+
) : props.results ? (
|
|
211
|
+
<>
|
|
212
|
+
{props.results.length > 0 ? (
|
|
213
|
+
<ul
|
|
214
|
+
aria-label="Search results"
|
|
215
|
+
className="m-0 grid list-none gap-0.5 p-0 pt-1"
|
|
216
|
+
>
|
|
217
|
+
{props.results.map(itemRow)}
|
|
218
|
+
</ul>
|
|
219
|
+
) : null}
|
|
220
|
+
{props.footer}
|
|
221
|
+
</>
|
|
205
222
|
) : (
|
|
206
223
|
<>
|
|
207
224
|
{groups.map((group, index) => (
|
|
@@ -213,9 +230,7 @@ export function ReviewDeskSidebar(props: {
|
|
|
213
230
|
label={group.label}
|
|
214
231
|
onToggle={() => props.onToggle(group.key)}
|
|
215
232
|
>
|
|
216
|
-
{group.items.map(
|
|
217
|
-
itemRow(item, item.kind === "action" ? "accent" : "muted"),
|
|
218
|
-
)}
|
|
233
|
+
{group.items.map(itemRow)}
|
|
219
234
|
</InboxGroup>
|
|
220
235
|
))}
|
|
221
236
|
<InboxGroup
|
|
@@ -235,15 +250,6 @@ export function ReviewDeskSidebar(props: {
|
|
|
235
250
|
/>
|
|
236
251
|
))}
|
|
237
252
|
</InboxGroup>
|
|
238
|
-
<InboxGroup
|
|
239
|
-
collapsed={props.collapsed.has("archived")}
|
|
240
|
-
count={props.archived.length}
|
|
241
|
-
first={firstKey(3)}
|
|
242
|
-
label="Archived"
|
|
243
|
-
onToggle={() => props.onToggle("archived")}
|
|
244
|
-
>
|
|
245
|
-
{props.archived.map((item) => itemRow(item, "closed"))}
|
|
246
|
-
</InboxGroup>
|
|
247
253
|
{props.footer}
|
|
248
254
|
</>
|
|
249
255
|
)}
|
|
@@ -102,7 +102,7 @@ export function ReviewItemActions(props: { item: ReviewItemPayload }) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
// Take and Dismiss are quiet, so the answer
|
|
105
|
+
// Take and Dismiss are quiet, so the answer stays the main action.
|
|
106
106
|
const secondaryActions = (
|
|
107
107
|
<>
|
|
108
108
|
{item.canTake ? (
|
|
@@ -149,15 +149,6 @@ export function ReviewItemActions(props: { item: ReviewItemPayload }) {
|
|
|
149
149
|
</div>
|
|
150
150
|
) : (
|
|
151
151
|
<div className="flex flex-wrap items-center gap-2">
|
|
152
|
-
{item.kind === "notice" ? (
|
|
153
|
-
<Button
|
|
154
|
-
disabled={action.isPending}
|
|
155
|
-
onClick={() => void run({ action: "complete" }).catch(() => {})}
|
|
156
|
-
variant="primary"
|
|
157
|
-
>
|
|
158
|
-
Done
|
|
159
|
-
</Button>
|
|
160
|
-
) : null}
|
|
161
152
|
<div className="flex flex-wrap gap-1 sm:ml-auto">
|
|
162
153
|
{secondaryActions}
|
|
163
154
|
</div>
|
|
@@ -25,19 +25,20 @@ const COUNT_REFRESH_MS = 60_000;
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* List the open Review items that the viewer can see, one page at a time.
|
|
28
|
-
* A search
|
|
29
|
-
*
|
|
28
|
+
* A search also finds archived items, with the first page. `items` holds the
|
|
29
|
+
* items of every loaded page, and `query` loads more. `forViewer` drops the
|
|
30
|
+
* items that are assigned only to other people.
|
|
30
31
|
*/
|
|
31
32
|
export function useReviewItemsData(filters: {
|
|
32
33
|
conversationId?: string;
|
|
33
|
-
|
|
34
|
+
forViewer?: boolean;
|
|
34
35
|
q?: string;
|
|
35
36
|
}) {
|
|
36
37
|
const params = new URLSearchParams();
|
|
37
38
|
if (filters.conversationId) {
|
|
38
39
|
params.set("conversationId", filters.conversationId);
|
|
39
40
|
}
|
|
40
|
-
if (filters.
|
|
41
|
+
if (filters.forViewer) params.set("for", "viewer");
|
|
41
42
|
if (filters.q) params.set("q", filters.q);
|
|
42
43
|
const search = params.toString();
|
|
43
44
|
const query = useInfiniteQuery({
|
|
@@ -137,7 +138,7 @@ export function useBlockedAutomationsData(enabled: boolean) {
|
|
|
137
138
|
});
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
/** Take, submit,
|
|
141
|
+
/** Take, submit, or dismiss one Review item. */
|
|
141
142
|
export function useReviewItemAction() {
|
|
142
143
|
const queryClient = useQueryClient();
|
|
143
144
|
return useMutation({
|
|
@@ -101,9 +101,8 @@ export function reviewItemWaitsForAnswer(item: ReviewItemPayload): boolean {
|
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/** The Review items of the inbox, in the groups and order
|
|
104
|
+
/** The open Review items of the inbox, in the groups and order it shows. */
|
|
105
105
|
export type InboxGroups = {
|
|
106
|
-
archived: ReviewItemPayload[];
|
|
107
106
|
fyi: ReviewItemPayload[];
|
|
108
107
|
needsYou: ReviewItemPayload[];
|
|
109
108
|
};
|
|
@@ -117,13 +116,12 @@ function inboxOrder(a: ReviewItemPayload, b: ReviewItemPayload): number {
|
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
/**
|
|
120
|
-
* Split items into inbox groups
|
|
121
|
-
*
|
|
119
|
+
* Split open items into inbox groups, oldest first, so the item that waited
|
|
120
|
+
* longest is at the top. Archived items are in no group.
|
|
122
121
|
*/
|
|
123
122
|
export function inboxGroups(items: readonly ReviewItemPayload[]): InboxGroups {
|
|
124
123
|
const open = items.filter((item) => item.status === "open");
|
|
125
124
|
return {
|
|
126
|
-
archived: items.filter((item) => item.status !== "open"),
|
|
127
125
|
fyi: open.filter((item) => item.kind === "notice").sort(inboxOrder),
|
|
128
126
|
needsYou: open.filter((item) => item.kind === "action").sort(inboxOrder),
|
|
129
127
|
};
|
package/src/client/types.ts
CHANGED
|
@@ -121,6 +121,8 @@ export type TranscriptViewMessage = {
|
|
|
121
121
|
actorIdentity?: ActorIdentity;
|
|
122
122
|
/** Agent invocation whose input this is. An agent wrote it, not a person. */
|
|
123
123
|
agentInvocationId?: string;
|
|
124
|
+
/** Identity id of the author. Conversation `authors` maps it to the person. */
|
|
125
|
+
actorIdentityId?: string;
|
|
124
126
|
contexts?: TranscriptViewTurnContext[];
|
|
125
127
|
/** Mailbox delivery mode while the message is still pending history commit. */
|
|
126
128
|
delivery?: "defer" | "interrupt";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConversationDetailReport,
|
|
3
|
+
ConversationReadPosition,
|
|
4
|
+
} from "@sentry/junior/api/schema";
|
|
5
|
+
import { readMockConversationDetail } from "./fixtures";
|
|
6
|
+
|
|
7
|
+
/** Process-local read positions so mock QA can reopen at unread rows. */
|
|
8
|
+
const mockReadPositions = new Map<string, number>();
|
|
9
|
+
|
|
10
|
+
/** Clear mock read positions so tests sharing a worker do not leak state. */
|
|
11
|
+
export function resetMockReadPositions(): void {
|
|
12
|
+
mockReadPositions.clear();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Move a mock read position forward, like the API. */
|
|
16
|
+
export function markMockConversationRead(
|
|
17
|
+
conversationId: string,
|
|
18
|
+
lastReadSeq: number,
|
|
19
|
+
): ConversationReadPosition | undefined {
|
|
20
|
+
const detail = readMockConversationDetail(conversationId);
|
|
21
|
+
if (!detail) return undefined;
|
|
22
|
+
const lastSeq = detail.events.at(-1)?.seq ?? 0;
|
|
23
|
+
const stored = Math.max(
|
|
24
|
+
mockReadPositions.get(conversationId) ?? -1,
|
|
25
|
+
Math.min(lastReadSeq, lastSeq),
|
|
26
|
+
);
|
|
27
|
+
mockReadPositions.set(conversationId, stored);
|
|
28
|
+
return { lastReadSeq: stored };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Add the mock read position to a detail report. */
|
|
32
|
+
export function withMockReadPosition(
|
|
33
|
+
report: ConversationDetailReport,
|
|
34
|
+
): ConversationDetailReport {
|
|
35
|
+
const lastReadSeq = mockReadPositions.get(report.conversationId);
|
|
36
|
+
return lastReadSeq === undefined
|
|
37
|
+
? report
|
|
38
|
+
: { ...report, readPosition: { lastReadSeq } };
|
|
39
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
conversationFeedQuerySchema,
|
|
16
16
|
conversationFeedSchema,
|
|
17
17
|
conversationParamsSchema,
|
|
18
|
+
conversationReadPositionSchema,
|
|
18
19
|
conversationPendingMessagesReportSchema,
|
|
19
20
|
conversationStatsReportSchema,
|
|
20
21
|
conversationWatchesReportSchema,
|
|
@@ -38,6 +39,10 @@ import {
|
|
|
38
39
|
automationRunListSchema,
|
|
39
40
|
} from "@sentry/junior/api/schema";
|
|
40
41
|
import { mockChartPng } from "./chart-png";
|
|
42
|
+
import {
|
|
43
|
+
markMockConversationRead,
|
|
44
|
+
withMockReadPosition,
|
|
45
|
+
} from "./read-positions";
|
|
41
46
|
import {
|
|
42
47
|
filterMockConversationsByRepository,
|
|
43
48
|
readMockCodeRepository,
|
|
@@ -244,7 +249,27 @@ export function createMockReportingApi(): Hono<{
|
|
|
244
249
|
const { conversationId } = params.data;
|
|
245
250
|
const report = readMockConversationDetail(conversationId, query.data.limit);
|
|
246
251
|
return report
|
|
247
|
-
? jsonResponse(
|
|
252
|
+
? jsonResponse(
|
|
253
|
+
conversationDetailReportSchema,
|
|
254
|
+
withMockReadPosition(report),
|
|
255
|
+
)
|
|
256
|
+
: errorResponse("Conversation not found.", 404);
|
|
257
|
+
});
|
|
258
|
+
app.put("/conversations/:conversationId/read-position", async (c) => {
|
|
259
|
+
const params = conversationParamsSchema.safeParse(c.req.param());
|
|
260
|
+
if (!params.success) {
|
|
261
|
+
return errorResponse("Invalid route parameters.", 400);
|
|
262
|
+
}
|
|
263
|
+
const body = conversationReadPositionSchema.safeParse(await c.req.json());
|
|
264
|
+
if (!body.success) {
|
|
265
|
+
return errorResponse("Invalid request body.", 400);
|
|
266
|
+
}
|
|
267
|
+
const result = markMockConversationRead(
|
|
268
|
+
params.data.conversationId,
|
|
269
|
+
body.data.lastReadSeq,
|
|
270
|
+
);
|
|
271
|
+
return result
|
|
272
|
+
? jsonResponse(conversationReadPositionSchema, result)
|
|
248
273
|
: errorResponse("Conversation not found.", 404);
|
|
249
274
|
});
|
|
250
275
|
app.patch("/conversations/:conversationId/archive", async (c) => {
|
package/src/tailwind.css
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
@import "shadcn/tailwind.css";
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
* Dashboard design tokens.
|
|
7
|
+
* Dashboard design tokens. `pnpm dashboard-style:check` enforces their use.
|
|
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,36 +0,0 @@
|
|
|
1
|
-
# Dashboard UI rules
|
|
2
|
-
|
|
3
|
-
These apply to all dashboard UI. `pnpm dashboard-style:check` enforces the
|
|
4
|
-
token rules.
|
|
5
|
-
|
|
6
|
-
- Add shadcn components with
|
|
7
|
-
`pnpm --filter @sentry/junior-dashboard exec shadcn add <component>`.
|
|
8
|
-
- Change a `ui/` file only when every caller needs the same style. Do not
|
|
9
|
-
wrap a `ui/` component just to rename it or its props.
|
|
10
|
-
- Do not apply the shadcn CLI's global border or body resets. Keep the
|
|
11
|
-
Tailwind radius scale.
|
|
12
|
-
- Confirmations use `ui/alert-dialog`, not `window.confirm`.
|
|
13
|
-
- Short task forms use `ui/dialog`, never a `Drawer` or `DetailsPanel`. Mount
|
|
14
|
-
each dialog through `useSheet`, only while open.
|
|
15
|
-
- Put only inline elements in a `ui/trace` row button.
|
|
16
|
-
- Label icon-only controls with `TooltipContent size="compact"`, not the
|
|
17
|
-
native `title` attribute.
|
|
18
|
-
- Set `focusableWhenDisabled` on a tooltip trigger `ui/button` that can be
|
|
19
|
-
disabled.
|
|
20
|
-
- Field ids are `<control id>-error` and `<control id>-help`.
|
|
21
|
-
- Put a checkbox or radio inside its `<label>`. Give each `RadioGroup` an
|
|
22
|
-
`aria-label`.
|
|
23
|
-
- A form page has one `Button variant="primary"`.
|
|
24
|
-
- Do not replace `ObjectIcon` with a provider logo.
|
|
25
|
-
- Use `light:` only for values that no token covers. Chart series use
|
|
26
|
-
`var(--color-*)`, not hex.
|
|
27
|
-
- Use `outline-dashboard-focus` for keyboard focus only.
|
|
28
|
-
- Labels use sentence case, with no uppercase or letter spacing.
|
|
29
|
-
- App navigation uses pills, page links use `SegmentedNav`, and panel
|
|
30
|
-
switches use `ui/tabs`.
|
|
31
|
-
- Do not add viewport, offset, or safe-area math in pages.
|
|
32
|
-
`layout/VisualViewportShell`, `conversations/ChatLayout`, and
|
|
33
|
-
`conversations/ComposerDock` own it. Home and create do not use
|
|
34
|
-
`ComposerDock`.
|
|
35
|
-
- Keep a feature component under `conversations/` or `pages/` until a second
|
|
36
|
-
real caller needs it.
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# Transcript components
|
|
2
|
-
|
|
3
|
-
- Keep horizontal padding inside each transcript row, so focus outlines are
|
|
4
|
-
not clipped. Do not add negative margins at `TranscriptSummaryTrigger` call
|
|
5
|
-
sites.
|
|
6
|
-
- The Automation trigger card shows the credentials from the run's dispatch
|
|
7
|
-
record, not the current setting. Keep the run input collapsed, because it
|
|
8
|
-
is not from a person.
|
|
9
|
-
- A nested `Transcript` never uses the panel of the page behind it.
|
|
10
|
-
- The page shows one right panel at a time. Closing a panel returns focus to
|
|
11
|
-
the control that opened it.
|
|
12
|
-
- The URL owns the selected event in the main Conversation. Standalone
|
|
13
|
-
transcripts keep it in `ConversationEventLog`.
|
|
14
|
-
- A pending exit must not close a new panel or another Conversation. A
|
|
15
|
-
pending fork blocks dismissal of its dialog.
|
|
16
|
-
- Raw JSON keeps the original ISO values. Persist the UTC/local mode, not the
|
|
17
|
-
timezone.
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Automation pages
|
|
2
|
-
|
|
3
|
-
- Core owns validation, edit access, creator-only rules, revision checks,
|
|
4
|
-
Schedule previews, and the Event catalog. Show core errors. Do not repeat
|
|
5
|
-
these rules in the page.
|
|
6
|
-
- Preview requests never save or run work.
|
|
7
|
-
- Send only changed fields. Keep unsupported triggers and stored Destinations
|
|
8
|
-
unless the user replaces them. Keep outcome order.
|
|
9
|
-
- Warn that an instruction or event selector change switches to system
|
|
10
|
-
credentials.
|
|
11
|
-
- Query refreshes must not replace an unsaved draft. After a stale save, let
|
|
12
|
-
the user keep their changes or load the latest version.
|
|
13
|
-
- Lifecycle actions and "Make active" read the latest revision first.
|
|
14
|
-
- `AutomationRecentRuns` shows terminal runs only. Do not infer queued or
|
|
15
|
-
running work from terminal records.
|