@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,8 +1,8 @@
1
1
  import { useState } from "react";
2
- import { Link } from "react-router";
3
2
  import { Plus } from "lucide-react";
4
3
  import type { AutomationEdit, SkillReport } from "@sentry/junior/api/schema";
5
4
  import { useSkillsData } from "../../api";
5
+ import { SkillLink, skillLabel } from "../../components/SkillLink";
6
6
  import { Button } from "../../components/ui/button";
7
7
  import { Textarea } from "../../components/ui/textarea";
8
8
  import {
@@ -12,7 +12,6 @@ import {
12
12
  FieldLabel,
13
13
  } from "../../components/ui/field";
14
14
  import { ToggleGroup, ToggleGroupItem } from "../../components/ui/toggle-group";
15
- import { systemPluginPath } from "../system/SystemPlugins";
16
15
  import { AutomationSkillPicker } from "./AutomationSkillPicker";
17
16
  import { taskDraft, type AutomationTaskDraft } from "./automationDraft";
18
17
 
@@ -118,7 +117,7 @@ function SkillFields(props: {
118
117
  const skillError =
119
118
  props.fieldError("task.skill") ??
120
119
  (missing
121
- ? `The skill ${task.skill} is no longer installed. This automation is blocked until you choose another skill or switch to an instruction.`
120
+ ? `${skillLabel(task.skill)} is no longer installed. This automation is blocked until you choose another skill or switch to an instruction.`
122
121
  : undefined);
123
122
  return (
124
123
  <>
@@ -139,16 +138,7 @@ function SkillFields(props: {
139
138
  >
140
139
  Junior loads this skill at the start of each run.
141
140
  {selected ? (
142
- <Link
143
- to={
144
- selected.pluginProvider
145
- ? systemPluginPath(selected.pluginProvider)
146
- : "/system#skills"
147
- }
148
- className="text-dashboard-accent-text"
149
- >
150
- View skill
151
- </Link>
141
+ <SkillLink name={selected.name}>View skill</SkillLink>
152
142
  ) : null}
153
143
  </FieldDescription>
154
144
  </Field>
@@ -28,6 +28,7 @@ import { Button } from "../../components/ui/button";
28
28
  import { FormNotice } from "../../components/FormNotice";
29
29
  import { InlineError } from "../../components/InlineError";
30
30
  import { PageContentSkeleton } from "../../components/PageContentSkeleton";
31
+ import { SkillLink } from "../../components/SkillLink";
31
32
  import { StatusChip } from "../../components/StatusChip";
32
33
  import { Card } from "../../components/layout/Card";
33
34
  import { DisplayText } from "../../display-text/DisplayText";
@@ -367,11 +368,7 @@ function fieldValue(
367
368
  case "title":
368
369
  return definition.title ?? "First line of the instruction";
369
370
  case "skill":
370
- return task.skill ? (
371
- <span className="font-mono">{task.skill}</span>
372
- ) : (
373
- "No skill"
374
- );
371
+ return task.skill ? <SkillLink name={task.skill} /> : "No skill";
375
372
  case "instruction":
376
373
  if (!task.instruction) return "No context";
377
374
  return (
@@ -0,0 +1,50 @@
1
+ import type { SkillReport } from "@sentry/junior/api/schema";
2
+ import { quickOpenMatch } from "../../components/quickOpenMatch";
3
+
4
+ export type SkillSearchRow = {
5
+ skill: SkillReport;
6
+ /** UTF-16 indices in the name that the query matched. */
7
+ nameMatches?: number[];
8
+ /** UTF-16 indices in the description that the query matched. */
9
+ descriptionMatches?: number[];
10
+ };
11
+
12
+ /**
13
+ * Rank skills for the picker search. A name match ranks above any
14
+ * description match, so `self` puts `self-update` at the top. Descriptions
15
+ * match only on whole terms: a scatter across a long sentence matches
16
+ * almost any query.
17
+ */
18
+ export function searchSkills(
19
+ skills: readonly SkillReport[],
20
+ query: string,
21
+ ): SkillSearchRow[] {
22
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
23
+ const named: { row: SkillSearchRow; score: number }[] = [];
24
+ const described: { row: SkillSearchRow; score: number }[] = [];
25
+ for (const skill of skills) {
26
+ const name = quickOpenMatch(skill.name, query);
27
+ if (name) {
28
+ named.push({
29
+ row: { skill, nameMatches: name.indices },
30
+ score: name.score,
31
+ });
32
+ continue;
33
+ }
34
+ const description = skill.description.toLowerCase();
35
+ if (!terms.every((term) => description.includes(term))) continue;
36
+ const match = quickOpenMatch(skill.description, query);
37
+ if (match) {
38
+ described.push({
39
+ row: { skill, descriptionMatches: match.indices },
40
+ score: match.score,
41
+ });
42
+ }
43
+ }
44
+ // Sort is stable, so equal scores keep the skill list's order.
45
+ const byScore = (left: { score: number }, right: { score: number }) =>
46
+ right.score - left.score;
47
+ return [...named.sort(byScore), ...described.sort(byScore)].map(
48
+ (ranked) => ranked.row,
49
+ );
50
+ }
@@ -10,7 +10,10 @@ import { Card } from "../../components/layout/Card";
10
10
  import { SectionHeader } from "../../components/layout/SectionHeader";
11
11
  import { SectionTitle } from "../../components/layout/SectionTitle";
12
12
  import { ConversationSidebarAnnotations } from "../../conversations/ConversationMeta";
13
- import { ConversationListStatusIcon } from "../../conversations/ConversationListStatusIcon";
13
+ import {
14
+ ConversationListStatusIcon,
15
+ ConversationStatusText,
16
+ } from "../../conversations/ConversationListStatusIcon";
14
17
  import { conversationPath } from "../../conversations/conversationRoutes";
15
18
  import {
16
19
  conversationDisplayTitle,
@@ -53,6 +56,7 @@ export function RepositorySection(props: {
53
56
  */
54
57
  export function RecentConversationRow(props: { conversation: Conversation }) {
55
58
  const conversation = props.conversation;
59
+ const status = visualStatusForConversation(conversation);
56
60
  const title = conversationDisplayTitle(conversation);
57
61
  const location = slackLocationLabel(conversation, { includeId: false });
58
62
  const hasAnnotations = Boolean(conversation.sidebarAnnotations?.length);
@@ -67,16 +71,21 @@ export function RecentConversationRow(props: { conversation: Conversation }) {
67
71
  <span className="mt-1.5 grid size-3 shrink-0 place-items-center">
68
72
  <ConversationListStatusIcon
69
73
  unread={false}
70
- isPrivate={conversation.visibility === "private"}
71
- status={visualStatusForConversation(conversation)}
74
+ status={status}
72
75
  waitingOnViewer={false}
73
76
  />
74
77
  </span>
75
78
  <div className="min-w-0 truncate font-display text-sm text-dashboard-text">
76
79
  {title}
77
80
  </div>
78
- <span className="whitespace-nowrap font-mono text-xs text-dashboard-text-muted">
79
- {formatRelativeTime(conversation.lastSeenAt)}
81
+ <span className="flex items-center gap-1.5 whitespace-nowrap font-mono text-xs text-dashboard-text-muted">
82
+ <ConversationStatusText
83
+ isPrivate={conversation.visibility === "private"}
84
+ status={status}
85
+ />
86
+ {formatRelativeTime(
87
+ conversation.lastMessageAt ?? conversation.lastSeenAt,
88
+ )}
80
89
  </span>
81
90
  {/* Keep the meta line at text height so annotation chips do not make
82
91
  this row taller than a code change row beside it. */}
@@ -16,13 +16,10 @@ import {
16
16
  } from "../../components/controls/TimeRangeSelector";
17
17
  import { Card } from "../../components/layout/Card";
18
18
  import { ConversationHomeList } from "../../conversations/ConversationHomeList";
19
- import {
20
- attentionConversationIds,
21
- pinnedAndAttentionFirst,
22
- } from "../../conversations/conversationSections";
19
+ import { pinnedAndWaitingFirst } from "../../conversations/conversationSections";
23
20
  import { buildConversations, getDashboardTimeZone } from "../../format";
24
21
  import { useDebouncedValue } from "../../debouncedValue";
25
- import { useWaitingConversationIds } from "../../review-desk/queries";
22
+ import { useWaitingConversations } from "../../review-desk/queries";
26
23
  import { CodeActivityChart } from "./CodeActivityChart";
27
24
  import { CodeChangeRow } from "./CodeChangeRow";
28
25
  import { repositoryActivity } from "./RepositorySections";
@@ -36,7 +33,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
36
33
  const search = useDebouncedValue(query.trim());
37
34
  const feed = useCodeRepositoryConversationsData(props.repositoryId, search);
38
35
  const conversations = buildConversations(feed.data?.conversations ?? []);
39
- const waitingConversationIds = useWaitingConversationIds();
36
+ const waitingConversations = useWaitingConversations();
40
37
  useEffect(() => {
41
38
  setPage(1);
42
39
  }, [search]);
@@ -56,10 +53,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
56
53
  ) : (
57
54
  <ConversationHomeList
58
55
  conversations={pageItems(
59
- pinnedAndAttentionFirst(
60
- conversations,
61
- attentionConversationIds(conversations, waitingConversationIds),
62
- ),
56
+ pinnedAndWaitingFirst(conversations, waitingConversations),
63
57
  page,
64
58
  PAGE_SIZE,
65
59
  )}
@@ -70,7 +64,7 @@ export function RepositoryConversationsTab(props: { repositoryId: string }) {
70
64
  }
71
65
  loading={!feed.data}
72
66
  timeZone={getDashboardTimeZone()}
73
- waitingConversationIds={waitingConversationIds}
67
+ waitingConversations={waitingConversations}
74
68
  />
75
69
  )}
76
70
  <PagePagination
@@ -1,8 +1,10 @@
1
1
  import { Boxes, KeyRound, Sparkles } from "lucide-react";
2
+ import type { ReactNode } from "react";
2
3
 
3
4
  import { Card } from "../../components/layout/Card";
4
5
  import { anchorTitleId } from "../../components/layout/AnchorHeading";
5
6
  import { SectionIntro } from "../../components/layout/SectionIntro";
7
+ import { SkillLink } from "../../components/SkillLink";
6
8
  import type { SystemPlugin } from "./SystemPlugins";
7
9
 
8
10
  /** Render non-reporting plugin metadata. */
@@ -28,6 +30,7 @@ export function PluginDetails(props: { plugin: SystemPlugin }) {
28
30
  <CapabilityList
29
31
  emptyText="No plugin-provided skills were discovered."
30
32
  items={props.plugin.skills.map((skill) => skill.name)}
33
+ renderItem={(name) => <SkillLink chip key={name} name={name} />}
31
34
  title="Skills"
32
35
  />
33
36
  {props.plugin.configKeys.length ? (
@@ -69,6 +72,7 @@ function Detail(props: { icon: typeof Boxes; label: string; value: string }) {
69
72
  function CapabilityList(props: {
70
73
  emptyText: string;
71
74
  items: string[];
75
+ renderItem?(item: string): ReactNode;
72
76
  title: string;
73
77
  }) {
74
78
  return (
@@ -78,14 +82,17 @@ function CapabilityList(props: {
78
82
  </h3>
79
83
  {props.items.length ? (
80
84
  <div className="mt-2.5 flex flex-wrap gap-2">
81
- {props.items.map((item) => (
82
- <span
83
- className="rounded-md border border-dashboard-border bg-dashboard-overlay-soft px-2.5 py-1.5 font-mono text-xs text-dashboard-text-muted"
84
- key={item}
85
- >
86
- {item}
87
- </span>
88
- ))}
85
+ {props.items.map(
86
+ (item) =>
87
+ props.renderItem?.(item) ?? (
88
+ <span
89
+ className="rounded-md border border-dashboard-border bg-dashboard-overlay-soft px-2.5 py-1.5 font-mono text-xs text-dashboard-text-muted"
90
+ key={item}
91
+ >
92
+ {item}
93
+ </span>
94
+ ),
95
+ )}
89
96
  </div>
90
97
  ) : (
91
98
  <p className="mt-2 mb-0 font-sans text-xs leading-relaxed text-dashboard-text-muted">
@@ -4,6 +4,7 @@ import { getDashboardAgentName } from "../../agentName";
4
4
 
5
5
  import { Card } from "../../components/layout/Card";
6
6
  import { SectionIntro } from "../../components/layout/SectionIntro";
7
+ import { SkillLink } from "../../components/SkillLink";
7
8
  import { formatCompactNumber } from "../../format";
8
9
 
9
10
  type SkillGroup = { pluginProvider?: string; skills: SkillReport[] };
@@ -81,12 +82,7 @@ function SkillGroupCard(props: { group: SkillGroup }) {
81
82
  </div>
82
83
  <div className="flex flex-wrap gap-2">
83
84
  {props.group.skills.map((skill) => (
84
- <span
85
- className="rounded-md border border-dashboard-border bg-dashboard-overlay-soft px-2.5 py-1.5 font-mono text-xs text-dashboard-text-muted"
86
- key={skill.name}
87
- >
88
- {skill.name}
89
- </span>
85
+ <SkillLink chip key={skill.name} name={skill.name} />
90
86
  ))}
91
87
  </div>
92
88
  </article>
@@ -155,47 +155,54 @@ export function useReviewItemAction() {
155
155
  });
156
156
  }
157
157
 
158
- /** Newest open Review item title and item count for one waiting Conversation. */
159
- export type WaitingConversation = { count: number; title: string };
158
+ /**
159
+ * Open Review items, newest first, that one Conversation waits on the viewer
160
+ * for, and when the wait began (the oldest item).
161
+ */
162
+ export type WaitingConversation = {
163
+ items: Array<{ plugin?: string; title: string }>;
164
+ waitingSince: string;
165
+ };
160
166
 
161
167
  const NO_WAITING: ReadonlyMap<string, WaitingConversation> = new Map();
162
168
 
169
+ const waitingConversationsQuery = {
170
+ queryKey: [...REVIEW_ITEMS_QUERY_KEY, "waiting-conversations"],
171
+ queryFn: ({ signal }: { signal: AbortSignal }) =>
172
+ fetchDashboardJson(
173
+ reviewItemWaitingConversationsSchema,
174
+ "/api/review-items/waiting-conversations",
175
+ signal,
176
+ ),
177
+ retry: false,
178
+ };
179
+
180
+ /** Whether the first read of the waiting Conversations has finished. */
181
+ export function useWaitingConversationsSettled(): boolean {
182
+ return !useQuery(waitingConversationsQuery).isPending;
183
+ }
184
+
163
185
  /**
164
- * Return the Conversations that wait on the viewer, by Conversation id. Today
165
- * only the review desk sets this: a linked open action item assigned to the
166
- * viewer.
186
+ * Return the Conversations that wait on the viewer, by Conversation id: each
187
+ * has a linked open action item assigned to the viewer. An agent's action
188
+ * asks a question; a plugin's action asks for a step such as a merge.
167
189
  */
168
190
  export function useWaitingConversations(): ReadonlyMap<
169
191
  string,
170
192
  WaitingConversation
171
193
  > {
172
- const query = useQuery({
173
- queryKey: [...REVIEW_ITEMS_QUERY_KEY, "waiting-conversations"],
174
- queryFn: ({ signal }) =>
175
- fetchDashboardJson(
176
- reviewItemWaitingConversationsSchema,
177
- "/api/review-items/waiting-conversations",
178
- signal,
179
- ),
180
- retry: false,
181
- });
194
+ const query = useQuery(waitingConversationsQuery);
182
195
  const conversations = query.data?.conversations;
183
196
  return useMemo(
184
197
  () =>
185
198
  conversations
186
199
  ? new Map(
187
- conversations.map(({ conversationId, count, title }) => [
200
+ conversations.map(({ conversationId, items, waitingSince }) => [
188
201
  conversationId,
189
- { count, title },
202
+ { items, waitingSince },
190
203
  ]),
191
204
  )
192
205
  : NO_WAITING,
193
206
  [conversations],
194
207
  );
195
208
  }
196
-
197
- /** Return the ids of the Conversations that wait on the viewer. */
198
- export function useWaitingConversationIds(): ReadonlySet<string> {
199
- const waiting = useWaitingConversations();
200
- return useMemo(() => new Set(waiting.keys()), [waiting]);
201
- }
@@ -26,7 +26,9 @@ export function reviewItemSubjectHref(url: string): string | undefined {
26
26
  /** Name the state of an item: what it needs while open, or how it closed. */
27
27
  export function reviewItemStatusLabel(item: ReviewItemPayload): string {
28
28
  if (item.status === "open") {
29
- return item.kind === "action" ? "Needs an answer" : "Notice";
29
+ if (item.kind === "notice") return "Notice";
30
+ // A plugin ask waits on a step outside Junior, so it has no question.
31
+ return item.request ? "Needs an answer" : "Waiting on you";
30
32
  }
31
33
  return item.status === "done" ? "Done" : "Dismissed";
32
34
  }
@@ -11,6 +11,7 @@ import type {
11
11
  import type {
12
12
  ActorIdentity,
13
13
  ConversationReportMessageEvent,
14
+ ConversationFeedItem,
14
15
  ConversationStatsReport,
15
16
  ReplyTimeSegmentsReport,
16
17
  ConversationSummaryReport,
@@ -167,6 +168,8 @@ export type ConversationTranscript = ConversationDetailReport;
167
168
 
168
169
  export type Conversation = {
169
170
  activityPreview?: ConversationSummaryReport["activityPreview"];
171
+ /** The latest Brief's status, summary, and intent, when visible. */
172
+ brief?: ConversationFeedItem["brief"];
170
173
  annotations?: ConversationSummaryReport["annotations"];
171
174
  sidebarAnnotations?: ConversationSummaryReport["sidebarAnnotations"];
172
175
  archivedAt?: string | null;
@@ -185,6 +188,8 @@ export type Conversation = {
185
188
  id: string;
186
189
  /** The viewer takes part, so they can pin, archive, rename, and mark it. */
187
190
  isParticipant: boolean;
191
+ /** Newest true user Message or shown Junior reply. Feed rows only. */
192
+ lastMessageAt?: string;
188
193
  lastProgressAt: string;
189
194
  lastSeenAt: string;
190
195
  locationId?: string;
@@ -201,7 +206,6 @@ export type Conversation = {
201
206
  parentConversationId?: string;
202
207
  /** Visible Threads under this row, newest first. */
203
208
  threads?: Conversation[];
204
- unfinishedWork?: boolean;
205
209
  /** A Turn waits on a person to finish an authorization. Server-owned. */
206
210
  awaitingAuthorization?: boolean;
207
211
  visibility?: ConversationSummaryReport["visibility"];
@@ -267,8 +267,12 @@ export function readMockWaitingConversations(nowMs: number) {
267
267
  return {
268
268
  conversations: [...byConversation].map(([conversationId, items]) => ({
269
269
  conversationId,
270
- count: items.length,
271
- title: items.at(-1)!.title,
270
+ items: [...items].reverse().map(({ title }) => ({ title })),
271
+ waitingSince: items
272
+ .map((item) => item.createdAt)
273
+ .reduce((oldest, createdAt) =>
274
+ createdAt < oldest ? createdAt : oldest,
275
+ ),
272
276
  })),
273
277
  needsViewer: open.length,
274
278
  };