@agent-native/core-corpus 0.212.0 → 0.213.1

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/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +29 -14
  3. package/corpus/templates/analytics/CHANGELOG.md +11 -0
  4. package/corpus/templates/analytics/actions/estimate-onboarding-journey-cost.ts +23 -6
  5. package/corpus/templates/analytics/actions/get-onboarding-journey.ts +20 -5
  6. package/corpus/templates/analytics/actions/provider-api-register.ts +2 -7
  7. package/corpus/templates/analytics/changelog/2026-10-21-analytics-recognizes-questions-about-connected-data-product-.md +6 -0
  8. package/corpus/templates/analytics/changelog/2026-10-21-onboarding-journeys-now-honor-larger-event-row-budgets.md +6 -0
  9. package/corpus/templates/analytics/changelog/2026-10-21-preserve-unknown-replay-availability-in-partial-journeys.md +6 -0
  10. package/corpus/templates/analytics/package.json +6 -0
  11. package/corpus/templates/analytics/scripts/journey-capture-plan.ts +18 -7
  12. package/corpus/templates/analytics/scripts/journey-capture.ts +3 -1
  13. package/corpus/templates/analytics/server/lib/db-admin-connections.ts +19 -12
  14. package/corpus/templates/analytics/server/lib/journey-tree.ts +24 -9
  15. package/corpus/templates/analytics/server/lib/onboarding-journey-cost.ts +163 -94
  16. package/corpus/templates/analytics/server/lib/onboarding-journey.ts +162 -74
  17. package/corpus/templates/assets/server/lib/org-admin.ts +20 -16
  18. package/corpus/templates/brain/server/lib/zoom-governance.ts +11 -6
  19. package/corpus/templates/clips/CHANGELOG.md +1 -0
  20. package/corpus/templates/clips/changelog/2026-10-16-rewind-status-says-when-it-remembers-audio.md +6 -0
  21. package/corpus/templates/clips/desktop/src/app.tsx +7 -4
  22. package/corpus/templates/clips/desktop/src/components/RewindCaptureStatus.tsx +25 -0
  23. package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +6 -1
  24. package/corpus/templates/clips/desktop/src/shared/config.ts +3 -0
  25. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2 -0
  26. package/corpus/templates/clips/server/lib/recordings.ts +4 -19
  27. package/corpus/templates/community/qr/package.json +1 -1
  28. package/corpus/templates/content/CHANGELOG.md +8 -0
  29. package/corpus/templates/content/actions/_content-space-access.ts +95 -6
  30. package/corpus/templates/content/actions/_database-utils.ts +20 -16
  31. package/corpus/templates/content/actions/_github-source-freshness.ts +72 -17
  32. package/corpus/templates/content/actions/_github-source-metadata.ts +63 -17
  33. package/corpus/templates/content/actions/_github-source.ts +141 -77
  34. package/corpus/templates/content/actions/check-github-connection.ts +15 -0
  35. package/corpus/templates/content/actions/create-github-collection.ts +180 -0
  36. package/corpus/templates/content/actions/disconnect-content-database-source.ts +5 -0
  37. package/corpus/templates/content/actions/link-github-folder.ts +104 -36
  38. package/corpus/templates/content/actions/refresh-content-database-source.ts +37 -4
  39. package/corpus/templates/content/actions/sync-github-folder.ts +23 -5
  40. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +172 -19
  41. package/corpus/templates/content/app/components/editor/database/GitHubLinkDialog.tsx +938 -0
  42. package/corpus/templates/content/app/components/editor/database/GitHubSourcePanel.tsx +228 -0
  43. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +561 -0
  44. package/corpus/templates/content/app/components/editor/database/GitHubUnlinkDialog.tsx +96 -0
  45. package/corpus/templates/content/app/components/editor/database/github-availability.ts +147 -0
  46. package/corpus/templates/content/app/components/editor/database/github-source-text.ts +280 -0
  47. package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
  48. package/corpus/templates/content/app/hooks/content-action-refresh.ts +6 -0
  49. package/corpus/templates/content/app/hooks/use-content-database.ts +3 -1
  50. package/corpus/templates/content/app/hooks/use-github-folder.ts +199 -0
  51. package/corpus/templates/content/app/i18n-data.ts +154 -1
  52. package/corpus/templates/content/app/lib/github-link-dialog.ts +86 -0
  53. package/corpus/templates/content/changelog/2026-10-18-connected-ai-assistants-can-link-a-new-collection-to-a-github.md +1 -1
  54. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +6 -0
  55. package/corpus/templates/content/changelog/2026-10-21-organization-favorites-include-documents-shared-with-the-cur.md +6 -0
  56. package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +32 -7
  57. package/corpus/templates/content/docs/product/encyclopedia.md +3 -3
  58. package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +2 -2
  59. package/corpus/templates/content/docs/product/roadmap.md +1 -1
  60. package/corpus/templates/content/parity/matrix.md +42 -42
  61. package/corpus/templates/content/parity/matrix.ts +14 -0
  62. package/corpus/templates/content/server/lib/github-source-client.ts +50 -1
  63. package/corpus/templates/content/shared/api.ts +3 -0
  64. package/corpus/templates/content/shared/github-source.ts +68 -0
  65. package/corpus/templates/design/CHANGELOG.md +1 -0
  66. package/corpus/templates/design/actions/connect-assets-mcp.ts +5 -9
  67. package/corpus/templates/design/changelog/2026-10-21-journey-canvases-accept-partial-analytics-samples-when-repla.md +6 -0
  68. package/corpus/templates/design/shared/journey-canvas.ts +6 -4
  69. package/corpus/templates/dispatch/.agents/skills/a2a-protocol/SKILL.md +16 -13
  70. package/corpus/templates/dispatch/CHANGELOG.md +10 -0
  71. package/corpus/templates/dispatch/changelog/2026-10-21-customer-ai-credit-questions-route-to-account-health-instead.md +6 -0
  72. package/corpus/templates/dispatch/changelog/2026-10-21-workspace-apps-load-reliably-with-organization-sign-in-and-f.md +6 -0
  73. package/corpus/templates/factory/server/lib/require-workspace-member.ts +5 -17
  74. package/corpus/templates/mail/server/lib/queued-drafts.ts +9 -4
  75. package/corpus/templates/slides/server/workspace-defaults.ts +12 -15
  76. package/package.json +1 -1
@@ -0,0 +1,561 @@
1
+ import { actionErrorMessage } from "@agent-native/core/client/hooks";
2
+ import { startWorkspaceProviderOAuth } from "@agent-native/core/client/integrations";
3
+ import type {
4
+ ContentDatabaseSource,
5
+ ContentGitHubSourceStatus,
6
+ } from "@shared/api";
7
+ import {
8
+ GITHUB_SOURCE_TYPE,
9
+ githubFolderLabel,
10
+ githubFolderName,
11
+ type GitHubSourceProblemCode,
12
+ } from "@shared/github-source";
13
+ import {
14
+ IconBrandGithub,
15
+ IconChevronDown,
16
+ IconExternalLink,
17
+ IconRefresh,
18
+ } from "@tabler/icons-react";
19
+ import {
20
+ Fragment,
21
+ useEffect,
22
+ useState,
23
+ type CSSProperties,
24
+ type ReactNode,
25
+ } from "react";
26
+ import { toast } from "sonner";
27
+
28
+ import { Button } from "@/components/ui/button";
29
+ import {
30
+ Popover,
31
+ PopoverContent,
32
+ PopoverTrigger,
33
+ } from "@/components/ui/popover";
34
+ import { Spinner } from "@/components/ui/spinner";
35
+ import {
36
+ useSyncGitHubFolder,
37
+ type SyncGitHubFolderResponse,
38
+ } from "@/hooks/use-github-folder";
39
+
40
+ import {
41
+ githubProblemFromErrorCode,
42
+ githubProblemStopsUpdates,
43
+ githubProblemText,
44
+ githubRelativeTime,
45
+ githubSkipReasonText,
46
+ shortCommit,
47
+ } from "./github-source-text";
48
+ import { GitHubUnlinkDialog } from "./GitHubUnlinkDialog";
49
+ import { dbText } from "./text";
50
+
51
+ /** How often "2 minutes ago" moves on. */
52
+ const RELATIVE_TIME_TICK_MS = 30_000;
53
+
54
+ /** Stands in for a name while a sentence is split around it. */
55
+ const NAME_MARK = "\u0000";
56
+
57
+ export function useNow(intervalMs = RELATIVE_TIME_TICK_MS) {
58
+ const [now, setNow] = useState(() => Date.now());
59
+ useEffect(() => {
60
+ const timer = window.setInterval(() => setNow(Date.now()), intervalMs);
61
+ return () => window.clearInterval(timer);
62
+ }, [intervalMs]);
63
+ return now;
64
+ }
65
+
66
+ /** "From the **docs** folder on GitHub", with the name in bold in any language. */
67
+ function FromFolder({ status }: { status: ContentGitHubSourceStatus }) {
68
+ const name = githubFolderName(status);
69
+ const sentence = status.path
70
+ ? dbText("githubFromFolder", { folder: NAME_MARK })
71
+ : dbText("githubFromRepo", { repo: NAME_MARK });
72
+ const [before, after = ""] = sentence.split(NAME_MARK);
73
+ return (
74
+ <span className="min-w-0 break-words text-foreground">
75
+ {before}
76
+ <strong className="font-medium">{name}</strong>
77
+ {after}
78
+ </span>
79
+ );
80
+ }
81
+
82
+ /** "Updated 2 minutes ago", or "Not updated yet" before the first update. */
83
+ export function githubUpdatedText(
84
+ status: ContentGitHubSourceStatus,
85
+ now: number,
86
+ ): string {
87
+ return status.syncedAt
88
+ ? dbText("githubUpdatedAgo", {
89
+ time: githubRelativeTime(status.syncedAt, now),
90
+ })
91
+ : dbText("githubNotUpdatedYet");
92
+ }
93
+
94
+ function announceSync(
95
+ result: SyncGitHubFolderResponse,
96
+ status: ContentGitHubSourceStatus,
97
+ ) {
98
+ const { state, problem, created, updated, renamed, trashed } = result.sync;
99
+ if (state === "error") {
100
+ toast.error(githubProblemText(problem?.code ?? "unavailable", status));
101
+ } else if (state === "busy") {
102
+ toast(dbText("githubSyncBusy"));
103
+ } else if (state === "unchanged") {
104
+ toast.success(dbText("githubAlreadyUpToDate"));
105
+ } else if (state === "partial") {
106
+ const progress = result.source?.metadata.github?.progress;
107
+ toast.success(
108
+ dbText("githubSyncPartialToast", {
109
+ done: progress?.done ?? created + updated,
110
+ total: progress?.total ?? created + updated + result.sync.remaining,
111
+ }),
112
+ );
113
+ } else {
114
+ const changed = created + updated + renamed + trashed;
115
+ // A new commit that touched nothing in the folder leaves the pages as current as "unchanged" does.
116
+ toast.success(
117
+ changed === 0
118
+ ? dbText("githubAlreadyUpToDate")
119
+ : changed === 1
120
+ ? dbText("githubSyncedToastOne")
121
+ : dbText("githubSyncedToast", { count: changed }),
122
+ );
123
+ }
124
+ }
125
+
126
+ /** "Check for updates", which asks GitHub now instead of waiting. */
127
+ export function useCheckGitHubForUpdates(source: ContentDatabaseSource) {
128
+ const sync = useSyncGitHubFolder();
129
+ const status = source.metadata.github;
130
+ return {
131
+ pending: sync.isPending,
132
+ run: () =>
133
+ sync.mutate(
134
+ { databaseId: source.databaseId },
135
+ {
136
+ onSuccess: (result) => status && announceSync(result, status),
137
+ onError: (error) => {
138
+ const problem = githubProblemFromErrorCode(
139
+ (error as { errorCode?: unknown } | null)?.errorCode,
140
+ );
141
+ toast.error(
142
+ problem
143
+ ? githubProblemText(problem, status)
144
+ : (actionErrorMessage(error) ??
145
+ dbText("githubLinkUnavailable")),
146
+ );
147
+ },
148
+ },
149
+ ),
150
+ };
151
+ }
152
+
153
+ function DetailRow({
154
+ label,
155
+ children,
156
+ }: {
157
+ label: string;
158
+ children: ReactNode;
159
+ }) {
160
+ return (
161
+ <div className="flex min-w-0 gap-2">
162
+ <dt className="w-20 shrink-0 text-muted-foreground">{label}</dt>
163
+ <dd className="min-w-0 flex-1 break-words">{children}</dd>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ /** An item of the status line, led by its "·" (see the line for why). */
169
+ function Dotted({ children }: { children: ReactNode }) {
170
+ return (
171
+ <span className="inline-flex min-w-0 items-center">
172
+ <span aria-hidden className="w-3 shrink-0 text-center">
173
+ ·
174
+ </span>
175
+ {children}
176
+ </span>
177
+ );
178
+ }
179
+
180
+ /** A path that wraps after a slash rather than in the middle of a name. */
181
+ function WrapsAtSlashes({ text }: { text: string }) {
182
+ return text.split("/").map((part, index) => (
183
+ <Fragment key={index}>
184
+ {index > 0 ? (
185
+ <>
186
+ /<wbr />
187
+ </>
188
+ ) : null}
189
+ {part}
190
+ </Fragment>
191
+ ));
192
+ }
193
+
194
+ /** Where the folder is and how current it is, as Details and the Sources panel show it. */
195
+ export function GitHubFolderFacts({
196
+ status,
197
+ now,
198
+ }: {
199
+ status: ContentGitHubSourceStatus;
200
+ now: number;
201
+ }) {
202
+ return (
203
+ <dl className="grid min-w-0 gap-1.5 text-xs">
204
+ <DetailRow label={dbText("githubDetailFolder")}>
205
+ <a
206
+ href={status.folderUrl}
207
+ target="_blank"
208
+ rel="noreferrer"
209
+ className="underline-offset-2 hover:underline"
210
+ >
211
+ <WrapsAtSlashes text={githubFolderLabel(status)} />
212
+ </a>
213
+ </DetailRow>
214
+ <DetailRow label={dbText("githubDetailBranch")}>
215
+ {status.branch}
216
+ </DetailRow>
217
+ {status.syncedCommitSha ? (
218
+ <DetailRow label={dbText("githubDetailVersion")}>
219
+ {shortCommit(status.syncedCommitSha)}
220
+ </DetailRow>
221
+ ) : null}
222
+ <DetailRow label={dbText("githubDetailUpdated")}>
223
+ {status.syncedAt
224
+ ? githubRelativeTime(status.syncedAt, now)
225
+ : dbText("githubNotUpdatedYet")}
226
+ </DetailRow>
227
+ </dl>
228
+ );
229
+ }
230
+
231
+ /** "1 file not shown", which opens the list of files and why. */
232
+ function NotShownFiles({
233
+ files,
234
+ count,
235
+ }: {
236
+ files: ContentGitHubSourceStatus["skipped"];
237
+ count: number;
238
+ }) {
239
+ return (
240
+ <Popover>
241
+ <PopoverTrigger asChild>
242
+ <button
243
+ type="button"
244
+ className="rounded-sm underline decoration-dotted underline-offset-2 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
245
+ >
246
+ {count === 1
247
+ ? dbText("githubNotShownOne")
248
+ : dbText("githubNotShown", { count })}
249
+ </button>
250
+ </PopoverTrigger>
251
+ <PopoverContent align="start" className="w-80">
252
+ <div className="grid min-w-0 gap-2 text-xs">
253
+ <div className="font-medium">{dbText("githubNotShownHeading")}</div>
254
+ <ul className="grid max-h-60 min-w-0 gap-1 overflow-y-auto">
255
+ {files.map((file) => (
256
+ <li key={file.path} className="flex min-w-0 items-baseline gap-2">
257
+ <span className="min-w-0 truncate" title={file.path}>
258
+ {file.path}
259
+ </span>
260
+ <span className="shrink-0 text-muted-foreground">
261
+ {githubSkipReasonText(file.reason)}
262
+ </span>
263
+ </li>
264
+ ))}
265
+ {count > files.length ? (
266
+ <li className="text-muted-foreground">
267
+ {dbText("githubLinkMore", { count: count - files.length })}
268
+ </li>
269
+ ) : null}
270
+ </ul>
271
+ </div>
272
+ </PopoverContent>
273
+ </Popover>
274
+ );
275
+ }
276
+
277
+ /** What each stopping problem offers, besides waiting. */
278
+ function problemActions(code: GitHubSourceProblemCode): {
279
+ reconnect: boolean;
280
+ open: boolean;
281
+ unlink: boolean;
282
+ } {
283
+ return {
284
+ reconnect: code === "connection",
285
+ open:
286
+ code === "forbidden" ||
287
+ code === "repository_not_found" ||
288
+ code === "folder_not_found",
289
+ unlink:
290
+ code === "repository_not_found" ||
291
+ code === "branch_not_found" ||
292
+ code === "folder_not_found" ||
293
+ code === "too_large",
294
+ };
295
+ }
296
+
297
+ function reconnectGitHub() {
298
+ startWorkspaceProviderOAuth("github", {
299
+ appId: "content",
300
+ returnPath: `${window.location.pathname}${window.location.search}`,
301
+ scope: "user",
302
+ });
303
+ }
304
+
305
+ /**
306
+ * The line above a collection that comes from a GitHub folder: which folder,
307
+ * when it last updated, where to edit it, and Details. A problem that stops
308
+ * updates gets its own red line with the one thing to do about it.
309
+ */
310
+ export function GitHubSourceStatusLine({
311
+ source,
312
+ documentId,
313
+ canEdit,
314
+ pageCount,
315
+ }: {
316
+ source: ContentDatabaseSource | null;
317
+ documentId: string;
318
+ canEdit: boolean;
319
+ pageCount: number;
320
+ }) {
321
+ const now = useNow();
322
+ const [unlinkOpen, setUnlinkOpen] = useState(false);
323
+ const [detailsOpen, setDetailsOpen] = useState(false);
324
+ const status =
325
+ source?.sourceType === GITHUB_SOURCE_TYPE
326
+ ? source.metadata.github
327
+ : undefined;
328
+ if (!source || !status) return null;
329
+ return (
330
+ <GitHubStatusLineContent
331
+ source={source}
332
+ status={status}
333
+ now={now}
334
+ documentId={documentId}
335
+ canEdit={canEdit}
336
+ pageCount={pageCount}
337
+ detailsOpen={detailsOpen}
338
+ onDetailsOpenChange={setDetailsOpen}
339
+ unlinkOpen={unlinkOpen}
340
+ onUnlinkOpenChange={setUnlinkOpen}
341
+ />
342
+ );
343
+ }
344
+
345
+ function GitHubStatusLineContent({
346
+ source,
347
+ status,
348
+ now,
349
+ documentId,
350
+ canEdit,
351
+ pageCount,
352
+ detailsOpen,
353
+ onDetailsOpenChange,
354
+ unlinkOpen,
355
+ onUnlinkOpenChange,
356
+ }: {
357
+ source: ContentDatabaseSource;
358
+ status: ContentGitHubSourceStatus;
359
+ now: number;
360
+ documentId: string;
361
+ canEdit: boolean;
362
+ pageCount: number;
363
+ detailsOpen: boolean;
364
+ onDetailsOpenChange: (open: boolean) => void;
365
+ unlinkOpen: boolean;
366
+ onUnlinkOpenChange: (open: boolean) => void;
367
+ }) {
368
+ const check = useCheckGitHubForUpdates(source);
369
+ const problem = status.problem;
370
+ const stops = problem ? githubProblemStopsUpdates(problem.code) : false;
371
+ const actions = problem && stops ? problemActions(problem.code) : null;
372
+ const syncing = !!status.pendingCommitSha && !!status.progress;
373
+ // Pictures waiting on storage are a note, not a stop: the pages still update.
374
+ const picturesWaiting =
375
+ problem?.code === "storage_unavailable" || status.imagesWaiting > 0;
376
+ const paused = !!problem && !stops && problem.code !== "storage_unavailable";
377
+ const openUnlink = () => {
378
+ onDetailsOpenChange(false);
379
+ onUnlinkOpenChange(true);
380
+ };
381
+
382
+ return (
383
+ <div className="mb-2 grid min-w-0 gap-1 text-xs" data-github-source-status>
384
+ {/* Every item starts with its dot, and the row sits one dot to the left
385
+ of a clipped box, so the dot of whichever item starts a line is cut
386
+ off: no line starts or ends with a dot, however the row wraps. */}
387
+ <div className="-m-px overflow-hidden p-px">
388
+ <div className="-ms-3 flex min-w-0 flex-wrap items-center gap-y-1 text-muted-foreground">
389
+ <Dotted>
390
+ <IconBrandGithub className="me-1.5 size-3.5 shrink-0" aria-hidden />
391
+ <FromFolder status={status} />
392
+ </Dotted>
393
+ {stops ? null : (
394
+ <Dotted>
395
+ <span>{githubUpdatedText(status, now)}</span>
396
+ </Dotted>
397
+ )}
398
+ <Dotted>
399
+ <a
400
+ href={status.folderUrl}
401
+ target="_blank"
402
+ rel="noreferrer"
403
+ className="inline-flex items-center gap-0.5 rounded-sm underline-offset-2 hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
404
+ >
405
+ {dbText("githubEditOnGitHub")}
406
+ <IconExternalLink className="size-3" aria-hidden />
407
+ </a>
408
+ </Dotted>
409
+ {status.skippedCount > 0 ? (
410
+ <Dotted>
411
+ <NotShownFiles
412
+ files={status.skipped}
413
+ count={status.skippedCount}
414
+ />
415
+ </Dotted>
416
+ ) : null}
417
+ {/* No dot before Details, but the same lead, so it isn't cut off. */}
418
+ <span className="ps-3">
419
+ <Popover open={detailsOpen} onOpenChange={onDetailsOpenChange}>
420
+ <PopoverTrigger asChild>
421
+ <button
422
+ type="button"
423
+ className="inline-flex items-center gap-0.5 rounded-sm px-1 py-0.5 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
424
+ >
425
+ {dbText("githubDetails")}
426
+ <IconChevronDown className="size-3" aria-hidden />
427
+ </button>
428
+ </PopoverTrigger>
429
+ <PopoverContent align="start" className="w-72">
430
+ <div className="grid min-w-0 gap-3">
431
+ <GitHubFolderFacts status={status} now={now} />
432
+ <div className="flex flex-wrap gap-1.5">
433
+ <Button
434
+ type="button"
435
+ size="xs"
436
+ variant="outline"
437
+ disabled={check.pending}
438
+ onClick={check.run}
439
+ >
440
+ {check.pending ? (
441
+ <Spinner />
442
+ ) : (
443
+ <IconRefresh aria-hidden />
444
+ )}
445
+ {dbText("githubCheckForUpdates")}
446
+ </Button>
447
+ {canEdit ? (
448
+ <Button
449
+ type="button"
450
+ size="xs"
451
+ variant="outline-destructive"
452
+ onClick={openUnlink}
453
+ >
454
+ {dbText("githubUnlinkFolder")}
455
+ </Button>
456
+ ) : null}
457
+ </div>
458
+ </div>
459
+ </PopoverContent>
460
+ </Popover>
461
+ </span>
462
+ </div>
463
+ </div>
464
+
465
+ {problem && stops ? (
466
+ // Only the problem is announced: the line above holds controls and a
467
+ // time that moves on every half minute.
468
+ <div
469
+ className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-destructive"
470
+ role="alert"
471
+ data-github-source-problem
472
+ >
473
+ <span className="min-w-0 break-words">
474
+ {githubProblemText(problem.code, status)}{" "}
475
+ {status.syncedAt
476
+ ? dbText("githubShowingAsOf", {
477
+ time: githubRelativeTime(status.syncedAt, now),
478
+ })
479
+ : null}
480
+ </span>
481
+ {actions?.reconnect && canEdit ? (
482
+ <Button
483
+ type="button"
484
+ size="xs"
485
+ variant="outline"
486
+ onClick={reconnectGitHub}
487
+ >
488
+ <IconBrandGithub aria-hidden />
489
+ {dbText("githubReconnect")}
490
+ </Button>
491
+ ) : null}
492
+ {actions?.open ? (
493
+ <Button asChild size="xs" variant="outline">
494
+ <a href={status.folderUrl} target="_blank" rel="noreferrer">
495
+ {dbText("githubOpenFolder")}
496
+ <IconExternalLink aria-hidden />
497
+ </a>
498
+ </Button>
499
+ ) : null}
500
+ {actions?.unlink && canEdit ? (
501
+ <Button
502
+ type="button"
503
+ size="xs"
504
+ variant="outline-destructive"
505
+ onClick={openUnlink}
506
+ >
507
+ {dbText("githubUnlinkFolder")}
508
+ </Button>
509
+ ) : null}
510
+ </div>
511
+ ) : null}
512
+
513
+ {paused && problem ? (
514
+ <div className="break-words text-muted-foreground">
515
+ {githubProblemText(problem.code, status)}
516
+ </div>
517
+ ) : null}
518
+
519
+ {picturesWaiting ? (
520
+ <div className="break-words text-muted-foreground">
521
+ {dbText("githubPicturesWaiting")}
522
+ </div>
523
+ ) : null}
524
+
525
+ {syncing && status.progress ? (
526
+ <div className="grid max-w-sm gap-1">
527
+ <div className="text-muted-foreground">
528
+ {dbText("githubSyncingProgress", {
529
+ done: status.progress.done,
530
+ total: status.progress.total,
531
+ })}
532
+ </div>
533
+ <div className="h-1.5 overflow-hidden rounded-full bg-muted">
534
+ <div
535
+ className="h-full w-(--github-sync-progress) rounded-full bg-foreground/70 transition-all"
536
+ style={
537
+ {
538
+ "--github-sync-progress": `${Math.round(
539
+ (status.progress.done /
540
+ Math.max(status.progress.total, 1)) *
541
+ 100,
542
+ )}%`,
543
+ } as CSSProperties
544
+ }
545
+ />
546
+ </div>
547
+ </div>
548
+ ) : null}
549
+
550
+ {canEdit ? (
551
+ <GitHubUnlinkDialog
552
+ open={unlinkOpen}
553
+ onOpenChange={onUnlinkOpenChange}
554
+ documentId={documentId}
555
+ sourceId={source.id}
556
+ pageCount={pageCount}
557
+ />
558
+ ) : null}
559
+ </div>
560
+ );
561
+ }
@@ -0,0 +1,96 @@
1
+ import { actionErrorMessage } from "@agent-native/core/client/hooks";
2
+ import { toast } from "sonner";
3
+
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ } from "@/components/ui/alert-dialog";
14
+ import { Spinner } from "@/components/ui/spinner";
15
+ import { useDisconnectContentDatabaseSource } from "@/hooks/use-content-database";
16
+
17
+ import { dbText } from "./text";
18
+
19
+ /**
20
+ * Asks before unlinking a GitHub folder: the pages stay and become editable,
21
+ * and the folder can't come back into this collection, so it says both.
22
+ */
23
+ export function GitHubUnlinkDialog({
24
+ open,
25
+ onOpenChange,
26
+ documentId,
27
+ sourceId,
28
+ pageCount,
29
+ onUnlinked,
30
+ }: {
31
+ open: boolean;
32
+ onOpenChange: (open: boolean) => void;
33
+ documentId: string;
34
+ sourceId: string;
35
+ pageCount: number;
36
+ onUnlinked?: () => void;
37
+ }) {
38
+ const unlink = useDisconnectContentDatabaseSource(documentId);
39
+ return (
40
+ <AlertDialog
41
+ open={open}
42
+ onOpenChange={(next) => {
43
+ if (unlink.isPending && !next) return;
44
+ onOpenChange(next);
45
+ }}
46
+ >
47
+ <AlertDialogContent>
48
+ <AlertDialogHeader>
49
+ <AlertDialogTitle>{dbText("githubUnlinkTitle")}</AlertDialogTitle>
50
+ <AlertDialogDescription>
51
+ {pageCount === 1
52
+ ? dbText("githubUnlinkBodyOne")
53
+ : dbText("githubUnlinkBody", { count: pageCount })}
54
+ </AlertDialogDescription>
55
+ </AlertDialogHeader>
56
+ <AlertDialogFooter>
57
+ <AlertDialogCancel disabled={unlink.isPending}>
58
+ {dbText("cancel")}
59
+ </AlertDialogCancel>
60
+ <AlertDialogAction
61
+ variant="destructive"
62
+ disabled={unlink.isPending}
63
+ onClick={(event) => {
64
+ // Stays open until the unlink is saved, so a failure is seen here.
65
+ event.preventDefault();
66
+ unlink.mutate(
67
+ { documentId, sourceId },
68
+ {
69
+ onSuccess: () => {
70
+ toast.success(dbText("githubUnlinkedToast"));
71
+ onOpenChange(false);
72
+ onUnlinked?.();
73
+ },
74
+ onError: (error) => {
75
+ toast.error(dbText("githubUnlinkFailed"), {
76
+ description:
77
+ actionErrorMessage(error) ??
78
+ (error instanceof Error
79
+ ? error.message
80
+ : dbText("tryAgain")),
81
+ });
82
+ },
83
+ },
84
+ );
85
+ }}
86
+ >
87
+ {unlink.isPending ? <Spinner className="mr-1 size-3.5" /> : null}
88
+ {unlink.isPending
89
+ ? dbText("githubUnlinking")
90
+ : dbText("githubUnlink")}
91
+ </AlertDialogAction>
92
+ </AlertDialogFooter>
93
+ </AlertDialogContent>
94
+ </AlertDialog>
95
+ );
96
+ }