@sentry/junior-dashboard 0.269.0 → 0.271.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 (76) hide show
  1. package/dist/app.js +2 -2
  2. package/dist/client.js +78 -75
  3. package/dist/index.js +2 -2
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +5 -4
  6. package/src/client/App.tsx +103 -65
  7. package/src/client/components/AGENTS.md +30 -59
  8. package/src/client/components/AutomationCard.tsx +2 -1
  9. package/src/client/components/ProfileMenu.tsx +0 -24
  10. package/src/client/components/UserInputForm.tsx +143 -39
  11. package/src/client/components/layout/PageHeader.tsx +25 -10
  12. package/src/client/conversations/AGENTS.md +12 -37
  13. package/src/client/conversations/ConversationBrief.tsx +55 -36
  14. package/src/client/conversations/ConversationComposer.tsx +27 -8
  15. package/src/client/conversations/ConversationEventLog.tsx +1 -0
  16. package/src/client/conversations/ConversationHomeList.tsx +43 -26
  17. package/src/client/conversations/ConversationPage.tsx +137 -40
  18. package/src/client/conversations/ConversationSectionHeading.tsx +44 -0
  19. package/src/client/conversations/ConversationSidebar.tsx +55 -25
  20. package/src/client/conversations/ConversationTranscript.tsx +3 -2
  21. package/src/client/conversations/ConversationWatches.tsx +4 -1
  22. package/src/client/conversations/ConversationWorkspace.tsx +43 -25
  23. package/src/client/conversations/EventDetails.tsx +6 -9
  24. package/src/client/conversations/PendingMailboxStack.tsx +5 -2
  25. package/src/client/conversations/TranscriptContextEventView.tsx +3 -4
  26. package/src/client/conversations/TranscriptEventView.tsx +9 -3
  27. package/src/client/conversations/TranscriptMessageView.tsx +6 -5
  28. package/src/client/conversations/TranscriptReasoningView.tsx +4 -5
  29. package/src/client/conversations/TranscriptRows.tsx +6 -1
  30. package/src/client/conversations/TranscriptStructuredEventView.tsx +5 -4
  31. package/src/client/conversations/TranscriptToolView.tsx +2 -1
  32. package/src/client/conversations/TranscriptView.tsx +6 -6
  33. package/src/client/conversations/conversationOutbox.ts +13 -3
  34. package/src/client/conversations/conversationRoutes.ts +30 -3
  35. package/src/client/conversations/conversationSections.ts +36 -8
  36. package/src/client/conversations/eventTranscript.ts +1 -0
  37. package/src/client/conversations/transcriptBottomPinning.ts +171 -81
  38. package/src/client/conversations/transcriptScrollAnchor.ts +101 -0
  39. package/src/client/conversations/useCollapsedConversationSections.ts +48 -0
  40. package/src/client/display-text/DisplayText.tsx +68 -0
  41. package/src/client/display-text/emoji.ts +54 -0
  42. package/src/client/{conversations/transcriptMarkdownLinks.ts → display-text/links.ts} +16 -3
  43. package/src/client/{conversations/TranscriptMarkdown.tsx → display-text/markdown.tsx} +83 -19
  44. package/src/client/display-text/mentions.tsx +148 -0
  45. package/src/client/display-text/plainText.ts +29 -0
  46. package/src/client/pages/automations/AGENTS.md +6 -25
  47. package/src/client/pages/automations/AutomationFilters.tsx +6 -6
  48. package/src/client/pages/automations/AutomationOverview.tsx +2 -2
  49. package/src/client/pages/automations/AutomationVersionsTab.tsx +12 -8
  50. package/src/client/pages/code/RepositoryTabs.tsx +6 -1
  51. package/src/client/pages/memory/MemoryDetails.tsx +5 -13
  52. package/src/client/pages/settings/ConnectionsPage.tsx +198 -0
  53. package/src/client/pages/settings/McpSettingsPage.tsx +77 -0
  54. package/src/client/pages/settings/PersonalTokensPage.tsx +198 -0
  55. package/src/client/pages/{SettingsPage.tsx → settings/SettingsPage.tsx} +54 -62
  56. package/src/client/pages/settings/SettingsPageLayout.tsx +45 -0
  57. package/src/client/primaryNav.ts +1 -1
  58. package/src/client/review-desk/ConversationReviewBar.tsx +0 -1
  59. package/src/client/review-desk/ReviewDeskPage.tsx +202 -241
  60. package/src/client/review-desk/ReviewDeskSidebar.tsx +253 -0
  61. package/src/client/review-desk/ReviewItemActions.tsx +6 -2
  62. package/src/client/review-desk/ReviewItemCard.tsx +58 -17
  63. package/src/client/review-desk/ReviewItemContext.tsx +108 -0
  64. package/src/client/review-desk/ReviewItemDetails.tsx +153 -0
  65. package/src/client/review-desk/ReviewItemView.tsx +159 -0
  66. package/src/client/review-desk/UnsentAnswers.tsx +148 -0
  67. package/src/client/review-desk/queries.ts +4 -2
  68. package/src/client/review-desk/reviewItemFormat.ts +83 -1
  69. package/src/client/components/charts/AGENTS.md +0 -15
  70. package/src/client/conversations/TranscriptText.tsx +0 -47
  71. package/src/client/conversations/conversationActivityPreview.ts +0 -16
  72. package/src/client/conversations/transcriptMentions.tsx +0 -134
  73. package/src/client/pages/ConnectionsPage.tsx +0 -207
  74. package/src/client/pages/PersonalTokensPage.tsx +0 -205
  75. package/src/client/pages/system/AGENTS.md +0 -12
  76. package/src/client/review-desk/ReviewItemDetail.tsx +0 -123
@@ -1,207 +0,0 @@
1
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
- import { Plug } from "lucide-react";
3
- import type { z } from "zod";
4
- import {
5
- connectionListSchema,
6
- disconnectConnectionResponseSchema,
7
- startConnectionResponseSchema,
8
- type connectionSchema,
9
- } from "@sentry/junior/api/schema";
10
- import { del, fetchDashboardJson, post } from "../http";
11
- import { dashboardContainerClass } from "../styles";
12
- import { getDashboardAgentName } from "../agentName";
13
- import { useDocumentTitle } from "../documentTitle";
14
- import { Button } from "../components/ui/button";
15
- import { InlineError } from "../components/InlineError";
16
- import { Card } from "../components/layout/Card";
17
-
18
- type Connection = z.infer<typeof connectionSchema>;
19
-
20
- const connectionsQueryKey = ["dashboard", "connections"] as const;
21
-
22
- function connectionPath(provider: string): string {
23
- return `/api/connections/${encodeURIComponent(provider)}`;
24
- }
25
-
26
- /** Connect and disconnect the viewer's plugin accounts. */
27
- export function ConnectionsPage() {
28
- useDocumentTitle("Connected accounts");
29
- const queryClient = useQueryClient();
30
- const connectionsQuery = useQuery({
31
- queryKey: connectionsQueryKey,
32
- queryFn: ({ signal }) =>
33
- fetchDashboardJson(connectionListSchema, "/api/connections", signal),
34
- // OAuth finishes in another tab. Refresh when the viewer returns.
35
- refetchOnWindowFocus: true,
36
- retry: false,
37
- });
38
- const connectMutation = useMutation({
39
- mutationFn: (input: { provider: string; popup: Window | null }) =>
40
- post(startConnectionResponseSchema, connectionPath(input.provider), {}),
41
- onSuccess: ({ authorizationUrl }, { popup }) => {
42
- // A blocked popup returns null, so use this tab. A tab the viewer
43
- // closed means they cancelled; keep them on the dashboard.
44
- if (!popup) {
45
- window.location.assign(authorizationUrl);
46
- return;
47
- }
48
- if (popup.closed) return;
49
- popup.opener = null;
50
- popup.location.href = authorizationUrl;
51
- },
52
- onError: (_error, { popup }) => {
53
- popup?.close();
54
- },
55
- });
56
- const disconnectMutation = useMutation({
57
- mutationFn: (connection: Connection) =>
58
- del(
59
- disconnectConnectionResponseSchema,
60
- connectionPath(connection.provider),
61
- ),
62
- onSuccess: (_result, connection) => {
63
- queryClient.setQueryData<{ connections: Connection[] }>(
64
- connectionsQueryKey,
65
- (current) =>
66
- current && {
67
- connections: current.connections.map((item) =>
68
- item.provider === connection.provider
69
- ? {
70
- provider: item.provider,
71
- displayName: item.displayName,
72
- description: item.description,
73
- connected: false,
74
- }
75
- : item,
76
- ),
77
- },
78
- );
79
- },
80
- });
81
- const connections = connectionsQuery.data?.connections ?? [];
82
- const busy = connectMutation.isPending || disconnectMutation.isPending;
83
- const pendingProvider = connectMutation.isPending
84
- ? connectMutation.variables?.provider
85
- : disconnectMutation.isPending
86
- ? disconnectMutation.variables?.provider
87
- : undefined;
88
- const listFailed = !connectionsQuery.data && connectionsQuery.isError;
89
- const displayedError = connectMutation.isError
90
- ? "Could not start the connection. Try again."
91
- : disconnectMutation.isError
92
- ? "Could not disconnect the account. Try again."
93
- : listFailed
94
- ? "Could not load connected accounts. Try again."
95
- : undefined;
96
-
97
- return (
98
- <div className={`${dashboardContainerClass} px-4 py-8 md:px-8`}>
99
- <section className="mx-auto w-full max-w-3xl">
100
- <h1 className="m-0 text-2xl font-bold">Connected accounts</h1>
101
- <p className="mt-2 mb-0 max-w-2xl text-sm text-dashboard-text-muted">
102
- Connect an account so {getDashboardAgentName()} can act as you in
103
- conversations you start here.
104
- </p>
105
-
106
- <Card className="mb-0 mt-6" padding="md" variant="raised">
107
- {displayedError ? (
108
- <InlineError className={listFailed ? undefined : "mb-4"}>
109
- {displayedError}
110
- </InlineError>
111
- ) : null}
112
- {listFailed ? null : (
113
- <div className="space-y-2">
114
- {connectionsQuery.isPending ? (
115
- <p className="m-0 text-sm text-dashboard-text-muted">
116
- Loading connected accounts…
117
- </p>
118
- ) : connections.length === 0 ? (
119
- <p className="m-0 text-sm text-dashboard-text-muted">
120
- No installed plugins support account connections.
121
- </p>
122
- ) : (
123
- connections.map((connection) => (
124
- <div
125
- className="flex items-center gap-3 rounded-md border border-dashboard-border-strong p-3"
126
- key={connection.provider}
127
- >
128
- <Plug
129
- className="shrink-0 text-dashboard-accent"
130
- size={16}
131
- />
132
- <div className="min-w-0 flex-1">
133
- <p className="m-0 truncate text-sm font-semibold">
134
- {connection.displayName}
135
- </p>
136
- <p className="mt-1 mb-0 line-clamp-2 break-words text-xs text-dashboard-text-muted">
137
- {connection.connected ? (
138
- connection.account ? (
139
- <>
140
- Connected as{" "}
141
- {connection.account.url ? (
142
- <a
143
- className="text-dashboard-text"
144
- href={connection.account.url}
145
- rel="noreferrer"
146
- target="_blank"
147
- >
148
- {connection.account.label}
149
- </a>
150
- ) : (
151
- connection.account.label
152
- )}
153
- </>
154
- ) : (
155
- "Connected"
156
- )
157
- ) : (
158
- connection.description
159
- )}
160
- </p>
161
- </div>
162
- {connection.connected ? (
163
- <Button
164
- aria-busy={pendingProvider === connection.provider}
165
- aria-label={`Disconnect ${connection.displayName}`}
166
- disabled={busy}
167
- onClick={() => {
168
- connectMutation.reset();
169
- disconnectMutation.mutate(connection);
170
- }}
171
- variant="danger"
172
- >
173
- {pendingProvider === connection.provider
174
- ? "Disconnecting…"
175
- : "Disconnect"}
176
- </Button>
177
- ) : (
178
- <Button
179
- aria-busy={pendingProvider === connection.provider}
180
- aria-label={`Connect ${connection.displayName}`}
181
- disabled={busy}
182
- onClick={() => {
183
- disconnectMutation.reset();
184
- // Open the tab during the click so popup blockers
185
- // allow it, then point it at the provider.
186
- const popup = window.open("about:blank", "_blank");
187
- connectMutation.mutate({
188
- provider: connection.provider,
189
- popup,
190
- });
191
- }}
192
- >
193
- {pendingProvider === connection.provider
194
- ? "Connecting…"
195
- : "Connect"}
196
- </Button>
197
- )}
198
- </div>
199
- ))
200
- )}
201
- </div>
202
- )}
203
- </Card>
204
- </section>
205
- </div>
206
- );
207
- }
@@ -1,205 +0,0 @@
1
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2
- import { KeyRound, Trash2 } from "lucide-react";
3
- import { useRef, useState } from "react";
4
- import {
5
- createdPersonalTokenSchema,
6
- personalTokenListSchema,
7
- type PersonalTokenMetadata,
8
- } from "@sentry/junior/api/schema";
9
- import { deleteDashboardResource, fetchDashboardJson, post } from "../http";
10
- import { dashboardContainerClass } from "../styles";
11
- import { getDashboardAgentName } from "../agentName";
12
- import { useDocumentTitle } from "../documentTitle";
13
- import { Button } from "../components/ui/button";
14
- import { Input } from "../components/ui/input";
15
- import { InlineError } from "../components/InlineError";
16
- import { Card } from "../components/layout/Card";
17
-
18
- const personalTokensQueryKey = ["dashboard", "personal-tokens"] as const;
19
-
20
- /** Create and revoke personal API tokens for local clients. */
21
- export function PersonalTokensPage() {
22
- useDocumentTitle("API tokens");
23
- const queryClient = useQueryClient();
24
- const createTokenStarted = useRef(false);
25
- const [name, setName] = useState("Local agent");
26
- const cancelTokenListRefetch = () =>
27
- queryClient.cancelQueries({
28
- exact: true,
29
- queryKey: personalTokensQueryKey,
30
- });
31
- const tokensQuery = useQuery({
32
- queryKey: personalTokensQueryKey,
33
- queryFn: ({ signal }) =>
34
- fetchDashboardJson(
35
- personalTokenListSchema,
36
- "/api/personal-tokens",
37
- signal,
38
- ),
39
- retry: false,
40
- });
41
- const createTokenMutation = useMutation({
42
- gcTime: 0,
43
- mutationFn: (tokenName: string) =>
44
- post(createdPersonalTokenSchema, "/api/personal-tokens", {
45
- name: tokenName,
46
- }),
47
- onMutate: () => cancelTokenListRefetch(),
48
- onSuccess: async (created) => {
49
- const { token: _token, ...metadata } = created;
50
- await cancelTokenListRefetch();
51
- queryClient.setQueryData<{ tokens: PersonalTokenMetadata[] }>(
52
- personalTokensQueryKey,
53
- (current) => ({
54
- tokens: [
55
- metadata,
56
- ...(current?.tokens ?? []).filter(
57
- (token) => token.id !== metadata.id,
58
- ),
59
- ],
60
- }),
61
- );
62
- },
63
- onSettled: () => {
64
- createTokenStarted.current = false;
65
- },
66
- });
67
- const revokeTokenMutation = useMutation({
68
- mutationFn: (token: PersonalTokenMetadata) =>
69
- deleteDashboardResource(
70
- `/api/personal-tokens/${encodeURIComponent(token.id)}`,
71
- ),
72
- onMutate: () => cancelTokenListRefetch(),
73
- onSuccess: async (_result, token) => {
74
- await cancelTokenListRefetch();
75
- queryClient.setQueryData<{ tokens: PersonalTokenMetadata[] }>(
76
- personalTokensQueryKey,
77
- (current) => ({
78
- tokens: (current?.tokens ?? []).filter(
79
- (item) => item.id !== token.id,
80
- ),
81
- }),
82
- );
83
- if (createTokenMutation.data?.id === token.id) {
84
- createTokenMutation.reset();
85
- }
86
- },
87
- });
88
- const createdToken = createTokenMutation.data;
89
- const tokens = tokensQuery.data?.tokens ?? [];
90
- const loading = tokensQuery.isPending;
91
- const busy = createTokenMutation.isPending || revokeTokenMutation.isPending;
92
- const displayedError =
93
- (createTokenMutation.isError
94
- ? "Could not create the API token. Try again."
95
- : revokeTokenMutation.isError
96
- ? "Could not revoke the API token. Try again."
97
- : undefined) ??
98
- (!tokensQuery.data && tokensQuery.isError
99
- ? "Could not load API tokens. Try again."
100
- : undefined);
101
-
102
- return (
103
- <div className={`${dashboardContainerClass} px-4 py-8 md:px-8`}>
104
- <section className="mx-auto w-full max-w-3xl">
105
- <h1 className="m-0 text-2xl font-bold">Personal API Tokens</h1>
106
- <p className="mt-2 mb-0 max-w-2xl text-sm text-dashboard-text-muted">
107
- Use a token to read {getDashboardAgentName()} APIs from a local agent
108
- or script. Tokens expire after 90 days.
109
- </p>
110
-
111
- <Card className="mb-0 mt-6" padding="md" variant="raised">
112
- {createdToken ? (
113
- <div className="mt-5 rounded-md border border-emerald-400/40 bg-emerald-400/5 p-3">
114
- <p className="mt-0 mb-2 text-sm font-semibold">
115
- Copy this token now. It won't be shown again.
116
- </p>
117
- <code className="block overflow-x-auto rounded-md bg-dashboard-ink p-2 text-xs text-emerald-300">
118
- {createdToken.token}
119
- </code>
120
- <Button
121
- className="mt-3"
122
- onClick={() => {
123
- createTokenMutation.reset();
124
- setName("Local agent");
125
- }}
126
- >
127
- Create another token
128
- </Button>
129
- </div>
130
- ) : (
131
- <div className="mt-5 flex gap-2">
132
- <Input
133
- aria-label="Token name"
134
- className="min-w-0 flex-1"
135
- maxLength={80}
136
- onChange={(event) => setName(event.target.value)}
137
- value={name}
138
- />
139
- <Button
140
- disabled={loading || busy || !name.trim()}
141
- onClick={() => {
142
- if (createTokenStarted.current) return;
143
- revokeTokenMutation.reset();
144
- createTokenStarted.current = true;
145
- createTokenMutation.mutate(name);
146
- }}
147
- >
148
- Create token
149
- </Button>
150
- </div>
151
- )}
152
-
153
- {displayedError ? (
154
- <InlineError className="mt-4">{displayedError}</InlineError>
155
- ) : null}
156
-
157
- <div className="mt-5 space-y-2">
158
- {loading ? (
159
- <p className="text-sm text-dashboard-text-muted">
160
- Loading API tokens…
161
- </p>
162
- ) : tokens.length === 0 ? (
163
- <p className="text-sm text-dashboard-text-muted">
164
- No active API tokens.
165
- </p>
166
- ) : (
167
- tokens.map((token) => (
168
- <div
169
- className="flex items-center gap-3 rounded-md border border-dashboard-border-strong p-3"
170
- key={token.id}
171
- >
172
- <KeyRound className="text-dashboard-accent" size={16} />
173
- <div className="min-w-0 flex-1">
174
- <p className="m-0 truncate text-sm font-semibold">
175
- {token.name}
176
- </p>
177
- <p className="mt-1 mb-0 text-xs text-dashboard-text-muted">
178
- Ends in {token.tokenSuffix} · Expires{" "}
179
- {new Date(token.expiresAt).toLocaleDateString()}
180
- </p>
181
- </div>
182
- <Button
183
- aria-label={`Revoke ${token.name}`}
184
- disabled={busy}
185
- onClick={() => {
186
- if (createTokenMutation.isError) {
187
- createTokenMutation.reset();
188
- }
189
- revokeTokenMutation.mutate(token);
190
- }}
191
- size="icon"
192
- variant="danger"
193
- type="button"
194
- >
195
- <Trash2 size={16} />
196
- </Button>
197
- </div>
198
- ))
199
- )}
200
- </div>
201
- </Card>
202
- </section>
203
- </div>
204
- );
205
- }
@@ -1,12 +0,0 @@
1
- # System pages
2
-
3
- - The URL selects a Workspace recipe: `/system/workspaces/:id` opens
4
- `WorkspaceDetails` beside the list.
5
- - The list report has no snapshots. The detail read adds the current
6
- snapshot. Keep snapshot loading and errors inside the Snapshot row.
7
- - Edit and Delete live in the panel header. List rows have no actions.
8
- - `WorkspaceDeleteDialog` keeps errors in the dialog and blocks dismissal
9
- while the request runs.
10
- - Close and Escape return focus to the selected row.
11
-
12
- Browser coverage: `e2e/system.spec.ts`.
@@ -1,123 +0,0 @@
1
- import { Archive } from "lucide-react";
2
- import type { ReactNode } from "react";
3
- import { Link } from "react-router";
4
- import type { ReviewItemPayload } from "@sentry/junior/api/schema";
5
-
6
- import { StatusChip } from "../components/StatusChip";
7
- import { TranscriptMarkdown } from "../conversations/TranscriptMarkdown";
8
- import { conversationPath } from "../conversations/conversationRoutes";
9
- import { automationPath, formatRelativeTime } from "../format";
10
- import { ReviewItemActions } from "./ReviewItemActions";
11
- import { useLinkedConversationTitles } from "./queries";
12
- import { assigneeLabel, reviewItemStatusLabel } from "./reviewItemFormat";
13
-
14
- function Fact(props: { children: ReactNode; label: string }) {
15
- return (
16
- <div className="grid content-start gap-0.5">
17
- <dt className="text-xs font-medium text-dashboard-text-muted">
18
- {props.label}
19
- </dt>
20
- <dd className="m-0 min-w-0 break-words text-sm">{props.children}</dd>
21
- </div>
22
- );
23
- }
24
-
25
- /** Show one Review item with its facts and the viewer's actions. */
26
- export function ReviewItemDetail(props: { item: ReviewItemPayload }) {
27
- const { item } = props;
28
- const subject = item.subject;
29
- const conversationTitles = useLinkedConversationTitles(item.conversationIds);
30
- const closed = item.status !== "open";
31
- return (
32
- <article className="grid min-w-0 content-start gap-5">
33
- {closed ? (
34
- // Search mixes open and archived items, so a closed item must be
35
- // easy to tell apart at the top of the detail.
36
- <p className="m-0 flex items-center gap-2 rounded-md bg-dashboard-fill-soft px-3 py-2 text-xs font-medium text-dashboard-text-muted">
37
- <Archive aria-hidden="true" size={14} />
38
- Archived · {reviewItemStatusLabel(item)}
39
- </p>
40
- ) : null}
41
- <header className="grid gap-1.5">
42
- <div className="flex min-w-0 items-start justify-between gap-3">
43
- <h2 className="m-0 min-w-0 break-words font-display text-xl font-medium leading-snug">
44
- {item.title}
45
- </h2>
46
- {closed ? null : (
47
- <StatusChip className="mt-1 shrink-0" size="compact" tone="accent">
48
- {reviewItemStatusLabel(item)}
49
- </StatusChip>
50
- )}
51
- </div>
52
- <p className="m-0 text-xs text-dashboard-text-muted">
53
- Added {formatRelativeTime(item.createdAt)}
54
- {item.visibility === "private" ? " · Private" : null}
55
- </p>
56
- </header>
57
- <TranscriptMarkdown text={item.body} />
58
- <section
59
- aria-label={item.status === "open" ? "Actions" : "Outcome"}
60
- className="border-t border-dashboard-border-subtle pt-5"
61
- >
62
- <ReviewItemActions item={item} />
63
- </section>
64
- <dl className="m-0 grid gap-4 border-t border-dashboard-border-subtle pt-5 sm:grid-cols-2">
65
- <Fact label="Assignees">
66
- {item.assignees.length > 0 || item.assignedToEveryone ? (
67
- assigneeLabel(item)
68
- ) : (
69
- <StatusChip size="compact" tone="warning">
70
- Unassigned
71
- </StatusChip>
72
- )}
73
- </Fact>
74
- {subject ? (
75
- <Fact label="Subject">
76
- {subject.url ? (
77
- <a href={subject.url} rel="noreferrer" target="_blank">
78
- {subject.title ?? subject.url}
79
- </a>
80
- ) : (
81
- (subject.title ?? subject.id)
82
- )}
83
- </Fact>
84
- ) : null}
85
- <Fact label="Added by">
86
- <Link to={conversationPath(item.addedByConversationId)}>
87
- Conversation
88
- </Link>
89
- {item.automationId ? (
90
- <>
91
- {" · "}
92
- <Link to={automationPath(item.automationId)}>Automation</Link>
93
- </>
94
- ) : null}
95
- </Fact>
96
- {item.conversationIds.length > 0 ? (
97
- <Fact label="Linked Conversations">
98
- <ul className="m-0 grid list-none gap-0.5 p-0">
99
- {item.conversationIds.map((conversationId, index) => {
100
- const title = conversationTitles[index];
101
- return (
102
- <li
103
- className={
104
- title ? "truncate" : "truncate font-mono text-xs"
105
- }
106
- key={conversationId}
107
- >
108
- <Link
109
- title={conversationId}
110
- to={conversationPath(conversationId)}
111
- >
112
- {title ?? conversationId}
113
- </Link>
114
- </li>
115
- );
116
- })}
117
- </ul>
118
- </Fact>
119
- ) : null}
120
- </dl>
121
- </article>
122
- );
123
- }