@sentry/junior-dashboard 0.276.0 → 0.278.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 +196 -72
- package/dist/client.js +94 -89
- package/dist/index.d.ts +1 -1
- package/dist/index.js +191 -70
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +20 -2
- package/src/app.ts +80 -12
- package/src/auth.ts +5 -1
- package/src/client/App.tsx +9 -4
- package/src/client/components/DetailsPanel.tsx +1 -1
- package/src/client/components/FormNotice.tsx +11 -14
- package/src/client/components/ProfileMenu.tsx +5 -6
- package/src/client/components/ui/resizable.tsx +50 -0
- package/src/client/conversations/AutoArchiveToggle.tsx +71 -0
- package/src/client/conversations/ConversationArchiveNotices.tsx +1 -1
- package/src/client/conversations/ConversationChildAgents.tsx +2 -1
- package/src/client/conversations/ConversationDetails.tsx +6 -3
- package/src/client/conversations/ConversationHeader.tsx +15 -2
- package/src/client/conversations/ConversationHeaderActions.tsx +27 -0
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationMemories.tsx +64 -67
- package/src/client/conversations/ConversationMeta.tsx +2 -7
- package/src/client/conversations/ConversationPage.tsx +175 -79
- package/src/client/conversations/ConversationSidebar.tsx +82 -25
- package/src/client/conversations/ConversationTranscript.tsx +11 -8
- package/src/client/conversations/ConversationWatches.tsx +15 -21
- package/src/client/conversations/ConversationWorkspace.tsx +132 -44
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +17 -3
- package/src/client/conversations/DetailsList.tsx +8 -14
- package/src/client/conversations/EventDetails.tsx +2 -62
- package/src/client/conversations/MemoryIcons.tsx +23 -0
- package/src/client/conversations/TranscriptActivityGroup.tsx +36 -14
- package/src/client/conversations/TranscriptContextEventView.tsx +7 -18
- package/src/client/conversations/TranscriptMemoryPopover.tsx +20 -25
- package/src/client/conversations/TranscriptRailEvent.tsx +0 -8
- package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
- package/src/client/conversations/TranscriptView.tsx +89 -20
- package/src/client/conversations/autoArchiveQueries.ts +53 -0
- package/src/client/conversations/conversationArchive.ts +255 -0
- package/src/client/conversations/conversationMemory.ts +2 -1
- package/src/client/conversations/conversationSections.ts +24 -18
- package/src/client/conversations/conversationSidebarLayout.ts +61 -0
- package/src/client/conversations/eventLog.ts +2 -31
- package/src/client/conversations/eventTranscript.ts +9 -42
- package/src/client/conversations/queries.ts +121 -250
- package/src/client/conversations/transcript.ts +46 -16
- package/src/client/conversations/transcriptBottomPinning.ts +40 -24
- package/src/client/conversations/transcriptReadPosition.ts +31 -7
- package/src/client/conversations/transcriptRenderModel.ts +1 -14
- package/src/client/conversations/transcriptSearch.tsx +3 -11
- package/src/client/conversations/transcriptUnreadHistory.ts +248 -0
- package/src/client/conversations/usePinConversation.ts +88 -0
- package/src/client/display-text/DisplayText.tsx +7 -2
- package/src/client/display-text/codeBlocks.ts +92 -0
- package/src/client/format.ts +3 -67
- package/src/client/http.ts +5 -5
- package/src/client/markdownExport.ts +32 -42
- package/src/client/pages/code/RepositoryTabs.tsx +2 -2
- package/src/client/pages/memory/MemoryDetails.tsx +6 -33
- package/src/client/pages/memory/MemoryPage.tsx +7 -92
- package/src/client/pages/memory/MemoryTimeline.tsx +16 -32
- package/src/client/pages/memory/memoryDashboard.ts +2 -38
- package/src/client/pages/memory/memoryRecord.ts +1 -6
- package/src/client/pages/settings/AutoArchiveSettings.tsx +145 -0
- package/src/client/pages/settings/SettingsPage.tsx +2 -0
- package/src/client/review-desk/ConversationReviewItems.tsx +7 -9
- package/src/client/review-desk/queries.ts +2 -1
- package/src/client/types.ts +19 -33
- package/src/conversation-cache.ts +7 -9
- package/src/mock-reporting/fixtures.ts +126 -60
- package/src/mock-reporting/routes.ts +50 -1
- package/src/tailwind.css +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.278.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -34,16 +34,17 @@
|
|
|
34
34
|
"react-dom": "^19.2.6",
|
|
35
35
|
"react-is": "^19.2.6",
|
|
36
36
|
"react-markdown": "^10.1.0",
|
|
37
|
+
"react-resizable-panels": "^4.17.0",
|
|
37
38
|
"react-router": "^7.18.2",
|
|
38
39
|
"recharts": "3.8.0",
|
|
39
40
|
"remark-breaks": "^4.0.0",
|
|
40
41
|
"remark-gfm": "^4.0.1",
|
|
41
42
|
"shiki": "4.1.0",
|
|
42
43
|
"zod": "^4.5.4",
|
|
43
|
-
"@sentry/junior-plugin-api": "0.
|
|
44
|
+
"@sentry/junior-plugin-api": "0.278.0"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
46
|
-
"@sentry/junior": "0.
|
|
47
|
+
"@sentry/junior": "0.278.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@oxlint/plugins": "1.79.0",
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
"tw-animate-css": "^1.4.0",
|
|
65
66
|
"typescript": "^7.0.2",
|
|
66
67
|
"vitest": "^4.1.12",
|
|
67
|
-
"@sentry/junior": "0.
|
|
68
|
+
"@sentry/junior": "0.278.0"
|
|
68
69
|
},
|
|
69
70
|
"scripts": {
|
|
70
71
|
"build": "pnpm --filter @sentry/junior-plugin-api build && pnpm --filter @sentry/junior build && pnpm run build:client && pnpm run build:css && pnpm run build:server",
|
package/src/api/schema.ts
CHANGED
|
@@ -13,9 +13,16 @@ export const dashboardSessionSchema = z
|
|
|
13
13
|
.object({ user: dashboardSessionUserSchema })
|
|
14
14
|
.strict();
|
|
15
15
|
|
|
16
|
+
/** Longest auto-archive grace period a user can pick: one week. */
|
|
17
|
+
export const AUTO_ARCHIVE_MAX_GRACE_HOURS = 7 * 24;
|
|
18
|
+
|
|
16
19
|
/** Signed-in viewer returned by `/api/me`. */
|
|
17
20
|
export const dashboardIdentitySchema = z
|
|
18
21
|
.object({
|
|
22
|
+
/** Auto-archive the viewer's conversations when their work is done. */
|
|
23
|
+
autoArchiveDefault: z.boolean().optional(),
|
|
24
|
+
/** Hours done work and the conversation stay quiet before auto-archive. */
|
|
25
|
+
autoArchiveGraceHours: z.number().int().positive().optional(),
|
|
19
26
|
user: dashboardSessionUserSchema
|
|
20
27
|
.extend({
|
|
21
28
|
/** Junior API path of the viewer's Slack photo, then Google photo. */
|
|
@@ -29,9 +36,20 @@ export const dashboardIdentitySchema = z
|
|
|
29
36
|
|
|
30
37
|
export const dashboardProfileUpdateSchema = z
|
|
31
38
|
.object({
|
|
32
|
-
|
|
39
|
+
autoArchiveDefault: z.boolean().optional(),
|
|
40
|
+
autoArchiveGraceHours: z
|
|
41
|
+
.number()
|
|
42
|
+
.int()
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(AUTO_ARCHIVE_MAX_GRACE_HOURS)
|
|
45
|
+
.optional(),
|
|
46
|
+
displayName: z.string().trim().min(1).max(80).optional(),
|
|
33
47
|
})
|
|
34
|
-
.strict()
|
|
48
|
+
.strict()
|
|
49
|
+
.refine(
|
|
50
|
+
(body) => Object.values(body).some((value) => value !== undefined),
|
|
51
|
+
"Change at least one setting.",
|
|
52
|
+
);
|
|
35
53
|
|
|
36
54
|
export const dashboardConfigSchema = z
|
|
37
55
|
.object({
|
package/src/app.ts
CHANGED
|
@@ -5,8 +5,12 @@ import {
|
|
|
5
5
|
createJuniorApi,
|
|
6
6
|
jsonResponse,
|
|
7
7
|
readViewerAvatarUrl,
|
|
8
|
+
DEFAULT_AUTO_ARCHIVE_SETTINGS,
|
|
9
|
+
readUserAutoArchiveSettings,
|
|
8
10
|
resolveViewerUser,
|
|
11
|
+
updateUserAutoArchiveSettings,
|
|
9
12
|
updateViewerDisplayName,
|
|
13
|
+
type AutoArchiveSettings,
|
|
10
14
|
type ConversationWorkQueue,
|
|
11
15
|
type JuniorApiVariables,
|
|
12
16
|
} from "@sentry/junior/api";
|
|
@@ -38,7 +42,7 @@ import {
|
|
|
38
42
|
type DashboardSession,
|
|
39
43
|
} from "./auth";
|
|
40
44
|
import { isAuthPath, type AuthenticatedRoute } from "./authenticated-routes";
|
|
41
|
-
import {
|
|
45
|
+
import { resetMockConversationViewerState } from "./mock-reporting/fixtures";
|
|
42
46
|
import { resetMockReadPositions } from "./mock-reporting/read-positions";
|
|
43
47
|
import { createMockReportingApi } from "./mock-reporting/routes";
|
|
44
48
|
import {
|
|
@@ -62,8 +66,9 @@ const DEFAULT_BASE_PATH = "/";
|
|
|
62
66
|
const DEFAULT_AUTH_PATH = "/api/auth";
|
|
63
67
|
const LOGIN_NEXT_PARAM = "next";
|
|
64
68
|
const LOCAL_VIEWER_EMAIL = "dev@example.com";
|
|
65
|
-
/** Process-local
|
|
69
|
+
/** Process-local profile settings for mock reporting only. */
|
|
66
70
|
const mockDisplayNamesByEmail = new Map<string, string>();
|
|
71
|
+
const mockAutoArchiveSettingsByEmail = new Map<string, AutoArchiveSettings>();
|
|
67
72
|
|
|
68
73
|
/**
|
|
69
74
|
* Clear process-local mock reporting state so tests sharing a worker do not
|
|
@@ -74,7 +79,8 @@ const mockDisplayNamesByEmail = new Map<string, string>();
|
|
|
74
79
|
*/
|
|
75
80
|
export function resetMockDashboardState(): void {
|
|
76
81
|
mockDisplayNamesByEmail.clear();
|
|
77
|
-
|
|
82
|
+
resetMockConversationViewerState();
|
|
83
|
+
mockAutoArchiveSettingsByEmail.clear();
|
|
78
84
|
resetMockReadPositions();
|
|
79
85
|
}
|
|
80
86
|
|
|
@@ -255,6 +261,25 @@ function callbackUrl(
|
|
|
255
261
|
return url.toString();
|
|
256
262
|
}
|
|
257
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Restart sign-in with the same return path when the Google callback fails,
|
|
266
|
+
* for example on a stale OAuth state cookie. Without it, Better Auth sends
|
|
267
|
+
* the person to `/` and the page is lost.
|
|
268
|
+
*/
|
|
269
|
+
function signInRetryUrl(
|
|
270
|
+
request: Request,
|
|
271
|
+
basePath: string,
|
|
272
|
+
options: DashboardReturnPathOptions = {},
|
|
273
|
+
): string {
|
|
274
|
+
const url = new URL(request.url);
|
|
275
|
+
const returnPath = requestedReturnPath(url, basePath, options);
|
|
276
|
+
url.search = "";
|
|
277
|
+
if (returnPath) {
|
|
278
|
+
url.searchParams.set(LOGIN_NEXT_PARAM, returnPath);
|
|
279
|
+
}
|
|
280
|
+
return url.toString();
|
|
281
|
+
}
|
|
282
|
+
|
|
258
283
|
function unauthorized(
|
|
259
284
|
request: Request,
|
|
260
285
|
basePath: string,
|
|
@@ -397,7 +422,11 @@ export function createDashboardApp(
|
|
|
397
422
|
if (session && dashboardSessionIsAuthorized(session, allowedDomains)) {
|
|
398
423
|
return Response.redirect(returnUrl, 302);
|
|
399
424
|
}
|
|
400
|
-
return auth.signInWithGoogle(
|
|
425
|
+
return auth.signInWithGoogle(
|
|
426
|
+
c.req.raw,
|
|
427
|
+
returnUrl,
|
|
428
|
+
signInRetryUrl(c.req.raw, basePath, { authenticatedRoutes }),
|
|
429
|
+
);
|
|
401
430
|
});
|
|
402
431
|
|
|
403
432
|
if (auth) {
|
|
@@ -508,7 +537,11 @@ export function createDashboardApp(
|
|
|
508
537
|
};
|
|
509
538
|
|
|
510
539
|
app.use("*", requireAuth);
|
|
511
|
-
|
|
540
|
+
// Core detail reads set a version token before they build the report. Mock
|
|
541
|
+
// reporting has no stored version, so it revalidates on content instead.
|
|
542
|
+
if (options.mockConversations) {
|
|
543
|
+
app.use("/api/conversations/:conversationId", revalidateConversation);
|
|
544
|
+
}
|
|
512
545
|
|
|
513
546
|
for (const route of authenticatedRoutes) {
|
|
514
547
|
const handler = (c: Context<{ Variables: Variables }>) => {
|
|
@@ -576,11 +609,23 @@ export function createDashboardApp(
|
|
|
576
609
|
userId && !options.mockConversations
|
|
577
610
|
? await readViewerAvatarUrl(userId)
|
|
578
611
|
: undefined;
|
|
612
|
+
async function readViewerAutoArchiveSettings(viewer: {
|
|
613
|
+
email: string;
|
|
614
|
+
id: string;
|
|
615
|
+
}): Promise<AutoArchiveSettings> {
|
|
616
|
+
return (
|
|
617
|
+
(options.mockConversations
|
|
618
|
+
? mockAutoArchiveSettingsByEmail.get(viewer.email)
|
|
619
|
+
: await readUserAutoArchiveSettings(viewer.id)) ??
|
|
620
|
+
DEFAULT_AUTO_ARCHIVE_SETTINGS
|
|
621
|
+
);
|
|
622
|
+
}
|
|
579
623
|
app.get("/api/me", async (c) => {
|
|
580
624
|
const session = c.get("authSession");
|
|
581
625
|
const viewer = c.get("viewer");
|
|
582
626
|
const avatarUrl = await viewerAvatarUrl(viewer?.id);
|
|
583
627
|
return jsonResponse(dashboardIdentitySchema, {
|
|
628
|
+
...(viewer ? await readViewerAutoArchiveSettings(viewer) : undefined),
|
|
584
629
|
user: {
|
|
585
630
|
...(avatarUrl ? { avatarUrl } : undefined),
|
|
586
631
|
email: session.user.email,
|
|
@@ -618,22 +663,34 @@ export function createDashboardApp(
|
|
|
618
663
|
);
|
|
619
664
|
}
|
|
620
665
|
const session = c.get("authSession");
|
|
666
|
+
const { displayName, ...autoArchive } = parsed.data;
|
|
667
|
+
// Parsed bodies omit settings the request did not send.
|
|
668
|
+
const changesAutoArchive = Object.keys(autoArchive).length > 0;
|
|
621
669
|
// Mock reporting keeps profile edits process-local and out of SQL.
|
|
622
670
|
if (options.mockConversations) {
|
|
623
|
-
|
|
671
|
+
if (displayName !== undefined) {
|
|
672
|
+
mockDisplayNamesByEmail.set(viewer.email, displayName);
|
|
673
|
+
}
|
|
674
|
+
if (changesAutoArchive) {
|
|
675
|
+
mockAutoArchiveSettingsByEmail.set(viewer.email, {
|
|
676
|
+
...(await readViewerAutoArchiveSettings(viewer)),
|
|
677
|
+
...autoArchive,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
624
680
|
return jsonResponse(dashboardIdentitySchema, {
|
|
681
|
+
...(await readViewerAutoArchiveSettings(viewer)),
|
|
625
682
|
user: {
|
|
626
683
|
email: session.user.email,
|
|
627
684
|
emailVerified: session.user.emailVerified,
|
|
628
685
|
isAdmin: viewer.isAdmin === true,
|
|
629
|
-
name:
|
|
686
|
+
name: mockDisplayNamesByEmail.get(viewer.email) ?? viewer.displayName,
|
|
630
687
|
},
|
|
631
688
|
});
|
|
632
689
|
}
|
|
633
|
-
const updated =
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
690
|
+
const updated =
|
|
691
|
+
displayName === undefined
|
|
692
|
+
? viewer
|
|
693
|
+
: await updateViewerDisplayName(viewer.id, displayName);
|
|
637
694
|
if (!updated) {
|
|
638
695
|
return jsonResponse(
|
|
639
696
|
apiErrorSchema,
|
|
@@ -642,13 +699,24 @@ export function createDashboardApp(
|
|
|
642
699
|
);
|
|
643
700
|
}
|
|
644
701
|
const avatarUrl = await viewerAvatarUrl(updated.id);
|
|
702
|
+
const savedAutoArchive = changesAutoArchive
|
|
703
|
+
? await updateUserAutoArchiveSettings(viewer.id, autoArchive)
|
|
704
|
+
: await readUserAutoArchiveSettings(viewer.id);
|
|
705
|
+
if (!savedAutoArchive) {
|
|
706
|
+
return jsonResponse(
|
|
707
|
+
apiErrorSchema,
|
|
708
|
+
{ error: "User not found." },
|
|
709
|
+
{ status: 404 },
|
|
710
|
+
);
|
|
711
|
+
}
|
|
645
712
|
return jsonResponse(dashboardIdentitySchema, {
|
|
713
|
+
...savedAutoArchive,
|
|
646
714
|
user: {
|
|
647
715
|
...(avatarUrl ? { avatarUrl } : undefined),
|
|
648
716
|
email: session.user.email,
|
|
649
717
|
emailVerified: session.user.emailVerified,
|
|
650
718
|
isAdmin: updated.isAdmin === true,
|
|
651
|
-
name: updated.displayName,
|
|
719
|
+
name: updated.displayName ?? session.user.name,
|
|
652
720
|
},
|
|
653
721
|
});
|
|
654
722
|
});
|
package/src/auth.ts
CHANGED
|
@@ -9,7 +9,11 @@ export type { DashboardSession };
|
|
|
9
9
|
export interface DashboardAuth {
|
|
10
10
|
handler(request: Request): Promise<Response>;
|
|
11
11
|
getSession(request: Request): Promise<DashboardSession | null>;
|
|
12
|
-
signInWithGoogle(
|
|
12
|
+
signInWithGoogle(
|
|
13
|
+
request: Request,
|
|
14
|
+
callbackURL: string,
|
|
15
|
+
errorCallbackURL: string,
|
|
16
|
+
): Promise<Response>;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
/** Read a personal bearer token from one dashboard API request. */
|
package/src/client/App.tsx
CHANGED
|
@@ -34,6 +34,7 @@ import { useDashboardServerVersion } from "./dashboard-version";
|
|
|
34
34
|
import { ConversationWorkspace } from "./conversations/ConversationWorkspace";
|
|
35
35
|
import { ConversationWorkspaceLoading } from "./conversations/ConversationWorkspaceLoading";
|
|
36
36
|
import { useConversationData } from "./conversations/queries";
|
|
37
|
+
import { readConversationStartPosition } from "./conversations/transcriptReadPosition";
|
|
37
38
|
import { codeRepositoryTabs } from "./pages/code/codeRepositoryRoutes";
|
|
38
39
|
import { RepositoryPage } from "./pages/code/RepositoryPage";
|
|
39
40
|
import { CodePage } from "./pages/code/CodePage";
|
|
@@ -154,8 +155,12 @@ export function DashboardShell() {
|
|
|
154
155
|
location.pathname.startsWith("/conversations/");
|
|
155
156
|
const conversationId = conversationIdFromPath(location.pathname);
|
|
156
157
|
const conversationsQuery = useConversationsData();
|
|
157
|
-
//
|
|
158
|
-
|
|
158
|
+
// The shell owns the open Conversation's data, and the page reads it. On a
|
|
159
|
+
// direct load, this fetch runs beside the dashboard core reads, before the
|
|
160
|
+
// page mounts. One fetch for each visit also brings the unread page.
|
|
161
|
+
const conversationDetail = useConversationData(conversationId, {
|
|
162
|
+
unreadPageAtOpen: readConversationStartPosition() === "unread",
|
|
163
|
+
});
|
|
159
164
|
const mobileConversation = useMemo(() => {
|
|
160
165
|
if (!conversationId) return undefined;
|
|
161
166
|
return buildConversations(
|
|
@@ -439,7 +444,7 @@ export function DashboardShell() {
|
|
|
439
444
|
loading ? (
|
|
440
445
|
<ConversationWorkspaceLoading detail={false} />
|
|
441
446
|
) : data ? (
|
|
442
|
-
<ConversationWorkspace />
|
|
447
|
+
<ConversationWorkspace detail={conversationDetail} />
|
|
443
448
|
) : (
|
|
444
449
|
<LoadingView
|
|
445
450
|
label={query.error?.message ?? "Dashboard unavailable"}
|
|
@@ -481,7 +486,7 @@ export function DashboardShell() {
|
|
|
481
486
|
loading ? (
|
|
482
487
|
<ConversationWorkspaceLoading detail />
|
|
483
488
|
) : data ? (
|
|
484
|
-
<ConversationWorkspace />
|
|
489
|
+
<ConversationWorkspace detail={conversationDetail} />
|
|
485
490
|
) : (
|
|
486
491
|
<LoadingView
|
|
487
492
|
label={query.error?.message ?? "Dashboard unavailable"}
|
|
@@ -124,7 +124,7 @@ export function DetailsPanel(props: {
|
|
|
124
124
|
</Button>
|
|
125
125
|
</div>
|
|
126
126
|
</header>
|
|
127
|
-
<div className="min-h-0 overflow-y-auto overscroll-contain">
|
|
127
|
+
<div className="thin-scrollbar min-h-0 overflow-y-auto overscroll-contain">
|
|
128
128
|
{props.children}
|
|
129
129
|
</div>
|
|
130
130
|
</aside>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { TriangleAlert } from "lucide-react";
|
|
3
3
|
import { cn } from "../styles";
|
|
4
|
+
import { Alert, AlertTitle } from "./ui/alert";
|
|
4
5
|
|
|
5
6
|
/** Keep form recovery text and actions visible without truncation. */
|
|
6
7
|
export function FormNotice(props: {
|
|
@@ -10,31 +11,27 @@ export function FormNotice(props: {
|
|
|
10
11
|
}) {
|
|
11
12
|
const error = props.tone === "error";
|
|
12
13
|
return (
|
|
13
|
-
|
|
14
|
+
// Flex, not the Alert grid: the text column keeps its content width and
|
|
15
|
+
// its margin collapses with the first paragraph. The children hold links
|
|
16
|
+
// and buttons, so they skip AlertDescription and its link styles.
|
|
17
|
+
<Alert
|
|
14
18
|
role={error ? "alert" : "status"}
|
|
15
19
|
className={cn(
|
|
16
|
-
"flex min-w-0
|
|
20
|
+
"flex min-w-0 rounded-md p-4 has-[>svg]:gap-x-3",
|
|
17
21
|
error
|
|
18
|
-
? "border-rose-300/25 bg-rose-300/5"
|
|
19
|
-
: "border-amber-300/25 bg-amber-300/5",
|
|
22
|
+
? "border-rose-300/25 bg-rose-300/5 *:[svg]:text-rose-300"
|
|
23
|
+
: "border-amber-300/25 bg-amber-300/5 *:[svg]:text-amber-300",
|
|
20
24
|
)}
|
|
21
25
|
>
|
|
22
|
-
<TriangleAlert
|
|
23
|
-
aria-hidden
|
|
24
|
-
size={18}
|
|
25
|
-
className={cn(
|
|
26
|
-
"mt-0.5 shrink-0",
|
|
27
|
-
error ? "text-rose-300" : "text-amber-300",
|
|
28
|
-
)}
|
|
29
|
-
/>
|
|
26
|
+
<TriangleAlert aria-hidden className="size-4.5 shrink-0" />
|
|
30
27
|
<div className="min-w-0">
|
|
31
|
-
<
|
|
28
|
+
<AlertTitle>{props.title}</AlertTitle>
|
|
32
29
|
{props.children ? (
|
|
33
30
|
<div className="mt-1 text-sm leading-relaxed text-dashboard-text-muted">
|
|
34
31
|
{props.children}
|
|
35
32
|
</div>
|
|
36
33
|
) : null}
|
|
37
34
|
</div>
|
|
38
|
-
</
|
|
35
|
+
</Alert>
|
|
39
36
|
);
|
|
40
37
|
}
|
|
@@ -17,6 +17,7 @@ import { cn, dashboardInteractiveTextClass } from "../styles";
|
|
|
17
17
|
import type { Identity } from "../types";
|
|
18
18
|
import { ThemeSwitcher } from "./ThemeSwitcher";
|
|
19
19
|
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
|
|
20
|
+
import { Button, buttonVariants } from "./ui/button";
|
|
20
21
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
|
21
22
|
|
|
22
23
|
type ProfileMenuProps = {
|
|
@@ -131,8 +132,7 @@ function SpendAmount({ value }: { value?: number }) {
|
|
|
131
132
|
);
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
const profileLinkClass =
|
|
135
|
-
"flex items-center gap-2.5 rounded-md px-2.5 py-2 text-sm font-semibold text-dashboard-text no-underline transition-colors hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:bg-dashboard-fill-strong focus-visible:text-dashboard-text focus-visible:outline-none";
|
|
135
|
+
const profileLinkClass = buttonVariants({ size: "menu" });
|
|
136
136
|
|
|
137
137
|
/** Group the signed-in identity, personal profile, and session actions. */
|
|
138
138
|
export function ProfileMenu({
|
|
@@ -218,17 +218,16 @@ export function ProfileMenu({
|
|
|
218
218
|
{page.label}
|
|
219
219
|
</Link>
|
|
220
220
|
))}
|
|
221
|
-
<
|
|
222
|
-
className="flex w-full cursor-pointer items-center gap-2.5 rounded-md border-0 bg-transparent px-2.5 py-2 text-left text-sm font-semibold text-dashboard-text transition-colors hover:bg-dashboard-fill-strong hover:text-dashboard-text focus-visible:bg-dashboard-fill-strong focus-visible:text-dashboard-text focus-visible:outline-none"
|
|
221
|
+
<Button
|
|
223
222
|
onClick={() => {
|
|
224
223
|
close();
|
|
225
224
|
void onSignOut();
|
|
226
225
|
}}
|
|
227
|
-
|
|
226
|
+
size="menu"
|
|
228
227
|
>
|
|
229
228
|
<LogOut aria-hidden="true" size={16} strokeWidth={2} />
|
|
230
229
|
Log out
|
|
231
|
-
</
|
|
230
|
+
</Button>
|
|
232
231
|
</>
|
|
233
232
|
);
|
|
234
233
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as ResizablePrimitive from "react-resizable-panels";
|
|
2
|
+
import { cn } from "@dashboard/lib/utils";
|
|
3
|
+
|
|
4
|
+
/** Lay out resizable panels in one row or column. */
|
|
5
|
+
function ResizablePanelGroup({
|
|
6
|
+
className,
|
|
7
|
+
...props
|
|
8
|
+
}: ResizablePrimitive.GroupProps) {
|
|
9
|
+
return (
|
|
10
|
+
<ResizablePrimitive.Group
|
|
11
|
+
data-slot="resizable-panel-group"
|
|
12
|
+
className={cn(
|
|
13
|
+
"flex h-full w-full aria-[orientation=vertical]:flex-col",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Hold one resizable region inside a panel group. */
|
|
22
|
+
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
|
|
23
|
+
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Drag or use arrow keys to resize the panels on each side. */
|
|
27
|
+
function ResizableHandle({
|
|
28
|
+
withHandle,
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}: ResizablePrimitive.SeparatorProps & {
|
|
32
|
+
withHandle?: boolean;
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<ResizablePrimitive.Separator
|
|
36
|
+
data-slot="resizable-handle"
|
|
37
|
+
className={cn(
|
|
38
|
+
"relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{withHandle && (
|
|
44
|
+
<div className="z-10 flex h-6 w-1 shrink-0 rounded-lg bg-border" />
|
|
45
|
+
)}
|
|
46
|
+
</ResizablePrimitive.Separator>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
import { CircleHelp } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { Checkbox } from "../components/ui/checkbox";
|
|
5
|
+
import { InlineError } from "../components/InlineError";
|
|
6
|
+
import {
|
|
7
|
+
Popover,
|
|
8
|
+
PopoverContent,
|
|
9
|
+
PopoverDescription,
|
|
10
|
+
PopoverTrigger,
|
|
11
|
+
} from "../components/ui/popover";
|
|
12
|
+
import { useSetConversationAutoArchive } from "./autoArchiveQueries";
|
|
13
|
+
|
|
14
|
+
const AUTO_ARCHIVE_HELP =
|
|
15
|
+
"Archive this conversation for you when its linked work is merged or closed and nobody replies during your wait time.";
|
|
16
|
+
|
|
17
|
+
/** Let a participant turn auto-archive on or off for this conversation. */
|
|
18
|
+
export function AutoArchiveToggle(props: {
|
|
19
|
+
autoArchive: boolean;
|
|
20
|
+
conversationId: string;
|
|
21
|
+
}) {
|
|
22
|
+
const update = useSetConversationAutoArchive(props.conversationId);
|
|
23
|
+
const helpId = useId();
|
|
24
|
+
return (
|
|
25
|
+
<div className="grid gap-1">
|
|
26
|
+
<div className="flex items-center gap-1.5">
|
|
27
|
+
<label className="flex cursor-pointer items-center gap-2.5">
|
|
28
|
+
<Checkbox
|
|
29
|
+
aria-describedby={helpId}
|
|
30
|
+
checked={props.autoArchive}
|
|
31
|
+
disabled={update.isPending}
|
|
32
|
+
onCheckedChange={(checked) => update.mutate(checked)}
|
|
33
|
+
/>
|
|
34
|
+
<span className="font-semibold text-dashboard-text">
|
|
35
|
+
Auto archive
|
|
36
|
+
</span>
|
|
37
|
+
</label>
|
|
38
|
+
{/* A popover, not a tooltip: touch screens open it with a tap. */}
|
|
39
|
+
<Popover>
|
|
40
|
+
<PopoverTrigger
|
|
41
|
+
delay={0}
|
|
42
|
+
openOnHover
|
|
43
|
+
render={
|
|
44
|
+
<button
|
|
45
|
+
aria-label="About auto archive"
|
|
46
|
+
className="grid place-items-center rounded-sm text-dashboard-text-muted hover:text-dashboard-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
47
|
+
type="button"
|
|
48
|
+
>
|
|
49
|
+
<CircleHelp aria-hidden="true" size={14} strokeWidth={2} />
|
|
50
|
+
</button>
|
|
51
|
+
}
|
|
52
|
+
/>
|
|
53
|
+
<PopoverContent
|
|
54
|
+
aria-label="About auto archive"
|
|
55
|
+
className="w-64 px-3 py-2 text-xs"
|
|
56
|
+
side="top"
|
|
57
|
+
>
|
|
58
|
+
<PopoverDescription>{AUTO_ARCHIVE_HELP}</PopoverDescription>
|
|
59
|
+
</PopoverContent>
|
|
60
|
+
</Popover>
|
|
61
|
+
{/* Keep the help text for screen readers when the popover is closed. */}
|
|
62
|
+
<span className="sr-only" id={helpId}>
|
|
63
|
+
{AUTO_ARCHIVE_HELP}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
{update.isError ? (
|
|
67
|
+
<InlineError>Could not save auto-archive. Try again.</InlineError>
|
|
68
|
+
) : null}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -5,7 +5,7 @@ import { Notice, NoticeAction } from "../components/Notice";
|
|
|
5
5
|
import { cn } from "../styles";
|
|
6
6
|
import type { Conversation } from "../types";
|
|
7
7
|
import { conversationDisplayTitle } from "../format";
|
|
8
|
-
import { useArchiveConversation } from "./
|
|
8
|
+
import { useArchiveConversation } from "./conversationArchive";
|
|
9
9
|
|
|
10
10
|
/** Show archive errors and the most recent reversible archive action. */
|
|
11
11
|
export function ConversationArchiveNotices(props: {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
formatUsageTotal,
|
|
25
25
|
} from "../format";
|
|
26
26
|
import { fetchDashboardJson } from "../http";
|
|
27
|
+
import { conversationSideReadQueryKey } from "./queries";
|
|
27
28
|
import { cn } from "../styles";
|
|
28
29
|
import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
29
30
|
|
|
@@ -56,7 +57,7 @@ export function useConversationChildAgents(conversationId: string) {
|
|
|
56
57
|
`/api/conversations/${encodeURIComponent(conversationId)}/agents`,
|
|
57
58
|
signal,
|
|
58
59
|
),
|
|
59
|
-
queryKey:
|
|
60
|
+
queryKey: conversationSideReadQueryKey(conversationId, "agents"),
|
|
60
61
|
retry: false,
|
|
61
62
|
});
|
|
62
63
|
}
|
|
@@ -39,6 +39,8 @@ type Brief = NonNullable<ConversationDetailReport["brief"]>;
|
|
|
39
39
|
* it, and what it cost. Each fact shows here only, not in the title bar.
|
|
40
40
|
*/
|
|
41
41
|
export function ConversationDetails(props: {
|
|
42
|
+
/** Viewer's auto-archive control. Null for non-participants. */
|
|
43
|
+
autoArchive: ReactNode;
|
|
42
44
|
brief?: Brief;
|
|
43
45
|
conversationId: string;
|
|
44
46
|
/** True when a sidebar review chip opened details. */
|
|
@@ -50,8 +52,6 @@ export function ConversationDetails(props: {
|
|
|
50
52
|
onOpenSubagent(target: SubagentTranscriptTarget): void;
|
|
51
53
|
/** Show one memory where its Turn recalled or captured it. */
|
|
52
54
|
onSelectMemory(entry: ConversationMemoryEntry & { turnId: string }): void;
|
|
53
|
-
/** The prompt that started each loaded Turn. */
|
|
54
|
-
memoryPrompts: Map<string, string>;
|
|
55
55
|
overview: ReactNode;
|
|
56
56
|
usage: ReactNode;
|
|
57
57
|
}) {
|
|
@@ -98,7 +98,6 @@ export function ConversationDetails(props: {
|
|
|
98
98
|
if (!panel) props.onClose();
|
|
99
99
|
props.onSelectMemory(entry);
|
|
100
100
|
}}
|
|
101
|
-
prompts={props.memoryPrompts}
|
|
102
101
|
/>
|
|
103
102
|
</div>
|
|
104
103
|
</DetailsPanel>
|
|
@@ -167,6 +166,10 @@ export function ConversationDetails(props: {
|
|
|
167
166
|
) : null}
|
|
168
167
|
</DetailsCard>
|
|
169
168
|
) : null}
|
|
169
|
+
{/* Linked work decides when auto-archive runs, so it follows. */}
|
|
170
|
+
{props.autoArchive != null ? (
|
|
171
|
+
<DetailsCard>{props.autoArchive}</DetailsCard>
|
|
172
|
+
) : null}
|
|
170
173
|
{agents?.length ? (
|
|
171
174
|
<DetailsCard count={agents.length} title="Subagents">
|
|
172
175
|
<ConversationChildAgents
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
HeaderIconButton,
|
|
15
15
|
TranscriptSearchToggle,
|
|
16
16
|
type ConversationArchiveAction,
|
|
17
|
+
type ConversationPinAction,
|
|
17
18
|
} from "./ConversationHeaderActions";
|
|
18
19
|
import { ConversationOverflowMenu } from "./ConversationOverflowMenu";
|
|
19
20
|
import { ShareConversationButton } from "./ShareConversationButton";
|
|
@@ -44,9 +45,11 @@ export function ConversationHeader(props: {
|
|
|
44
45
|
location?: ReactNode;
|
|
45
46
|
onSearchChange(value: string): void;
|
|
46
47
|
onViewChange(value: TranscriptViewMode): void;
|
|
48
|
+
pin: ConversationPinAction;
|
|
47
49
|
privacy: ReactNode;
|
|
48
50
|
search: string;
|
|
49
51
|
sentryConversationUrl?: string;
|
|
52
|
+
sidebarToggle?: ReactNode;
|
|
50
53
|
title: string;
|
|
51
54
|
view: TranscriptViewMode;
|
|
52
55
|
}) {
|
|
@@ -92,6 +95,7 @@ export function ConversationHeader(props: {
|
|
|
92
95
|
<ConversationHeaderActions
|
|
93
96
|
archive={props.archive}
|
|
94
97
|
conversationId={props.conversationId}
|
|
98
|
+
pin={props.pin}
|
|
95
99
|
copyAction={props.copyAction}
|
|
96
100
|
detailsOpen={detailsOpen}
|
|
97
101
|
onDetailsClick={() => {
|
|
@@ -116,6 +120,7 @@ export function ConversationHeader(props: {
|
|
|
116
120
|
const showMobileHeader =
|
|
117
121
|
searchOpenVisible ||
|
|
118
122
|
props.archive.error ||
|
|
123
|
+
props.pin.error ||
|
|
119
124
|
Boolean(props.linkedWork) ||
|
|
120
125
|
Boolean(props.forks) ||
|
|
121
126
|
Boolean(props.agentTree);
|
|
@@ -161,8 +166,11 @@ export function ConversationHeader(props: {
|
|
|
161
166
|
!showMobileHeader && "hidden md:block",
|
|
162
167
|
)}
|
|
163
168
|
>
|
|
164
|
-
<div className="hidden h-
|
|
169
|
+
<div className="hidden h-13 min-w-0 items-center justify-between gap-4 px-4 md:flex">
|
|
165
170
|
<div className="flex min-w-0 items-center gap-2">
|
|
171
|
+
{props.sidebarToggle ? (
|
|
172
|
+
<div className="shrink-0">{props.sidebarToggle}</div>
|
|
173
|
+
) : null}
|
|
166
174
|
{props.agentTree ? (
|
|
167
175
|
<div className="min-w-0 shrink-[2]">{props.agentTree}</div>
|
|
168
176
|
) : props.location ? (
|
|
@@ -216,9 +224,9 @@ export function ConversationHeader(props: {
|
|
|
216
224
|
</ConversationOverflowMenu>
|
|
217
225
|
<HeaderIconButton
|
|
218
226
|
ref={detailsButtonRef}
|
|
227
|
+
expanded={detailsOpen}
|
|
219
228
|
label="Conversation details"
|
|
220
229
|
onClick={props.onDetailsClick}
|
|
221
|
-
pressed={detailsOpen}
|
|
222
230
|
>
|
|
223
231
|
<PanelRight aria-hidden="true" size={15} strokeWidth={2} />
|
|
224
232
|
</HeaderIconButton>
|
|
@@ -259,6 +267,11 @@ export function ConversationHeader(props: {
|
|
|
259
267
|
Could not update archive state.
|
|
260
268
|
</div>
|
|
261
269
|
) : null}
|
|
270
|
+
{props.pin.error ? (
|
|
271
|
+
<div className="px-4 pb-3 text-xs text-red-300 md:px-7">
|
|
272
|
+
Could not update pin.
|
|
273
|
+
</div>
|
|
274
|
+
) : null}
|
|
262
275
|
</header>
|
|
263
276
|
</>
|
|
264
277
|
);
|