@sentry/junior-dashboard 0.294.0 → 0.295.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 (93) hide show
  1. package/dist/app.js +7 -5
  2. package/dist/client-manifest.json +1 -1
  3. package/dist/index.js +7 -5
  4. package/dist/public/chunks/{AdminPage-mn71yp2v.js → AdminPage-lctmhvpg.js} +1 -1
  5. package/dist/public/chunks/{AnchorHeading-cp3vx8qa.js → AnchorHeading-ixbupfij.js} +1 -1
  6. package/dist/public/chunks/{Area-h08sxa33.js → Area-d95rfkv5.js} +1 -1
  7. package/dist/public/chunks/AutomationPage-m78li1hv.js +3 -0
  8. package/dist/public/chunks/{AutomationRunDetails-jjdhbqtn.js → AutomationRunDetails-dx5lutte.js} +1 -1
  9. package/dist/public/chunks/{AutomationRunsPage-j50ps7nt.js → AutomationRunsPage-cba6nbxd.js} +1 -1
  10. package/dist/public/chunks/{AutomationStatus-czfg6ayl.js → AutomationStatus-eqq5lrad.js} +1 -1
  11. package/dist/public/chunks/AutomationsPage-cp7wtt2v.js +8 -0
  12. package/dist/public/chunks/{BarChart-e3oi6in1.js → BarChart-lbkezxbi.js} +1 -1
  13. package/dist/public/chunks/{BoardPage-f0uxqg7u.js → BoardPage-m4zqf62q.js} +1 -1
  14. package/dist/public/chunks/{CodeChangeRow-dk3gkjut.js → CodeChangeRow-d620nk75.js} +2 -2
  15. package/dist/public/chunks/CodePage-h2ayars2.js +1 -0
  16. package/dist/public/chunks/{CodeSummaryCards-o1po2jis.js → CodeSummaryCards-iotfblbp.js} +1 -1
  17. package/dist/public/chunks/{ConnectionsPage-mholegdk.js → ConnectionsPage-nm7w1i05.js} +2 -2
  18. package/dist/public/chunks/{DirectoryMetric-dgzufji4.js → DirectoryMetric-huy2ptzz.js} +1 -1
  19. package/dist/public/chunks/{DrawerContent-ovjgr16m.js → DrawerContent-n3biwp5m.js} +1 -1
  20. package/dist/public/chunks/{FilterBar-mtnh7fbi.js → FilterBar-dqnt967g.js} +1 -1
  21. package/dist/public/chunks/FormNotice-kj5pzsl7.js +1 -0
  22. package/dist/public/chunks/{LocationDetailPage-ggd6zgzp.js → LocationDetailPage-iqtx76w6.js} +2 -2
  23. package/dist/public/chunks/{LocationsPage-fh6vzxa1.js → LocationsPage-ju5jfejt.js} +2 -2
  24. package/dist/public/chunks/{McpSettingsPage-fm3fh2ux.js → McpSettingsPage-brav5c42.js} +1 -1
  25. package/dist/public/chunks/{PeoplePage-odrbcmhj.js → PeoplePage-lfq8nrto.js} +2 -2
  26. package/dist/public/chunks/{PersonProfilePage-gynexe89.js → PersonProfilePage-ep45sxr6.js} +1 -1
  27. package/dist/public/chunks/{PersonalTokensPage-4o4zj02w.js → PersonalTokensPage-jbt4q7uz.js} +1 -1
  28. package/dist/public/chunks/PluginReportRecordSet-ml18z19r.js +1 -0
  29. package/dist/public/chunks/{PluginUserPage-kwylbbpr.js → PluginUserPage-ilar25ic.js} +2 -2
  30. package/dist/public/chunks/RepositoryPage-o9m3vty4.js +1 -0
  31. package/dist/public/chunks/SettingsPage-jc9et1qq.js +1 -0
  32. package/dist/public/chunks/SystemPage-bbp8cdds.js +15 -0
  33. package/dist/public/chunks/{WorkspaceFormPage-hwvyvnuk.js → WorkspaceFormPage-kshmsdek.js} +2 -2
  34. package/dist/public/chunks/{WorkspacesPage-lu9w3ugz.js → WorkspacesPage-ohasc10u.js} +1 -1
  35. package/dist/public/chunks/alert-ifqo1uj7.js +1 -0
  36. package/dist/public/chunks/{automationFormat-hd7pnocr.js → automationFormat-kv314fzh.js} +1 -1
  37. package/dist/public/chunks/{chart-myr1oeen.js → chart-il2ahs5y.js} +1 -1
  38. package/dist/public/chunks/{circle-dollar-sign-fpl4dmok.js → circle-dollar-sign-h7k4kr7s.js} +1 -1
  39. package/dist/public/chunks/{code-highlighter-i3ux67or.js → code-highlighter-ea4j0ys1.js} +1 -1
  40. package/dist/public/chunks/{coins-bqkaxeba.js → coins-l2c8c17w.js} +1 -1
  41. package/dist/public/chunks/{format-gle1s0ri.js → format-flkutk9e.js} +2 -2
  42. package/dist/public/chunks/junior-g62bxb2t.js +4 -0
  43. package/dist/public/chunks/{native-select-jfknh8t4.js → native-select-fxs3p0bj.js} +1 -1
  44. package/dist/public/chunks/{plus-gndielka.js → plus-54e3zzys.js} +1 -1
  45. package/dist/public/chunks/vendor-icons-blmhaswc.js +698 -0
  46. package/dist/public/chunks/{workspaceQueries-m1ksz95c.js → workspaceQueries-onxawhg4.js} +1 -1
  47. package/dist/public/client-ctcthgam.js +165 -0
  48. package/dist/public/styles-4CIT7L7K.css +2 -0
  49. package/dist/tailwind.css +1 -1
  50. package/package.json +4 -4
  51. package/src/client/api.ts +16 -0
  52. package/src/client/components/MarkdownViewToggle.tsx +24 -0
  53. package/src/client/components/SkillLink.tsx +119 -0
  54. package/src/client/conversations/ConversationHomeList.tsx +87 -76
  55. package/src/client/conversations/ConversationListStatusIcon.tsx +39 -55
  56. package/src/client/conversations/ConversationSectionList.tsx +101 -0
  57. package/src/client/conversations/ConversationSidebar.tsx +159 -169
  58. package/src/client/conversations/ConversationWorkspace.tsx +5 -16
  59. package/src/client/conversations/TextAttachmentPreview.tsx +5 -14
  60. package/src/client/conversations/WaitingChip.tsx +78 -0
  61. package/src/client/conversations/conversationSections.ts +78 -55
  62. package/src/client/conversations/heldListOrder.ts +170 -0
  63. package/src/client/conversations/toolCallPreview.ts +3 -1
  64. package/src/client/conversations/transcriptSource.ts +7 -2
  65. package/src/client/conversations/useMarkConversationUnread.ts +2 -2
  66. package/src/client/format.ts +18 -2
  67. package/src/client/pages/automations/AutomationDraftChanges.tsx +2 -1
  68. package/src/client/pages/automations/AutomationOverview.tsx +3 -3
  69. package/src/client/pages/automations/AutomationSkillPicker.tsx +75 -31
  70. package/src/client/pages/automations/AutomationTaskFields.tsx +3 -13
  71. package/src/client/pages/automations/AutomationVersionsTab.tsx +2 -5
  72. package/src/client/pages/automations/skillSearch.ts +50 -0
  73. package/src/client/pages/code/RepositorySections.tsx +14 -5
  74. package/src/client/pages/code/RepositoryTabs.tsx +5 -11
  75. package/src/client/pages/system/PluginDetails.tsx +15 -8
  76. package/src/client/pages/system/SkillInventory.tsx +2 -6
  77. package/src/client/review-desk/queries.ts +30 -23
  78. package/src/client/review-desk/reviewItemFormat.ts +3 -1
  79. package/src/client/types.ts +5 -1
  80. package/src/mock-reporting/review-items.ts +6 -2
  81. package/dist/public/chunks/AutomationPage-e1l74ezg.js +0 -10
  82. package/dist/public/chunks/AutomationsPage-cphxl4l6.js +0 -8
  83. package/dist/public/chunks/CodePage-i97ivyji.js +0 -1
  84. package/dist/public/chunks/FormNotice-eyo2kdte.js +0 -1
  85. package/dist/public/chunks/PluginReportRecordSet-hm1cwp6c.js +0 -1
  86. package/dist/public/chunks/RepositoryPage-fti2vfu4.js +0 -1
  87. package/dist/public/chunks/SettingsPage-kxspx37h.js +0 -1
  88. package/dist/public/chunks/SystemPage-jurblfgy.js +0 -15
  89. package/dist/public/chunks/SystemPlugins-jr37f1cv.js +0 -1
  90. package/dist/public/chunks/junior-hdb4wc4q.js +0 -4
  91. package/dist/public/chunks/vendor-icons-mxz7yi04.js +0 -692
  92. package/dist/public/client-f7asutg5.js +0 -165
  93. package/dist/public/styles-ZJJGBKKX.css +0 -2
@@ -1,10 +1,7 @@
1
- import { memo, useCallback, useMemo, useState } from "react";
1
+ import { memo, useCallback, useMemo, useState, type ReactNode } from "react";
2
2
  import {
3
3
  Archive,
4
4
  ArchiveRestore,
5
- ChevronRight,
6
- Inbox,
7
- LockKeyhole,
8
5
  Pin,
9
6
  SquarePen,
10
7
  } from "lucide-react";
@@ -22,7 +19,6 @@ import { useConversationSubscription } from "./useConversationSubscription";
22
19
  import { ContinueInSlackDialog } from "./ContinueInSlackDialog";
23
20
  import { ConversationRowMenu } from "./ConversationRowMenu";
24
21
  import {
25
- PinnedConversationOrder,
26
22
  usePinnedRowDrag,
27
23
  type PinnedRowDrag,
28
24
  } from "./PinnedConversationOrder";
@@ -37,11 +33,10 @@ import { formatCompactAge } from "./conversationAge";
37
33
  import { cn } from "../styles";
38
34
  import type { Conversation } from "../types";
39
35
  import { AnimatedList } from "./AnimatedList";
40
- import {
41
- attentionConversationIds,
42
- buildConversationSections,
43
- type ConversationSection,
44
- } from "./conversationSections";
36
+ import { WaitingChip } from "./WaitingChip";
37
+ import type { ConversationSection } from "./conversationSections";
38
+ import { ConversationSectionList } from "./ConversationSectionList";
39
+ import { ConversationStatusText } from "./ConversationListStatusIcon";
45
40
  import { ConversationSectionHeading } from "./ConversationSectionHeading";
46
41
  import {
47
42
  ConversationSidebarThreadRow,
@@ -145,40 +140,6 @@ export function ConversationSidebar(props: {
145
140
  return next;
146
141
  });
147
142
  }, []);
148
- // Rebuild section rows only when their inputs change. Avoid fresh Date.now()
149
- // arrays on unrelated parent renders while the reader is scrolling.
150
- const entries = useMemo(
151
- () =>
152
- conversationSidebarEntries(
153
- buildConversationSections(props.conversations, {
154
- attentionIds: attentionConversationIds(
155
- props.conversations,
156
- props.waitingConversations.keys(),
157
- ),
158
- nowMs: Date.now(),
159
- timeZone: props.timeZone,
160
- }),
161
- collapsedKeys,
162
- { expandedThreadRoots, selectedId: props.selectedId },
163
- ),
164
- [
165
- collapsedKeys,
166
- expandedThreadRoots,
167
- props.selectedId,
168
- props.conversations,
169
- props.timeZone,
170
- props.waitingConversations,
171
- ],
172
- );
173
- const pinnedConversations = useMemo(
174
- () =>
175
- entries.flatMap((entry) =>
176
- entry.kind === "conversation" && entry.conversation.pinnedAt
177
- ? [entry.conversation]
178
- : [],
179
- ),
180
- [entries],
181
- );
182
143
  const handleReorderError = useCallback(
183
144
  (conversation: Conversation) =>
184
145
  showActionError(conversation, "Could not reorder"),
@@ -203,82 +164,123 @@ export function ConversationSidebar(props: {
203
164
  New conversation
204
165
  </Button>
205
166
  </div>
206
- <ScrollArea viewportClassName="px-3 pb-[max(0.5rem,env(safe-area-inset-bottom))]">
207
- {props.error ? (
208
- <div className="p-2">
209
- <EmptyTelemetry>{props.error}</EmptyTelemetry>
210
- </div>
211
- ) : props.loading && entries.length === 0 ? (
212
- <ConversationRowsLoading />
213
- ) : (
214
- <PinnedConversationOrder
215
- conversations={pinnedConversations}
216
- onError={handleReorderError}
217
- reorder={props.onReorderPins}
167
+ {/* The list wraps the always-shown scroll area, so a pointer that
168
+ rests on it through the first load, or on its padding or
169
+ scrollbar, still holds the order. */}
170
+ <ConversationSectionList
171
+ className="relative min-h-0"
172
+ conversations={props.conversations}
173
+ loading={props.loading}
174
+ onReorderError={handleReorderError}
175
+ onReorderPins={props.onReorderPins}
176
+ timeZone={props.timeZone}
177
+ waitingConversations={props.waitingConversations}
178
+ >
179
+ {({ releaseHold, sections }) => (
180
+ <ScrollArea
181
+ className="h-full"
182
+ viewportClassName="px-3 pb-[max(0.5rem,env(safe-area-inset-bottom))]"
218
183
  >
219
- <AnimatedList
220
- ariaLabel="Your conversations"
221
- className="grid gap-0.5"
222
- empty={
223
- !props.loading ? (
224
- <div className="p-2">
225
- <EmptyTelemetry>
226
- No conversations match this view.
227
- </EmptyTelemetry>
228
- </div>
229
- ) : undefined
230
- }
231
- getKey={conversationEntryKey}
232
- items={entries}
233
- renderItem={(entry) =>
234
- entry.kind === "section" ? (
235
- <ConversationSectionHeading
236
- className={cn("px-3 pb-1", entry.first ? "pt-1" : "pt-3")}
237
- collapsed={entry.collapsed}
238
- count={entry.count}
239
- label={entry.label}
240
- onToggle={() => toggleSection(entry.sectionKey)}
241
- />
242
- ) : entry.kind === "thread-toggle" ? (
243
- <ConversationSidebarThreadToggle
244
- expanded={entry.expanded}
245
- hiddenCount={entry.hiddenCount}
246
- onToggle={() => toggleThreads(entry.rootId)}
247
- />
248
- ) : entry.kind === "thread" ? (
249
- <ConversationSidebarThreadRow
250
- conversation={entry.conversation}
251
- depth={entry.depth}
252
- selected={entry.conversation.id === props.selectedId}
253
- waiting={props.waitingConversations.has(
254
- entry.conversation.id,
255
- )}
256
- />
257
- ) : (
258
- <ConversationSidebarEntryRow
259
- conversation={entry.conversation}
260
- onArchiveError={showArchiveError}
261
- onArchived={showArchived}
262
- onPinError={handlePinError}
263
- onPinned={clearActionError}
264
- onUnreadError={handleUnreadError}
265
- onActionError={showActionError}
266
- selected={entry.conversation.id === props.selectedId}
267
- waiting={props.waitingConversations.get(
268
- entry.conversation.id,
269
- )}
270
- />
271
- )
272
- }
273
- role="navigation"
274
- />
275
- </PinnedConversationOrder>
184
+ {props.error ? (
185
+ <div className="p-2">
186
+ <EmptyTelemetry>{props.error}</EmptyTelemetry>
187
+ </div>
188
+ ) : props.loading && props.conversations.length === 0 ? (
189
+ <ConversationRowsLoading />
190
+ ) : (
191
+ <ConversationSidebarEntries
192
+ collapsedKeys={collapsedKeys}
193
+ expandedThreadRoots={expandedThreadRoots}
194
+ loading={props.loading}
195
+ sections={sections}
196
+ selectedId={props.selectedId}
197
+ renderItem={(entry) =>
198
+ entry.kind === "section" ? (
199
+ <ConversationSectionHeading
200
+ className={cn("px-3 pb-1", entry.first ? "pt-1" : "pt-3")}
201
+ collapsed={entry.collapsed}
202
+ count={entry.count}
203
+ label={entry.label}
204
+ onToggle={() => toggleSection(entry.sectionKey)}
205
+ />
206
+ ) : entry.kind === "thread-toggle" ? (
207
+ <ConversationSidebarThreadToggle
208
+ expanded={entry.expanded}
209
+ hiddenCount={entry.hiddenCount}
210
+ onToggle={() => toggleThreads(entry.rootId)}
211
+ />
212
+ ) : entry.kind === "thread" ? (
213
+ <ConversationSidebarThreadRow
214
+ conversation={entry.conversation}
215
+ depth={entry.depth}
216
+ selected={entry.conversation.id === props.selectedId}
217
+ waiting={props.waitingConversations.has(
218
+ entry.conversation.id,
219
+ )}
220
+ />
221
+ ) : (
222
+ <ConversationSidebarEntryRow
223
+ conversation={entry.conversation}
224
+ onArchiveError={showArchiveError}
225
+ onArchived={showArchived}
226
+ onPinError={handlePinError}
227
+ onPinned={clearActionError}
228
+ onUnreadError={handleUnreadError}
229
+ onActionError={showActionError}
230
+ onRowAction={releaseHold}
231
+ selected={entry.conversation.id === props.selectedId}
232
+ waiting={props.waitingConversations.get(
233
+ entry.conversation.id,
234
+ )}
235
+ />
236
+ )
237
+ }
238
+ />
239
+ )}
240
+ </ScrollArea>
276
241
  )}
277
- </ScrollArea>
242
+ </ConversationSectionList>
278
243
  </aside>
279
244
  );
280
245
  }
281
246
 
247
+ /** Show the sidebar rows of the held sections, with their motion. */
248
+ function ConversationSidebarEntries(props: {
249
+ collapsedKeys: ReadonlySet<string>;
250
+ expandedThreadRoots: ReadonlySet<string>;
251
+ loading: boolean;
252
+ renderItem(entry: ConversationSidebarEntry): ReactNode;
253
+ sections: ConversationSection[];
254
+ selectedId?: string;
255
+ }) {
256
+ const { collapsedKeys, expandedThreadRoots, sections, selectedId } = props;
257
+ const entries = useMemo(
258
+ () =>
259
+ conversationSidebarEntries(sections, collapsedKeys, {
260
+ expandedThreadRoots,
261
+ selectedId,
262
+ }),
263
+ [collapsedKeys, expandedThreadRoots, sections, selectedId],
264
+ );
265
+ return (
266
+ <AnimatedList
267
+ ariaLabel="Your conversations"
268
+ className="grid gap-0.5"
269
+ empty={
270
+ !props.loading ? (
271
+ <div className="p-2">
272
+ <EmptyTelemetry>No conversations match this view.</EmptyTelemetry>
273
+ </div>
274
+ ) : undefined
275
+ }
276
+ getKey={conversationEntryKey}
277
+ items={entries}
278
+ renderItem={props.renderItem}
279
+ role="navigation"
280
+ />
281
+ );
282
+ }
283
+
282
284
  /** Match the usual conversation row density while the first feed loads. */
283
285
  function ConversationRowsLoading() {
284
286
  return (
@@ -379,6 +381,8 @@ type ConversationSidebarRowProps = {
379
381
  onPinned(conversation: Conversation): void;
380
382
  onUnreadError(conversation: Conversation, wasMarkingUnread: boolean): void;
381
383
  onActionError(conversation: Conversation, title: string): void;
384
+ /** Runs before a row action that can move rows, so its result shows. */
385
+ onRowAction(): void;
382
386
  selected: boolean;
383
387
  waiting?: WaitingConversation;
384
388
  };
@@ -454,12 +458,22 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(
454
458
  const participant = props.conversation.isParticipant;
455
459
  const canToggleRead =
456
460
  participant && !props.waiting && (unread || canMarkUnread);
457
- const togglePin = () => pin.mutate(!pinned);
458
- const toggleArchive = () =>
461
+ const setPinned = (next: boolean) => {
462
+ props.onRowAction();
463
+ pin.mutate(next);
464
+ };
465
+ const togglePin = () => setPinned(!pinned);
466
+ const setUnread = (next: boolean) => {
467
+ props.onRowAction();
468
+ markUnread.mutate(next);
469
+ };
470
+ const toggleArchive = () => {
471
+ props.onRowAction();
459
472
  archive.mutate({
460
473
  archived: !archived,
461
474
  lastSeenAt: props.conversation.lastSeenAt,
462
475
  });
476
+ };
463
477
  return (
464
478
  <ContextMenu
465
479
  // The menu shows rows from the detail, so read it as a hover does.
@@ -503,11 +517,6 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(
503
517
  className="size-1.5 shrink-0 rounded-full bg-dashboard-accent"
504
518
  role="img"
505
519
  />
506
- ) : props.conversation.visibility === "private" ? (
507
- <LockKeyhole
508
- aria-label="Private conversation"
509
- className="size-3 shrink-0 text-dashboard-text-faint"
510
- />
511
520
  ) : null}
512
521
  </span>
513
522
  <span
@@ -524,7 +533,7 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(
524
533
  {title}
525
534
  </span>
526
535
  <ConversationRowState
527
- lastSeenAt={props.conversation.lastSeenAt}
536
+ conversation={props.conversation}
528
537
  status={status}
529
538
  />
530
539
  </Link>
@@ -536,7 +545,7 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(
536
545
  aria-label={`Mark ${title} as ${unread ? "read" : "unread"}`}
537
546
  className="group/read absolute top-1 left-[0.4375rem] z-10 grid size-6 cursor-pointer place-items-center rounded-md transition hover:bg-dashboard-surface-active focus-visible:bg-dashboard-surface-active focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus disabled:cursor-not-allowed"
538
547
  disabled={markUnread.isPending}
539
- onClick={() => markUnread.mutate(!unread)}
548
+ onClick={() => setUnread(!unread)}
540
549
  type="button"
541
550
  >
542
551
  {/* On hover, preview the state that a click sets. */}
@@ -558,28 +567,10 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(
558
567
  </Tooltip>
559
568
  ) : null}
560
569
  {props.waiting ? (
561
- <Link
562
- className="mb-1.5 ml-[2.125rem] mr-3 flex min-w-0 items-center gap-1.5 rounded-md border border-dashboard-border-strong bg-dashboard-surface-raised px-2 py-1 font-sans text-xs text-dashboard-text no-underline transition-colors hover:border-dashboard-border-interactive focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus"
563
- to={conversationPath(props.conversation.id, { reviewItems: true })}
564
- >
565
- <Inbox
566
- aria-hidden="true"
567
- className="size-3 shrink-0 text-dashboard-text-muted"
568
- />
569
- <span className="min-w-0 truncate">{props.waiting.title}</span>
570
- {props.waiting.count > 1 ? (
571
- <span className="ml-auto shrink-0 text-dashboard-text-faint">
572
- +{props.waiting.count - 1} more
573
- </span>
574
- ) : null}
575
- <ChevronRight
576
- aria-hidden="true"
577
- className={cn(
578
- "size-3 shrink-0 text-dashboard-text-muted",
579
- props.waiting.count > 1 ? undefined : "ml-auto",
580
- )}
581
- />
582
- </Link>
570
+ <WaitingChip
571
+ conversationId={props.conversation.id}
572
+ waiting={props.waiting}
573
+ />
583
574
  ) : null}
584
575
  {/* Mobile rows pin from the conversation menu, so only the archive
585
576
  button takes row space there. Archived rows restore before they pin. */}
@@ -649,8 +640,8 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(
649
640
  replace: props.selected,
650
641
  state: OPEN_EVENT_LOG_STATE,
651
642
  }),
652
- pin: (next) => pin.mutate(next),
653
- read: (next) => markUnread.mutate(next),
643
+ pin: setPinned,
644
+ read: setUnread,
654
645
  rename: () => setRenaming(true),
655
646
  subscription: (subscribed) => subscription.mutate(subscribed),
656
647
  }}
@@ -682,32 +673,31 @@ const ConversationSidebarRow = memo(function ConversationSidebarRow(
682
673
  );
683
674
  });
684
675
 
685
- /** Show the row age, or the status when the conversation runs, failed, or stopped. */
676
+ /**
677
+ * Show the row's secondary text: a lock when private, then the status when
678
+ * the conversation runs, failed, or stopped, or else the age of its last
679
+ * Message.
680
+ */
686
681
  function ConversationRowState(props: {
687
- lastSeenAt: string;
682
+ conversation: Conversation;
688
683
  status: ReturnType<typeof visualStatusForConversation>;
689
684
  }) {
690
- if (props.status === "active") {
691
- return (
692
- <Spinner
693
- aria-label="Running"
694
- className="size-3.5 text-dashboard-text-muted"
695
- />
696
- );
697
- }
698
- if (props.status === "failed") {
699
- return <span className="font-sans text-xs text-rose-400">Failed</span>;
700
- }
701
- if (props.status === "stopped") {
702
- return (
703
- <span className="font-sans text-xs text-dashboard-text-muted">
704
- Stopped
705
- </span>
706
- );
707
- }
685
+ const isPrivate = props.conversation.visibility === "private";
708
686
  return (
709
- <span className="font-sans text-xs tabular-nums text-dashboard-text-faint">
710
- {formatCompactAge(props.lastSeenAt)}
687
+ <span className="flex items-center gap-1">
688
+ <ConversationStatusText isPrivate={isPrivate} status={props.status} />
689
+ {props.status === "active" ? (
690
+ <Spinner
691
+ aria-label="Running"
692
+ className="size-3.5 text-dashboard-text-muted"
693
+ />
694
+ ) : props.status === "failed" || props.status === "stopped" ? null : (
695
+ <span className="font-sans text-xs tabular-nums text-dashboard-text-faint">
696
+ {formatCompactAge(
697
+ props.conversation.lastMessageAt ?? props.conversation.lastSeenAt,
698
+ )}
699
+ </span>
700
+ )}
711
701
  </span>
712
702
  );
713
703
  }
@@ -51,10 +51,7 @@ import {
51
51
  FOCUS_REPLY_STATE,
52
52
  NEW_CONVERSATION_PATH,
53
53
  } from "./conversationRoutes";
54
- import {
55
- attentionConversationIds,
56
- pinnedAndAttentionFirst,
57
- } from "./conversationSections";
54
+ import { pinnedAndWaitingFirst } from "./conversationSections";
58
55
  import { buildConversations, getDashboardTimeZone } from "../format";
59
56
  import { useDebouncedValue } from "../debouncedValue";
60
57
  import { usePushNotificationsTip } from "../notifications/pushNotificationsTip";
@@ -63,10 +60,7 @@ import type { Conversation } from "../types";
63
60
  import { ConversationPage } from "./ConversationPage";
64
61
  import { useAutoLoadHistoryPage } from "./transcriptBottomPinning";
65
62
  import { FeedThreads, feedThreadsById } from "./feedThreads";
66
- import {
67
- useWaitingConversationIds,
68
- useWaitingConversations,
69
- } from "../review-desk/queries";
63
+ import { useWaitingConversations } from "../review-desk/queries";
70
64
  import { UnsentAnswersProvider } from "../review-desk/UnsentAnswers";
71
65
  import { useReorderPinnedConversations } from "./useReorderPinnedConversations";
72
66
  import {
@@ -146,17 +140,12 @@ export function ConversationWorkspace(props: {
146
140
  [conversations],
147
141
  );
148
142
  const waitingConversations = useWaitingConversations();
149
- const waitingConversationIds = useWaitingConversationIds();
150
143
 
151
144
  // The feed holds every row. The home list shows them a batch at a time
152
145
  // and shows the next batch when the reader scrolls near its end.
153
146
  const orderedConversations = useMemo(
154
- () =>
155
- pinnedAndAttentionFirst(
156
- conversations,
157
- attentionConversationIds(conversations, waitingConversationIds),
158
- ),
159
- [conversations, waitingConversationIds],
147
+ () => pinnedAndWaitingFirst(conversations, waitingConversations),
148
+ [conversations, waitingConversations],
160
149
  );
161
150
  const visibleConversations = useMemo(
162
151
  () => orderedConversations.slice(0, visibleCount),
@@ -328,7 +317,7 @@ export function ConversationWorkspace(props: {
328
317
  : reorderPins
329
318
  }
330
319
  timeZone={getDashboardTimeZone()}
331
- waitingConversationIds={waitingConversationIds}
320
+ waitingConversations={waitingConversations}
332
321
  />
333
322
  {hiddenCount > 0 ? (
334
323
  <div
@@ -7,9 +7,12 @@ import { createPortal } from "react-dom";
7
7
  import { HighlightedCode, HighlightedCodeFallback } from "../code";
8
8
  import { Drawer } from "../components/Drawer";
9
9
  import { InlineError } from "../components/InlineError";
10
+ import {
11
+ MarkdownViewToggle,
12
+ type MarkdownView,
13
+ } from "../components/MarkdownViewToggle";
10
14
  import { buttonVariants } from "../components/ui/button";
11
15
  import { Skeleton } from "../components/ui/skeleton";
12
- import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
13
16
  import { DisplayText } from "../display-text/DisplayText";
14
17
  import { readDashboardBytes } from "../http";
15
18
  import {
@@ -18,8 +21,6 @@ import {
18
21
  type TextPreviewLanguage,
19
22
  } from "./textPreview";
20
23
 
21
- type MarkdownView = "formatted" | "raw";
22
-
23
24
  const SKELETON_LINE_WIDTHS = ["w-3/4", "w-full", "w-5/6", "w-2/3", "w-full"];
24
25
 
25
26
  /**
@@ -61,17 +62,7 @@ export function TextAttachmentPreview(props: {
61
62
  actions={
62
63
  <>
63
64
  {markdown ? (
64
- <ToggleGroup
65
- aria-label="Markdown view"
66
- onValueChange={(values) => {
67
- const next = values[0];
68
- if (next) setView(next);
69
- }}
70
- value={[view]}
71
- >
72
- <ToggleGroupItem value="formatted">Formatted</ToggleGroupItem>
73
- <ToggleGroupItem value="raw">Raw</ToggleGroupItem>
74
- </ToggleGroup>
65
+ <MarkdownViewToggle onChange={setView} value={view} />
75
66
  ) : null}
76
67
  <a
77
68
  aria-label={`Download ${filename}`}
@@ -0,0 +1,78 @@
1
+ import { ChevronRight, Inbox } from "lucide-react";
2
+ import { Link } from "react-router";
3
+ import {
4
+ Tooltip,
5
+ TooltipContent,
6
+ TooltipTrigger,
7
+ } from "../components/ui/tooltip";
8
+ import type { WaitingConversation } from "../review-desk/queries";
9
+ import { cn } from "../styles";
10
+ import { conversationPath } from "./conversationRoutes";
11
+ import { GitHubMark } from "./GitHubMark";
12
+ import { pluginSource } from "./transcriptSource";
13
+
14
+ /** Show the plugin's mark for a plugin item, and the Inbox icon otherwise. */
15
+ function WaitingItemMark(props: { plugin?: string }) {
16
+ return pluginSource(props.plugin) === "github" ? (
17
+ <GitHubMark className="size-3 shrink-0 text-dashboard-text-muted" />
18
+ ) : (
19
+ <Inbox
20
+ aria-hidden="true"
21
+ className="size-3 shrink-0 text-dashboard-text-muted"
22
+ />
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Link a waiting row to its open Review items. The chip shows the newest
28
+ * title, and its tooltip lists every title in full.
29
+ */
30
+ export function WaitingChip(props: {
31
+ conversationId: string;
32
+ waiting: WaitingConversation;
33
+ }) {
34
+ const [newest, ...older] = props.waiting.items;
35
+ if (!newest) return null;
36
+ return (
37
+ <Tooltip>
38
+ <TooltipTrigger
39
+ render={
40
+ <Link
41
+ className="mb-1.5 ml-[2.125rem] mr-3 flex min-w-0 items-center gap-1.5 rounded-md border border-dashboard-border-strong bg-dashboard-surface-raised px-2 py-1 font-sans text-xs text-dashboard-text no-underline transition-colors hover:border-dashboard-border-interactive focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dashboard-focus"
42
+ to={conversationPath(props.conversationId, { reviewItems: true })}
43
+ >
44
+ <WaitingItemMark plugin={newest.plugin} />
45
+ <span className="min-w-0 truncate">{newest.title}</span>
46
+ {older.length > 0 ? (
47
+ <span className="ml-auto shrink-0 text-dashboard-text-faint">
48
+ +{older.length} more
49
+ </span>
50
+ ) : null}
51
+ <ChevronRight
52
+ aria-hidden="true"
53
+ className={cn(
54
+ "size-3 shrink-0 text-dashboard-text-muted",
55
+ older.length > 0 ? undefined : "ml-auto",
56
+ )}
57
+ />
58
+ </Link>
59
+ }
60
+ />
61
+ <TooltipContent align="start" side="right">
62
+ <ul className="flex flex-col gap-1">
63
+ {props.waiting.items.map((item, index) => (
64
+ <li
65
+ className="flex items-start gap-1.5 text-dashboard-text"
66
+ key={index}
67
+ >
68
+ <span className="mt-0.5 flex">
69
+ <WaitingItemMark plugin={item.plugin} />
70
+ </span>
71
+ <span className="min-w-0 break-words">{item.title}</span>
72
+ </li>
73
+ ))}
74
+ </ul>
75
+ </TooltipContent>
76
+ </Tooltip>
77
+ );
78
+ }