@sentry/junior-dashboard 0.275.0 → 0.277.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 +194 -40
- package/dist/client.js +120 -92
- package/dist/index.d.ts +1 -1
- package/dist/index.js +189 -38
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +20 -2
- package/src/app.ts +88 -15
- package/src/auth.ts +5 -1
- package/src/client/components/FormNotice.tsx +11 -14
- package/src/client/components/ProfileMenu.tsx +5 -6
- package/src/client/conversations/AutoArchiveToggle.tsx +35 -0
- package/src/client/conversations/ConversationArchiveNotices.tsx +1 -1
- package/src/client/conversations/ConversationChildAgents.tsx +2 -1
- package/src/client/conversations/ConversationComposer.tsx +42 -26
- package/src/client/conversations/ConversationDetails.tsx +66 -35
- package/src/client/conversations/ConversationHeader.tsx +19 -2
- package/src/client/conversations/ConversationHeaderActions.tsx +23 -0
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
- package/src/client/conversations/ConversationMemories.tsx +114 -77
- package/src/client/conversations/ConversationMeta.tsx +47 -42
- package/src/client/conversations/ConversationPage.tsx +183 -60
- package/src/client/conversations/ConversationSidebar.tsx +80 -24
- package/src/client/conversations/ConversationTranscript.tsx +37 -8
- package/src/client/conversations/ConversationWatches.tsx +42 -26
- package/src/client/conversations/ConversationWorkspace.tsx +4 -4
- package/src/client/conversations/DetailsList.tsx +8 -14
- package/src/client/conversations/MemoryIcons.tsx +23 -0
- package/src/client/conversations/PendingMailboxStack.tsx +11 -1
- package/src/client/conversations/QuickQuestion.tsx +476 -0
- package/src/client/conversations/TelemetryMetrics.tsx +2 -1
- package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
- package/src/client/conversations/TranscriptEventView.tsx +25 -2
- package/src/client/conversations/TranscriptMemoryPopover.tsx +174 -0
- package/src/client/conversations/TranscriptMessageView.tsx +46 -16
- package/src/client/conversations/TranscriptRows.tsx +9 -2
- package/src/client/conversations/TranscriptTurnContextView.tsx +9 -5
- package/src/client/conversations/UsageCard.tsx +3 -3
- package/src/client/conversations/autoArchiveQueries.ts +53 -0
- package/src/client/conversations/conversationArchive.ts +255 -0
- package/src/client/conversations/conversationMemory.ts +139 -0
- package/src/client/conversations/conversationSections.ts +24 -18
- package/src/client/conversations/eventTranscript.ts +8 -0
- package/src/client/conversations/queries.ts +48 -238
- 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/pages/code/RepositoryTabs.tsx +2 -2
- 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/ReviewItemActions.tsx +1 -1
- package/src/client/review-desk/ReviewItemChangeRow.tsx +85 -0
- package/src/client/review-desk/ReviewItemContext.tsx +41 -23
- package/src/client/review-desk/queries.ts +2 -1
- package/src/client/review-desk/reviewItemFormat.ts +16 -0
- package/src/client/sentry.ts +77 -0
- package/src/client/types.ts +7 -0
- package/src/client.tsx +3 -0
- package/src/conversation-cache.ts +1 -1
- package/src/mock-reporting/fixtures.ts +103 -27
- package/src/mock-reporting/routes.ts +47 -0
- package/src/mock-reporting/watches.ts +2 -0
- package/src/shell.ts +27 -2
- package/src/tailwind.css +8 -19
- package/src/client/components/ActiveIndicator.tsx +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-dashboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.277.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@base-ui/react": "^1.9.0",
|
|
26
|
+
"@sentry/browser": "11.10.0",
|
|
26
27
|
"@tanstack/react-query": "^5.100.14",
|
|
27
28
|
"@uidotdev/usehooks": "2.4.1",
|
|
28
29
|
"class-variance-authority": "^0.7.1",
|
|
@@ -39,10 +40,10 @@
|
|
|
39
40
|
"remark-gfm": "^4.0.1",
|
|
40
41
|
"shiki": "4.1.0",
|
|
41
42
|
"zod": "^4.5.4",
|
|
42
|
-
"@sentry/junior-plugin-api": "0.
|
|
43
|
+
"@sentry/junior-plugin-api": "0.277.0"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
|
-
"@sentry/junior": "0.
|
|
46
|
+
"@sentry/junior": "0.277.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@oxlint/plugins": "1.79.0",
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"tw-animate-css": "^1.4.0",
|
|
64
65
|
"typescript": "^7.0.2",
|
|
65
66
|
"vitest": "^4.1.12",
|
|
66
|
-
"@sentry/junior": "0.
|
|
67
|
+
"@sentry/junior": "0.277.0"
|
|
67
68
|
},
|
|
68
69
|
"scripts": {
|
|
69
70
|
"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,13 +5,20 @@ 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";
|
|
13
17
|
import { apiErrorSchema } from "@sentry/junior/api/schema";
|
|
14
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
getBrowserSentryConfig,
|
|
20
|
+
initSentry,
|
|
21
|
+
} from "@sentry/junior/instrumentation";
|
|
15
22
|
import { JUNIOR_VERSION } from "@sentry/junior/version";
|
|
16
23
|
import { DASHBOARD_VERSION_HEADER } from "./dashboard-version";
|
|
17
24
|
import { revalidateConversation } from "./conversation-cache";
|
|
@@ -35,7 +42,7 @@ import {
|
|
|
35
42
|
type DashboardSession,
|
|
36
43
|
} from "./auth";
|
|
37
44
|
import { isAuthPath, type AuthenticatedRoute } from "./authenticated-routes";
|
|
38
|
-
import {
|
|
45
|
+
import { resetMockConversationViewerState } from "./mock-reporting/fixtures";
|
|
39
46
|
import { resetMockReadPositions } from "./mock-reporting/read-positions";
|
|
40
47
|
import { createMockReportingApi } from "./mock-reporting/routes";
|
|
41
48
|
import {
|
|
@@ -59,8 +66,9 @@ const DEFAULT_BASE_PATH = "/";
|
|
|
59
66
|
const DEFAULT_AUTH_PATH = "/api/auth";
|
|
60
67
|
const LOGIN_NEXT_PARAM = "next";
|
|
61
68
|
const LOCAL_VIEWER_EMAIL = "dev@example.com";
|
|
62
|
-
/** Process-local
|
|
69
|
+
/** Process-local profile settings for mock reporting only. */
|
|
63
70
|
const mockDisplayNamesByEmail = new Map<string, string>();
|
|
71
|
+
const mockAutoArchiveSettingsByEmail = new Map<string, AutoArchiveSettings>();
|
|
64
72
|
|
|
65
73
|
/**
|
|
66
74
|
* Clear process-local mock reporting state so tests sharing a worker do not
|
|
@@ -71,7 +79,8 @@ const mockDisplayNamesByEmail = new Map<string, string>();
|
|
|
71
79
|
*/
|
|
72
80
|
export function resetMockDashboardState(): void {
|
|
73
81
|
mockDisplayNamesByEmail.clear();
|
|
74
|
-
|
|
82
|
+
resetMockConversationViewerState();
|
|
83
|
+
mockAutoArchiveSettingsByEmail.clear();
|
|
75
84
|
resetMockReadPositions();
|
|
76
85
|
}
|
|
77
86
|
|
|
@@ -252,6 +261,25 @@ function callbackUrl(
|
|
|
252
261
|
return url.toString();
|
|
253
262
|
}
|
|
254
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
|
+
|
|
255
283
|
function unauthorized(
|
|
256
284
|
request: Request,
|
|
257
285
|
basePath: string,
|
|
@@ -394,7 +422,11 @@ export function createDashboardApp(
|
|
|
394
422
|
if (session && dashboardSessionIsAuthorized(session, allowedDomains)) {
|
|
395
423
|
return Response.redirect(returnUrl, 302);
|
|
396
424
|
}
|
|
397
|
-
return auth.signInWithGoogle(
|
|
425
|
+
return auth.signInWithGoogle(
|
|
426
|
+
c.req.raw,
|
|
427
|
+
returnUrl,
|
|
428
|
+
signInRetryUrl(c.req.raw, basePath, { authenticatedRoutes }),
|
|
429
|
+
);
|
|
398
430
|
});
|
|
399
431
|
|
|
400
432
|
if (auth) {
|
|
@@ -505,7 +537,11 @@ export function createDashboardApp(
|
|
|
505
537
|
};
|
|
506
538
|
|
|
507
539
|
app.use("*", requireAuth);
|
|
508
|
-
|
|
540
|
+
// Core detail reads set a version ETag 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
|
+
}
|
|
509
545
|
|
|
510
546
|
for (const route of authenticatedRoutes) {
|
|
511
547
|
const handler = (c: Context<{ Variables: Variables }>) => {
|
|
@@ -527,10 +563,12 @@ export function createDashboardApp(
|
|
|
527
563
|
}
|
|
528
564
|
}
|
|
529
565
|
|
|
566
|
+
const renderPage = () =>
|
|
567
|
+
renderDashboard(basePath, agentName, getBrowserSentryConfig());
|
|
530
568
|
for (const { nested, path } of dashboardPagePaths(basePath)) {
|
|
531
|
-
app.get(path,
|
|
569
|
+
app.get(path, renderPage);
|
|
532
570
|
if (nested) {
|
|
533
|
-
app.get(`${path}/*`,
|
|
571
|
+
app.get(`${path}/*`, renderPage);
|
|
534
572
|
}
|
|
535
573
|
}
|
|
536
574
|
for (const route of options.pluginRoutes ?? []) {
|
|
@@ -571,11 +609,23 @@ export function createDashboardApp(
|
|
|
571
609
|
userId && !options.mockConversations
|
|
572
610
|
? await readViewerAvatarUrl(userId)
|
|
573
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
|
+
}
|
|
574
623
|
app.get("/api/me", async (c) => {
|
|
575
624
|
const session = c.get("authSession");
|
|
576
625
|
const viewer = c.get("viewer");
|
|
577
626
|
const avatarUrl = await viewerAvatarUrl(viewer?.id);
|
|
578
627
|
return jsonResponse(dashboardIdentitySchema, {
|
|
628
|
+
...(viewer ? await readViewerAutoArchiveSettings(viewer) : undefined),
|
|
579
629
|
user: {
|
|
580
630
|
...(avatarUrl ? { avatarUrl } : undefined),
|
|
581
631
|
email: session.user.email,
|
|
@@ -613,22 +663,34 @@ export function createDashboardApp(
|
|
|
613
663
|
);
|
|
614
664
|
}
|
|
615
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;
|
|
616
669
|
// Mock reporting keeps profile edits process-local and out of SQL.
|
|
617
670
|
if (options.mockConversations) {
|
|
618
|
-
|
|
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
|
+
}
|
|
619
680
|
return jsonResponse(dashboardIdentitySchema, {
|
|
681
|
+
...(await readViewerAutoArchiveSettings(viewer)),
|
|
620
682
|
user: {
|
|
621
683
|
email: session.user.email,
|
|
622
684
|
emailVerified: session.user.emailVerified,
|
|
623
685
|
isAdmin: viewer.isAdmin === true,
|
|
624
|
-
name:
|
|
686
|
+
name: mockDisplayNamesByEmail.get(viewer.email) ?? viewer.displayName,
|
|
625
687
|
},
|
|
626
688
|
});
|
|
627
689
|
}
|
|
628
|
-
const updated =
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
690
|
+
const updated =
|
|
691
|
+
displayName === undefined
|
|
692
|
+
? viewer
|
|
693
|
+
: await updateViewerDisplayName(viewer.id, displayName);
|
|
632
694
|
if (!updated) {
|
|
633
695
|
return jsonResponse(
|
|
634
696
|
apiErrorSchema,
|
|
@@ -637,13 +699,24 @@ export function createDashboardApp(
|
|
|
637
699
|
);
|
|
638
700
|
}
|
|
639
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
|
+
}
|
|
640
712
|
return jsonResponse(dashboardIdentitySchema, {
|
|
713
|
+
...savedAutoArchive,
|
|
641
714
|
user: {
|
|
642
715
|
...(avatarUrl ? { avatarUrl } : undefined),
|
|
643
716
|
email: session.user.email,
|
|
644
717
|
emailVerified: session.user.emailVerified,
|
|
645
718
|
isAdmin: updated.isAdmin === true,
|
|
646
|
-
name: updated.displayName,
|
|
719
|
+
name: updated.displayName ?? session.user.name,
|
|
647
720
|
},
|
|
648
721
|
});
|
|
649
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. */
|
|
@@ -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,35 @@
|
|
|
1
|
+
import { Checkbox } from "../components/ui/checkbox";
|
|
2
|
+
import { InlineError } from "../components/InlineError";
|
|
3
|
+
import { useSetConversationAutoArchive } from "./autoArchiveQueries";
|
|
4
|
+
|
|
5
|
+
/** Let a participant turn auto-archive on or off for this conversation. */
|
|
6
|
+
export function AutoArchiveToggle(props: {
|
|
7
|
+
autoArchive: boolean;
|
|
8
|
+
conversationId: string;
|
|
9
|
+
}) {
|
|
10
|
+
const update = useSetConversationAutoArchive(props.conversationId);
|
|
11
|
+
return (
|
|
12
|
+
<div className="grid gap-1">
|
|
13
|
+
<label className="flex cursor-pointer items-start gap-2.5">
|
|
14
|
+
<Checkbox
|
|
15
|
+
checked={props.autoArchive}
|
|
16
|
+
className="mt-0.5"
|
|
17
|
+
disabled={update.isPending}
|
|
18
|
+
onCheckedChange={(checked) => update.mutate(checked)}
|
|
19
|
+
/>
|
|
20
|
+
<span className="grid gap-0.5">
|
|
21
|
+
<span className="font-semibold text-dashboard-text">
|
|
22
|
+
Auto-archive when done
|
|
23
|
+
</span>
|
|
24
|
+
<span>
|
|
25
|
+
Archive this conversation for you when its linked work is merged or
|
|
26
|
+
closed and nobody replies during your wait time.
|
|
27
|
+
</span>
|
|
28
|
+
</span>
|
|
29
|
+
</label>
|
|
30
|
+
{update.isError ? (
|
|
31
|
+
<InlineError>Could not save auto-archive. Try again.</InlineError>
|
|
32
|
+
) : null}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -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
|
}
|
|
@@ -81,7 +81,10 @@ type ConversationComposerProps = {
|
|
|
81
81
|
* crushed beside the mobile send button.
|
|
82
82
|
*/
|
|
83
83
|
footerStart?: ReactNode;
|
|
84
|
+
/** Text only, for a narrow panel: no images, no key hint, icon-only send. */
|
|
85
|
+
compact?: boolean;
|
|
84
86
|
label: string;
|
|
87
|
+
placeholder?: string;
|
|
85
88
|
/**
|
|
86
89
|
* Restore the submitted text after a failed accept when there is no mailbox
|
|
87
90
|
* outbox (new conversation create). Existing conversations keep failures in
|
|
@@ -433,7 +436,12 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
433
436
|
)}
|
|
434
437
|
onDragOver={(event) => {
|
|
435
438
|
if (!event.dataTransfer.types.includes("Files")) return;
|
|
439
|
+
// Always cancel a file drop, or the browser opens the file.
|
|
436
440
|
event.preventDefault();
|
|
441
|
+
if (props.compact) {
|
|
442
|
+
event.dataTransfer.dropEffect = "none";
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
437
445
|
event.dataTransfer.dropEffect = "copy";
|
|
438
446
|
setDragging(true);
|
|
439
447
|
}}
|
|
@@ -444,12 +452,13 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
444
452
|
onDrop={(event) => {
|
|
445
453
|
if (!event.dataTransfer.types.includes("Files")) return;
|
|
446
454
|
event.preventDefault();
|
|
455
|
+
if (props.compact) return;
|
|
447
456
|
setDragging(false);
|
|
448
457
|
void addImages(Array.from(event.dataTransfer.files));
|
|
449
458
|
}}
|
|
450
459
|
onPaste={(event) => {
|
|
451
460
|
const files = Array.from(event.clipboardData.files);
|
|
452
|
-
if (!files.length) return;
|
|
461
|
+
if (props.compact || !files.length) return;
|
|
453
462
|
event.preventDefault();
|
|
454
463
|
void addImages(files);
|
|
455
464
|
}}
|
|
@@ -510,7 +519,7 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
510
519
|
}}
|
|
511
520
|
onFocus={props.onFocus}
|
|
512
521
|
onKeyDown={handleKeyDown}
|
|
513
|
-
placeholder="Message Junior…"
|
|
522
|
+
placeholder={props.placeholder ?? "Message Junior…"}
|
|
514
523
|
ref={textareaRef}
|
|
515
524
|
rows={1}
|
|
516
525
|
spellCheck={autocorrect}
|
|
@@ -524,27 +533,29 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
524
533
|
)}
|
|
525
534
|
>
|
|
526
535
|
<div className="flex min-w-0 items-center gap-3">
|
|
527
|
-
|
|
528
|
-
<
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
536
|
+
{props.compact ? null : (
|
|
537
|
+
<Tooltip>
|
|
538
|
+
<TooltipTrigger
|
|
539
|
+
render={
|
|
540
|
+
<Button
|
|
541
|
+
focusableWhenDisabled
|
|
542
|
+
aria-label="Attach images"
|
|
543
|
+
disabled={sendLocked || readingImages}
|
|
544
|
+
onClick={() => fileInputRef.current?.click()}
|
|
545
|
+
size="icon"
|
|
546
|
+
type="button"
|
|
547
|
+
>
|
|
548
|
+
<ImagePlus aria-hidden="true" size={16} />
|
|
549
|
+
</Button>
|
|
550
|
+
}
|
|
551
|
+
/>
|
|
552
|
+
<TooltipContent size="compact">
|
|
553
|
+
Attach images (up to 3 MB total)
|
|
554
|
+
</TooltipContent>
|
|
555
|
+
</Tooltip>
|
|
556
|
+
)}
|
|
546
557
|
{props.footerStart}
|
|
547
|
-
{props.footerStart ? null : (
|
|
558
|
+
{props.footerStart || props.compact ? null : (
|
|
548
559
|
<div className="hidden min-w-0 font-sans text-xs leading-relaxed text-dashboard-text-muted md:block">
|
|
549
560
|
Enter to send · Shift+Enter for a new line
|
|
550
561
|
</div>
|
|
@@ -565,7 +576,10 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
565
576
|
) : null}
|
|
566
577
|
<Button
|
|
567
578
|
aria-label={sendLocked ? "Sending message" : props.submitLabel}
|
|
568
|
-
className=
|
|
579
|
+
className={cn(
|
|
580
|
+
"disabled:!border-transparent disabled:!bg-dashboard-fill-hover disabled:!text-dashboard-text-muted disabled:!opacity-100",
|
|
581
|
+
props.compact && "w-9 px-0",
|
|
582
|
+
)}
|
|
569
583
|
disabled={
|
|
570
584
|
(!canSend && !images.length) ||
|
|
571
585
|
!online ||
|
|
@@ -576,9 +590,11 @@ export const ConversationComposer = memo(function ConversationComposer(
|
|
|
576
590
|
type="submit"
|
|
577
591
|
>
|
|
578
592
|
<Send aria-hidden="true" size={14} />
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
593
|
+
{props.compact ? null : (
|
|
594
|
+
<span className="hidden md:inline">
|
|
595
|
+
{sendLocked ? "Sending…" : props.submitLabel}
|
|
596
|
+
</span>
|
|
597
|
+
)}
|
|
582
598
|
</Button>
|
|
583
599
|
</div>
|
|
584
600
|
</div>
|