@sentry/junior-dashboard 0.274.0 → 0.276.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/app.js +28 -9
  2. package/dist/client.js +115 -92
  3. package/dist/index.js +28 -9
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +5 -4
  6. package/src/app.ts +8 -3
  7. package/src/client/App.tsx +11 -0
  8. package/src/client/components/Metric.tsx +0 -32
  9. package/src/client/components/ProfileMenu.tsx +56 -4
  10. package/src/client/components/layout/DashboardChrome.tsx +3 -3
  11. package/src/client/components/layout/DashboardHeader.tsx +41 -4
  12. package/src/client/components/layout/NavRail.tsx +43 -40
  13. package/src/client/conversations/ConversationComposer.tsx +42 -26
  14. package/src/client/conversations/ConversationDetails.tsx +149 -166
  15. package/src/client/conversations/ConversationHeader.tsx +126 -132
  16. package/src/client/conversations/ConversationHeaderActions.tsx +92 -120
  17. package/src/client/conversations/ConversationListStatusIcon.tsx +16 -11
  18. package/src/client/conversations/ConversationMemories.tsx +118 -78
  19. package/src/client/conversations/ConversationMeta.tsx +94 -84
  20. package/src/client/conversations/ConversationOverflowMenu.tsx +59 -0
  21. package/src/client/conversations/ConversationPage.tsx +197 -422
  22. package/src/client/conversations/ConversationReplyFooter.tsx +342 -0
  23. package/src/client/conversations/ConversationSidebar.tsx +5 -7
  24. package/src/client/conversations/ConversationTranscript.tsx +37 -8
  25. package/src/client/conversations/ConversationWatches.tsx +37 -15
  26. package/src/client/conversations/PendingMailboxStack.tsx +11 -1
  27. package/src/client/conversations/QuickQuestion.tsx +476 -0
  28. package/src/client/conversations/TelemetryMetrics.tsx +2 -15
  29. package/src/client/conversations/TranscriptActivityGroup.tsx +5 -0
  30. package/src/client/conversations/TranscriptEventView.tsx +25 -2
  31. package/src/client/conversations/TranscriptMemoryPopover.tsx +179 -0
  32. package/src/client/conversations/TranscriptMessageView.tsx +46 -16
  33. package/src/client/conversations/TranscriptRows.tsx +9 -2
  34. package/src/client/conversations/TranscriptView.tsx +15 -32
  35. package/src/client/conversations/UsageCard.tsx +4 -18
  36. package/src/client/conversations/conversationMemory.ts +138 -0
  37. package/src/client/conversations/conversationSections.ts +18 -14
  38. package/src/client/conversations/eventTranscript.ts +8 -0
  39. package/src/client/review-desk/ConversationReviewItems.tsx +99 -0
  40. package/src/client/review-desk/ReviewItemActions.tsx +1 -1
  41. package/src/client/review-desk/ReviewItemChangeRow.tsx +85 -0
  42. package/src/client/review-desk/ReviewItemContext.tsx +41 -23
  43. package/src/client/review-desk/reviewItemFormat.ts +16 -0
  44. package/src/client/sentry.ts +77 -0
  45. package/src/client/types.ts +6 -0
  46. package/src/client.tsx +3 -0
  47. package/src/mock-reporting/watches.ts +2 -0
  48. package/src/shell.ts +27 -2
  49. package/src/tailwind.css +12 -19
  50. package/src/client/components/ActiveIndicator.tsx +0 -14
  51. package/src/client/conversations/ConversationHeaderMeta.tsx +0 -20
  52. package/src/client/review-desk/ConversationReviewBar.tsx +0 -85
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-dashboard",
3
- "version": "0.274.0",
3
+ "version": "0.276.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.274.0"
43
+ "@sentry/junior-plugin-api": "0.276.0"
43
44
  },
44
45
  "peerDependencies": {
45
- "@sentry/junior": "0.274.0"
46
+ "@sentry/junior": "0.276.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.274.0"
67
+ "@sentry/junior": "0.276.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/app.ts CHANGED
@@ -11,7 +11,10 @@ import {
11
11
  type JuniorApiVariables,
12
12
  } from "@sentry/junior/api";
13
13
  import { apiErrorSchema } from "@sentry/junior/api/schema";
14
- import { initSentry } from "@sentry/junior/instrumentation";
14
+ import {
15
+ getBrowserSentryConfig,
16
+ initSentry,
17
+ } from "@sentry/junior/instrumentation";
15
18
  import { JUNIOR_VERSION } from "@sentry/junior/version";
16
19
  import { DASHBOARD_VERSION_HEADER } from "./dashboard-version";
17
20
  import { revalidateConversation } from "./conversation-cache";
@@ -527,10 +530,12 @@ export function createDashboardApp(
527
530
  }
528
531
  }
529
532
 
533
+ const renderPage = () =>
534
+ renderDashboard(basePath, agentName, getBrowserSentryConfig());
530
535
  for (const { nested, path } of dashboardPagePaths(basePath)) {
531
- app.get(path, () => renderDashboard(basePath, agentName));
536
+ app.get(path, renderPage);
532
537
  if (nested) {
533
- app.get(`${path}/*`, () => renderDashboard(basePath, agentName));
538
+ app.get(`${path}/*`, renderPage);
534
539
  }
535
540
  }
536
541
  for (const route of options.pluginRoutes ?? []) {
@@ -240,6 +240,17 @@ export function DashboardShell() {
240
240
  mobileNavigationOpen={mobileNavigationOpen}
241
241
  navItems={primaryNavItems}
242
242
  onMobileNavigationOpenChange={setMobileNavigationOpen}
243
+ profile={
244
+ loggedIn ? (
245
+ <ProfileMenu
246
+ identity={data!.me}
247
+ onSignOut={signOut}
248
+ spend={personalSpendQuery.data}
249
+ userPages={userPages}
250
+ variant="top-bar"
251
+ />
252
+ ) : undefined
253
+ }
243
254
  mobileIdentity={
244
255
  loggedIn ? (
245
256
  <ProfileMenu
@@ -12,11 +12,6 @@ export type MetricTooltipLine = {
12
12
  valueStyle?: "heading";
13
13
  };
14
14
 
15
- export type MetricListItem = {
16
- content: ReactNode;
17
- key: string;
18
- };
19
-
20
15
  function TooltipLines(props: { lines: MetricTooltipLine[] }) {
21
16
  return (
22
17
  <span className="grid min-w-0 self-start grid-cols-[minmax(0,1fr)_auto] content-start gap-x-3 gap-y-1.5">
@@ -126,30 +121,3 @@ export function MetricValue(props: {
126
121
  </Tooltip>
127
122
  );
128
123
  }
129
-
130
- /** Render inline metadata with consistent dot spacing across dashboard headers. */
131
- export function MetricList(props: {
132
- className?: string;
133
- items: MetricListItem[];
134
- }) {
135
- return (
136
- <div
137
- className={cn(
138
- "flex flex-wrap items-center gap-x-1.5 gap-y-1",
139
- props.className,
140
- )}
141
- >
142
- {props.items.map((item, index) => (
143
- <span
144
- className="inline-flex min-w-0 items-center gap-x-1.5"
145
- key={item.key}
146
- >
147
- {index > 0 ? (
148
- <span className="text-dashboard-text-muted">·</span>
149
- ) : null}
150
- <span className="min-w-0">{item.content}</span>
151
- </span>
152
- ))}
153
- </div>
154
- );
155
- }
@@ -1,4 +1,11 @@
1
- import { Boxes, LogOut, Settings, ShieldCheck, UserRound } from "lucide-react";
1
+ import {
2
+ Boxes,
3
+ ChevronDown,
4
+ LogOut,
5
+ Settings,
6
+ ShieldCheck,
7
+ UserRound,
8
+ } from "lucide-react";
2
9
  import { useEffect, useRef, useState } from "react";
3
10
  import { Link } from "react-router";
4
11
  import type { PersonalSpendReport } from "@sentry/junior/api/schema";
@@ -19,11 +26,17 @@ type ProfileMenuProps = {
19
26
  userPages: PluginUserPageLink[];
20
27
  /**
21
28
  * `popover` is the avatar control at the bottom of the desktop nav rail.
29
+ * `top-bar` is the spend and avatar control in the tall desktop top bar.
22
30
  * `sheet-spend` is the vertical spend callout at the top of the mobile sheet.
23
31
  * `sheet-links` is plain account destinations for the mobile nav sheet.
24
32
  * `sheet-identity` is the quiet signed-in strip at the bottom of that sheet.
25
33
  */
26
- variant?: "popover" | "sheet-spend" | "sheet-links" | "sheet-identity";
34
+ variant?:
35
+ | "popover"
36
+ | "top-bar"
37
+ | "sheet-spend"
38
+ | "sheet-links"
39
+ | "sheet-identity";
27
40
  };
28
41
 
29
42
  const HOVER_OPEN_DELAY_MS = 80;
@@ -307,6 +320,7 @@ export function ProfileMenu({
307
320
  );
308
321
  }
309
322
 
323
+ const topBar = variant === "top-bar";
310
324
  return (
311
325
  <Popover
312
326
  onOpenChange={(nextOpen, details) => {
@@ -324,18 +338,56 @@ export function ProfileMenu({
324
338
  <PopoverTrigger
325
339
  aria-label={`${open ? "Close" : "Open"} profile menu for ${name}. Personal model spend: 7 days ${sevenDaySpend}, 30 days ${thirtyDaySpend}.`}
326
340
  className={cn(
327
- "grid size-10 cursor-pointer place-items-center rounded-lg border-0 bg-transparent text-dashboard-text transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70",
341
+ "cursor-pointer rounded-lg border-0 bg-transparent text-dashboard-text transition-colors hover:bg-dashboard-fill-hover hover:text-dashboard-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus/70",
342
+ topBar
343
+ ? "group flex h-10 items-center gap-2 px-1.5"
344
+ : "grid size-10 place-items-center",
328
345
  open && "bg-dashboard-fill-hover text-dashboard-text",
329
346
  )}
330
347
  closeDelay={HOVER_CLOSE_DELAY_MS}
331
348
  delay={HOVER_OPEN_DELAY_MS}
332
349
  openOnHover
333
350
  >
351
+ {topBar ? (
352
+ <span className="flex items-center gap-2">
353
+ <span className="flex items-baseline gap-1 whitespace-nowrap tabular-nums">
354
+ <span className="text-xs font-medium tracking-[0.08em] text-dashboard-text-muted">
355
+ 7d
356
+ </span>
357
+ <span className="text-xs font-semibold text-dashboard-text">
358
+ <SpendAmount value={spend?.sevenDaysUsd} />
359
+ </span>
360
+ </span>
361
+ <span
362
+ aria-hidden="true"
363
+ className="h-3 w-px bg-dashboard-fill-strong"
364
+ />
365
+ <span className="flex items-baseline gap-1 whitespace-nowrap tabular-nums">
366
+ <span className="text-xs font-medium tracking-[0.08em] text-dashboard-text-muted">
367
+ 30d
368
+ </span>
369
+ <span className="text-xs font-semibold text-dashboard-text">
370
+ <SpendAmount value={spend?.thirtyDaysUsd} />
371
+ </span>
372
+ </span>
373
+ </span>
374
+ ) : null}
334
375
  <ProfileAvatar user={identity.user} />
376
+ {topBar ? (
377
+ <ChevronDown
378
+ aria-hidden="true"
379
+ className={cn(
380
+ "text-dashboard-text-muted transition-[color,transform] group-hover:text-dashboard-text",
381
+ open && "rotate-180 text-dashboard-text",
382
+ )}
383
+ size={14}
384
+ strokeWidth={2}
385
+ />
386
+ ) : null}
335
387
  </PopoverTrigger>
336
388
  <PopoverContent
337
389
  align="end"
338
- side="right"
390
+ side={topBar ? "bottom" : "right"}
339
391
  className="w-64 rounded-xl border-0 bg-dashboard-surface-raised/95 p-1.5 shadow-2xl shadow-black/75 backdrop-blur-xl"
340
392
  id="profile-popover"
341
393
  >
@@ -104,8 +104,8 @@ export function DashboardChromeProvider(props: { children: ReactNode }) {
104
104
  /**
105
105
  * Sticky shell chrome: mobile header, secondary navigation slot, and banner.
106
106
  * One bottom border closes the header and page links, so page link
107
- * underlines sit on it. Banners render below that border. On desktop the
108
- * header is hidden, so the border shows only under page links.
107
+ * underlines sit on it. Banners render below that border. Short desktop
108
+ * windows hide the header, so the border shows only under page links.
109
109
  */
110
110
  export function DashboardChrome(props: {
111
111
  banner?: ReactNode;
@@ -118,7 +118,7 @@ export function DashboardChrome(props: {
118
118
 
119
119
  return (
120
120
  <div className="sticky top-0 z-30 bg-dashboard-bg/95 pt-[env(safe-area-inset-top)]">
121
- <div className="border-b border-dashboard-border-strong md:border-b-0 md:has-[[data-secondary-slot]>*]:border-b">
121
+ <div className="border-b border-dashboard-border-strong md:border-b-0 md:has-[[data-secondary-slot]>*]:border-b md:tall:border-b">
122
122
  {props.header}
123
123
  <div
124
124
  className="hidden md:block"
@@ -3,6 +3,7 @@ import { useEffect, useRef, type ReactNode, type RefObject } from "react";
3
3
  import { Link, NavLink } from "react-router";
4
4
 
5
5
  import { getDashboardAgentName } from "../../agentName";
6
+ import { JuniorLogo } from "../JuniorLogo";
6
7
  import { Sheet, SheetClose, SheetContent } from "../ui/sheet";
7
8
  import { useSheet } from "../ui/useSheet";
8
9
  import {
@@ -31,8 +32,8 @@ function NavCount(props: { count?: number }) {
31
32
  }
32
33
 
33
34
  /**
34
- * Render the mobile shell header and navigation sheet. Desktop navigation
35
- * lives in `NavRail`.
35
+ * Render the shell header: the mobile bar and navigation sheet, and the
36
+ * desktop top bar on tall windows. Short desktop windows use `NavRail`.
36
37
  */
37
38
  export function DashboardHeader(props: {
38
39
  /** Back target used on mobile conversation detail. */
@@ -47,6 +48,8 @@ export function DashboardHeader(props: {
47
48
  mobileSpend?: ReactNode;
48
49
  navItems: DashboardHeaderNavItem[];
49
50
  onMobileNavigationOpenChange(open: boolean): void;
51
+ /** Spend and account control at the right of the tall desktop top bar. */
52
+ profile?: ReactNode;
50
53
  version?: string;
51
54
  workspaceActive: boolean;
52
55
  }) {
@@ -63,6 +66,13 @@ export function DashboardHeader(props: {
63
66
  onOpenChangeRef.current(true);
64
67
  });
65
68
 
69
+ const navLinkClass = ({ isActive }: { isActive: boolean }) =>
70
+ cn(
71
+ "shrink-0 whitespace-nowrap rounded-lg px-3 py-2 font-sans text-sm font-medium tracking-normal no-underline transition-colors",
72
+ isActive
73
+ ? "bg-dashboard-accent/[0.1] text-dashboard-accent-text"
74
+ : cn("hover:bg-dashboard-fill-soft", dashboardInteractiveTextClass),
75
+ );
66
76
  const sheetLinkClass = ({ isActive }: { isActive: boolean }) =>
67
77
  cn(
68
78
  "rounded-lg px-3 py-3 font-sans text-sm font-medium no-underline transition-colors",
@@ -85,8 +95,35 @@ export function DashboardHeader(props: {
85
95
  }, [props.mobileNavigationOpen]);
86
96
 
87
97
  return (
88
- <header className="relative bg-dashboard-bg md:hidden">
89
- <div className="grid w-full min-w-0 grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-x-2 px-3 py-2">
98
+ <header className="relative bg-dashboard-bg md:hidden md:tall:block">
99
+ <div className="hidden w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-5 px-7 py-3 md:tall:grid">
100
+ <Link
101
+ aria-label={`${getDashboardAgentName()} home`}
102
+ className="flex items-center text-inherit no-underline"
103
+ to="/"
104
+ >
105
+ <JuniorLogo />
106
+ </Link>
107
+ <nav aria-label="Primary" className="flex min-w-0 items-center gap-1">
108
+ <Link
109
+ aria-current={props.workspaceActive ? "page" : undefined}
110
+ className={navLinkClass({ isActive: props.workspaceActive })}
111
+ to="/"
112
+ >
113
+ Conversations
114
+ </Link>
115
+ {props.navItems.map((item) => (
116
+ <NavLink className={navLinkClass} key={item.key} to={item.to}>
117
+ {item.label}
118
+ <NavCount count={item.count} />
119
+ </NavLink>
120
+ ))}
121
+ </nav>
122
+ {props.profile ? (
123
+ <div className="justify-self-end">{props.profile}</div>
124
+ ) : null}
125
+ </div>
126
+ <div className="grid w-full min-w-0 grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center gap-x-2 px-3 py-2 md:hidden">
90
127
  {conversationMode && props.mobileBackTo ? (
91
128
  <Link
92
129
  aria-label="Back to conversations"
@@ -82,7 +82,7 @@ function RailItem(props: {
82
82
  {props.count ? (
83
83
  <SidebarMenuBadge
84
84
  aria-hidden="true"
85
- className="-top-2 -right-1.5 h-4 min-w-4 rounded-full bg-dashboard-accent px-1 text-2xs font-semibold leading-none normal-nums text-dashboard-text-inverse ring-2 ring-sidebar group-data-[collapsible=icon]:flex peer-data-[size=default]/menu-button:-top-2 peer-data-active/menu-button:text-dashboard-text-inverse peer-hover/menu-button:text-dashboard-text-inverse"
85
+ className="top-0 right-0 h-4 min-w-4 rounded-full bg-dashboard-accent px-1 text-2xs font-semibold leading-none normal-nums text-dashboard-text-inverse ring-2 ring-sidebar group-data-[collapsible=icon]:flex peer-data-[size=default]/menu-button:top-0 peer-data-active/menu-button:text-dashboard-text-inverse peer-hover/menu-button:text-dashboard-text-inverse"
86
86
  >
87
87
  {props.count > 99 ? "99+" : props.count.toLocaleString("en-US")}
88
88
  </SidebarMenuBadge>
@@ -105,8 +105,8 @@ function RailNavItem(props: { item: DashboardHeaderNavItem }) {
105
105
  }
106
106
 
107
107
  /**
108
- * Desktop primary navigation as a shadcn icon sidebar. Mobile keeps the top
109
- * bar and navigation sheet in `DashboardHeader`.
108
+ * Desktop primary navigation as a shadcn icon sidebar on short windows. Tall
109
+ * desktop windows and mobile use `DashboardHeader`.
110
110
  */
111
111
  export function NavRail(props: {
112
112
  navItems: DashboardHeaderNavItem[];
@@ -114,43 +114,46 @@ export function NavRail(props: {
114
114
  workspaceActive: boolean;
115
115
  }) {
116
116
  return (
117
- <Sidebar
118
- className="border-dashboard-border-strong pt-[env(safe-area-inset-top)]"
119
- collapsible="icon"
120
- >
121
- <SidebarHeader className="items-center pt-3 pb-4">
122
- <Link
123
- aria-label={`${getDashboardAgentName()} home`}
124
- className="grid place-items-center text-inherit no-underline"
125
- to="/"
126
- >
127
- <JuniorLogo />
128
- </Link>
129
- </SidebarHeader>
130
- {/* Stock icon mode hides overflow; the rail stays collapsed, so let
117
+ // Tall windows use the top bar in `DashboardHeader` instead.
118
+ <div className="contents md:tall:hidden">
119
+ <Sidebar
120
+ className="border-dashboard-border-strong pt-[env(safe-area-inset-top)]"
121
+ collapsible="icon"
122
+ >
123
+ <SidebarHeader className="items-center pt-3 pb-4">
124
+ <Link
125
+ aria-label={`${getDashboardAgentName()} home`}
126
+ className="grid place-items-center text-inherit no-underline"
127
+ to="/"
128
+ >
129
+ <JuniorLogo />
130
+ </Link>
131
+ </SidebarHeader>
132
+ {/* Stock icon mode hides overflow; the rail stays collapsed, so let
131
133
  plugin pages scroll on short windows. */}
132
- <SidebarContent className="group-data-[collapsible=icon]:overflow-auto">
133
- <SidebarGroup className="items-center py-0">
134
- <nav aria-label="Primary" className="w-full">
135
- <SidebarMenu className="items-center gap-1.5">
136
- <RailItem
137
- active={props.workspaceActive}
138
- icon={MessagesSquare}
139
- label="Conversations"
140
- to="/"
141
- />
142
- {props.navItems.map((item) => (
143
- <RailNavItem item={item} key={item.key} />
144
- ))}
145
- </SidebarMenu>
146
- </nav>
147
- </SidebarGroup>
148
- </SidebarContent>
149
- {props.profile ? (
150
- <SidebarFooter className="items-center pb-3">
151
- {props.profile}
152
- </SidebarFooter>
153
- ) : null}
154
- </Sidebar>
134
+ <SidebarContent className="group-data-[collapsible=icon]:overflow-auto">
135
+ <SidebarGroup className="items-center py-0">
136
+ <nav aria-label="Primary" className="w-full">
137
+ <SidebarMenu className="items-center gap-1.5">
138
+ <RailItem
139
+ active={props.workspaceActive}
140
+ icon={MessagesSquare}
141
+ label="Conversations"
142
+ to="/"
143
+ />
144
+ {props.navItems.map((item) => (
145
+ <RailNavItem item={item} key={item.key} />
146
+ ))}
147
+ </SidebarMenu>
148
+ </nav>
149
+ </SidebarGroup>
150
+ </SidebarContent>
151
+ {props.profile ? (
152
+ <SidebarFooter className="items-center pb-3">
153
+ {props.profile}
154
+ </SidebarFooter>
155
+ ) : null}
156
+ </Sidebar>
157
+ </div>
155
158
  );
156
159
  }
@@ -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
- <Tooltip>
528
- <TooltipTrigger
529
- render={
530
- <Button
531
- focusableWhenDisabled
532
- aria-label="Attach images"
533
- disabled={sendLocked || readingImages}
534
- onClick={() => fileInputRef.current?.click()}
535
- size="icon"
536
- type="button"
537
- >
538
- <ImagePlus aria-hidden="true" size={16} />
539
- </Button>
540
- }
541
- />
542
- <TooltipContent size="compact">
543
- Attach images (up to 3 MB total)
544
- </TooltipContent>
545
- </Tooltip>
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="disabled:!border-transparent disabled:!bg-dashboard-fill-hover disabled:!text-dashboard-text-muted disabled:!opacity-100"
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
- <span className="hidden md:inline">
580
- {sendLocked ? "Sending…" : props.submitLabel}
581
- </span>
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>