@sentry/junior-dashboard 0.271.0 → 0.272.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 +67 -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/conversations/ConversationPage.tsx +11 -0
- package/src/client/conversations/ConversationTranscript.tsx +74 -5
- package/src/client/conversations/TranscriptRows.tsx +23 -0
- package/src/client/conversations/TranscriptView.tsx +22 -0
- package/src/client/conversations/queries.ts +30 -5
- 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/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 -44
- 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 +3 -5
- package/src/client/review-desk/reviewItemFormat.ts +3 -5
- 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
|
}
|
|
@@ -78,22 +69,29 @@ export function ReviewDeskPage() {
|
|
|
78
69
|
useDocumentTitle("Inbox");
|
|
79
70
|
const [searchParams] = useSearchParams();
|
|
80
71
|
const [search, setSearch, q] = useDebouncedSearchParam();
|
|
81
|
-
const {
|
|
82
|
-
items: list,
|
|
83
|
-
moreArchived,
|
|
84
|
-
query: items,
|
|
85
|
-
} = useReviewItemsData({ includeArchived: true, q });
|
|
72
|
+
const { items: list, moreArchived, query: items } = useReviewItemsData({ q });
|
|
86
73
|
const showBlocked = !q;
|
|
87
74
|
const blocked = useBlockedAutomationsData(showBlocked);
|
|
88
75
|
const blockedAutomations = showBlocked
|
|
89
76
|
? (blocked.data?.automations ?? [])
|
|
90
77
|
: [];
|
|
91
78
|
const groups = useMemo(() => inboxGroups(list), [list]);
|
|
79
|
+
// A search lists every match in one list. Later pages add open items, so
|
|
80
|
+
// keep open matches before archived ones.
|
|
81
|
+
const results = useMemo(
|
|
82
|
+
() =>
|
|
83
|
+
q
|
|
84
|
+
? [
|
|
85
|
+
...list.filter((item) => item.status === "open"),
|
|
86
|
+
...list.filter((item) => item.status !== "open"),
|
|
87
|
+
]
|
|
88
|
+
: undefined,
|
|
89
|
+
[list, q],
|
|
90
|
+
);
|
|
92
91
|
const open = [...groups.needsYou, ...groups.fyi];
|
|
93
92
|
|
|
94
|
-
// Archived items stay out of the way until a search or the reader asks.
|
|
95
93
|
const [collapsed, setCollapsed] = useState<ReadonlySet<InboxGroupKey>>(
|
|
96
|
-
() => new Set(
|
|
94
|
+
() => new Set(),
|
|
97
95
|
);
|
|
98
96
|
const toggle = (key: InboxGroupKey) =>
|
|
99
97
|
setCollapsed((current) => {
|
|
@@ -102,16 +100,6 @@ export function ReviewDeskPage() {
|
|
|
102
100
|
else next.add(key);
|
|
103
101
|
return next;
|
|
104
102
|
});
|
|
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
103
|
|
|
116
104
|
const itemParam = searchParams.get("item") ?? undefined;
|
|
117
105
|
// Wide screens select the item that waited longest. Small screens show
|
|
@@ -125,12 +113,11 @@ export function ReviewDeskPage() {
|
|
|
125
113
|
|
|
126
114
|
// Read source titles only for the rows on screen and the selected item.
|
|
127
115
|
const sourceIds = useMemo(() => {
|
|
128
|
-
const
|
|
129
|
-
...(
|
|
130
|
-
...(
|
|
131
|
-
...(shownCollapsed.has("archived") ? [] : groups.archived),
|
|
132
|
-
...(selected ? [selected] : []),
|
|
116
|
+
const rows = results ?? [
|
|
117
|
+
...(collapsed.has("needs-you") ? [] : groups.needsYou),
|
|
118
|
+
...(collapsed.has("fyi") ? [] : groups.fyi),
|
|
133
119
|
];
|
|
120
|
+
const shown = selected ? [...rows, selected] : rows;
|
|
134
121
|
return [
|
|
135
122
|
...new Set(
|
|
136
123
|
shown
|
|
@@ -138,7 +125,7 @@ export function ReviewDeskPage() {
|
|
|
138
125
|
.map((item) => item.addedByConversationId),
|
|
139
126
|
),
|
|
140
127
|
];
|
|
141
|
-
}, [groups,
|
|
128
|
+
}, [collapsed, groups, results, selected]);
|
|
142
129
|
const sourceTitles = useLinkedConversationTitles(sourceIds);
|
|
143
130
|
const sourceTitle = (item: ReviewItemPayload) =>
|
|
144
131
|
sourceTitles[sourceIds.indexOf(item.addedByConversationId)];
|
|
@@ -162,9 +149,8 @@ export function ReviewDeskPage() {
|
|
|
162
149
|
)}
|
|
163
150
|
>
|
|
164
151
|
<ReviewDeskSidebar
|
|
165
|
-
archived={groups.archived}
|
|
166
152
|
blocked={blockedAutomations}
|
|
167
|
-
collapsed={
|
|
153
|
+
collapsed={collapsed}
|
|
168
154
|
error={Boolean(items.error && !items.data)}
|
|
169
155
|
footer={
|
|
170
156
|
<>
|
|
@@ -179,9 +165,9 @@ export function ReviewDeskPage() {
|
|
|
179
165
|
Could not load more items. Try again.
|
|
180
166
|
</InlineError>
|
|
181
167
|
) : null}
|
|
182
|
-
{moreArchived
|
|
168
|
+
{moreArchived ? (
|
|
183
169
|
<p className="m-0 px-3 py-2 text-xs text-dashboard-text-muted">
|
|
184
|
-
|
|
170
|
+
Narrow the search to find older archived items.
|
|
185
171
|
</p>
|
|
186
172
|
) : null}
|
|
187
173
|
{loaded && q && list.length === 0 ? (
|
|
@@ -189,12 +175,7 @@ export function ReviewDeskPage() {
|
|
|
189
175
|
No items match "{q}".
|
|
190
176
|
</p>
|
|
191
177
|
) : null}
|
|
192
|
-
{empty ?
|
|
193
|
-
<InboxEmpty
|
|
194
|
-
className="md:hidden"
|
|
195
|
-
onShowArchived={showArchived}
|
|
196
|
-
/>
|
|
197
|
-
) : null}
|
|
178
|
+
{empty ? <InboxEmpty className="md:hidden" /> : null}
|
|
198
179
|
</>
|
|
199
180
|
}
|
|
200
181
|
fyi={groups.fyi}
|
|
@@ -202,6 +183,7 @@ export function ReviewDeskPage() {
|
|
|
202
183
|
needsYou={groups.needsYou}
|
|
203
184
|
onSearchChange={setSearch}
|
|
204
185
|
onToggle={toggle}
|
|
186
|
+
results={results}
|
|
205
187
|
search={search}
|
|
206
188
|
searchParams={listParams}
|
|
207
189
|
selectedId={selected?.id}
|
|
@@ -250,7 +232,7 @@ export function ReviewDeskPage() {
|
|
|
250
232
|
) : direct.error ? (
|
|
251
233
|
<InlineError className="p-7">Review item not found.</InlineError>
|
|
252
234
|
) : empty ? (
|
|
253
|
-
<InboxEmpty
|
|
235
|
+
<InboxEmpty />
|
|
254
236
|
) : null}
|
|
255
237
|
</section>
|
|
256
238
|
</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,17 @@ 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.
|
|
30
30
|
*/
|
|
31
31
|
export function useReviewItemsData(filters: {
|
|
32
32
|
conversationId?: string;
|
|
33
|
-
includeArchived?: boolean;
|
|
34
33
|
q?: string;
|
|
35
34
|
}) {
|
|
36
35
|
const params = new URLSearchParams();
|
|
37
36
|
if (filters.conversationId) {
|
|
38
37
|
params.set("conversationId", filters.conversationId);
|
|
39
38
|
}
|
|
40
|
-
if (filters.includeArchived) params.set("include", "archived");
|
|
41
39
|
if (filters.q) params.set("q", filters.q);
|
|
42
40
|
const search = params.toString();
|
|
43
41
|
const query = useInfiniteQuery({
|
|
@@ -137,7 +135,7 @@ export function useBlockedAutomationsData(enabled: boolean) {
|
|
|
137
135
|
});
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
/** Take, submit,
|
|
138
|
+
/** Take, submit, or dismiss one Review item. */
|
|
141
139
|
export function useReviewItemAction() {
|
|
142
140
|
const queryClient = useQueryClient();
|
|
143
141
|
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
|
};
|
|
@@ -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.
|