@agent-native/core 0.136.5 → 0.137.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/components/AgentCompletionSound.tsx +105 -0
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +44 -35
- package/corpus/templates/analytics/app/i18n-data.ts +38 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +53 -1
- package/corpus/templates/analytics/app/pages/settings/settings-search.ts +6 -0
- package/corpus/templates/analytics/changelog/2026-08-04-choose-whether-analytics-plays-a-bell-when-the-agent-finishe.md +6 -0
- package/corpus/templates/analytics/shared/analytics-user-prefs.ts +2 -0
- package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
- package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
- package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
- package/corpus/templates/content/actions/_database-utils.ts +6 -0
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
- package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
- package/corpus/templates/content/actions/configure-document-property.ts +113 -32
- package/corpus/templates/content/actions/delete-content-database.ts +5 -7
- package/corpus/templates/content/actions/delete-document-property.ts +109 -62
- package/corpus/templates/content/actions/delete-document.ts +178 -36
- package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
- package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
- package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
- package/corpus/templates/content/actions/remove-database-items.ts +13 -0
- package/corpus/templates/content/actions/set-document-property.ts +69 -1
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +1 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +24 -0
- package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
- package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
- package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
- package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
- package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
- package/corpus/templates/design/app/i18n-data.ts +8 -0
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
- package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
- package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
- package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
- package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
- package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
- package/corpus/templates/factory/.env.example +17 -0
- package/corpus/templates/factory/AGENTS.md +27 -17
- package/corpus/templates/factory/README.md +19 -9
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
- package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
- package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
- package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
- package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
- package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
- package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
- package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
- package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
- package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
- package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
- package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
- package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
- package/corpus/templates/factory/app/routes/factory.tsx +462 -9
- package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
- package/corpus/templates/factory/netlify.toml +2 -2
- package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
- package/corpus/templates/factory/server/connectors/slack.ts +133 -5
- package/corpus/templates/factory/server/db/schema.ts +6 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
- package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
- package/corpus/templates/factory/server/plugins/auth.ts +1 -0
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
- package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
- package/corpus/templates/factory/server/triage/contracts.ts +14 -1
- package/corpus/templates/factory/server/triage/github-client.ts +404 -0
- package/corpus/templates/factory/server/triage/metadata.ts +44 -0
- package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
- package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
- package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
- package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
- package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
- package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
- package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
- package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
- package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
- package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
- package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
- package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
- package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
- package/corpus/templates/plan/shared/plan-content.ts +17 -2
- package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +321 -0
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +32 -11
- package/corpus/templates/slides/app/lib/recent-references.ts +82 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -0
- package/corpus/templates/slides/app/pages/Index.tsx +236 -181
- package/dist/cli/skills-content/canvas.d.ts +3 -3
- package/dist/cli/skills-content/canvas.js +16 -5
- package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
- package/dist/cli/skills-content/visual-plan-skill.js +27 -0
- package/dist/cli/skills-content/wireframe.d.ts +2 -2
- package/dist/cli/skills-content/wireframe.js +7 -0
- package/dist/client/AgentPanel.js +23 -8
- package/dist/client/DefaultSpinner.d.ts +0 -12
- package/dist/client/DefaultSpinner.js +22 -2
- package/dist/client/api-path.d.ts +6 -0
- package/dist/client/api-path.js +55 -0
- package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
- package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
- package/dist/client/onboarding/first-run-enabled.js +9 -0
- package/dist/client/onboarding/index.d.ts +2 -1
- package/dist/client/onboarding/index.js +1 -0
- package/dist/client/onboarding/use-onboarding.d.ts +6 -1
- package/dist/client/onboarding/use-onboarding.js +35 -1
- package/dist/client/route-state.js +10 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/connections/catalog.d.ts +4 -4
- package/dist/connections/catalog.js +3 -3
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/deploy/build.js +2 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/onboarding/app-profile.d.ts +4 -0
- package/dist/onboarding/app-profile.js +378 -0
- package/dist/onboarding/index.d.ts +2 -1
- package/dist/onboarding/index.js +1 -0
- package/dist/onboarding/plugin.d.ts +2 -0
- package/dist/onboarding/plugin.js +21 -1
- package/dist/onboarding/types.d.ts +19 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +12 -12
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/auth.js +21 -1
- package/dist/server/better-auth-instance.d.ts +3 -2
- package/dist/server/better-auth-instance.js +9 -7
- package/dist/server/email-markdown.js +3 -8
- package/dist/shared/first-run-onboarding.d.ts +3 -0
- package/dist/shared/first-run-onboarding.js +3 -0
- package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
- package/dist/triggers/index.d.ts +3 -0
- package/dist/triggers/index.js +5 -0
- package/docs/content/cross-app-sso.mdx +26 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
|
@@ -32,8 +32,6 @@ import {
|
|
|
32
32
|
IconShare,
|
|
33
33
|
IconSettings,
|
|
34
34
|
IconSearch,
|
|
35
|
-
IconUpload,
|
|
36
|
-
IconLink,
|
|
37
35
|
} from "@tabler/icons-react";
|
|
38
36
|
import { ReactNode, useEffect, useMemo, useState } from "react";
|
|
39
37
|
import { NavLink, useLocation, useParams } from "react-router";
|
|
@@ -43,6 +41,7 @@ import {
|
|
|
43
41
|
CaptureInstallButton,
|
|
44
42
|
CaptureInstallInlineLink,
|
|
45
43
|
} from "@/components/capture-install-options";
|
|
44
|
+
import { ImportMenu } from "@/components/import-menu";
|
|
46
45
|
import {
|
|
47
46
|
AlertDialog,
|
|
48
47
|
AlertDialogAction,
|
|
@@ -400,34 +399,14 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
400
399
|
</div>
|
|
401
400
|
|
|
402
401
|
<div className="flex flex-col items-center gap-1 px-2">
|
|
403
|
-
<
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
</NavLink>
|
|
412
|
-
</TooltipTrigger>
|
|
413
|
-
<TooltipContent side="right">
|
|
414
|
-
{t("preRecord.uploadVideo")}
|
|
415
|
-
</TooltipContent>
|
|
416
|
-
</Tooltip>
|
|
417
|
-
<Tooltip>
|
|
418
|
-
<TooltipTrigger asChild>
|
|
419
|
-
<NavLink
|
|
420
|
-
to="/import"
|
|
421
|
-
aria-label={t("preRecord.importLoom")}
|
|
422
|
-
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent/60 hover:text-foreground"
|
|
423
|
-
>
|
|
424
|
-
<IconLink className="h-4 w-4" />
|
|
425
|
-
</NavLink>
|
|
426
|
-
</TooltipTrigger>
|
|
427
|
-
<TooltipContent side="right">
|
|
428
|
-
{t("preRecord.importLoom")}
|
|
429
|
-
</TooltipContent>
|
|
430
|
-
</Tooltip>
|
|
402
|
+
<ImportMenu
|
|
403
|
+
uploadHref="/record?autoUpload=1"
|
|
404
|
+
importLoomHref="/import"
|
|
405
|
+
iconOnly
|
|
406
|
+
variant="ghost"
|
|
407
|
+
menuSide="right"
|
|
408
|
+
menuAlign="start"
|
|
409
|
+
/>
|
|
431
410
|
</div>
|
|
432
411
|
|
|
433
412
|
<nav className="mt-3 flex flex-col items-center gap-1 px-2">
|
|
@@ -463,28 +442,13 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
463
442
|
{t("navigation.newRecording")}
|
|
464
443
|
</NavLink>
|
|
465
444
|
</Button>
|
|
466
|
-
<
|
|
467
|
-
|
|
445
|
+
<ImportMenu
|
|
446
|
+
uploadHref="/record?autoUpload=1"
|
|
447
|
+
importLoomHref="/import"
|
|
468
448
|
size="sm"
|
|
469
449
|
variant="ghost"
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
<NavLink to="/record?autoUpload=1">
|
|
473
|
-
<IconUpload className="h-4 w-4" />
|
|
474
|
-
{t("preRecord.uploadVideo")}
|
|
475
|
-
</NavLink>
|
|
476
|
-
</Button>
|
|
477
|
-
<Button
|
|
478
|
-
className="mt-1.5 w-full gap-1.5 text-muted-foreground"
|
|
479
|
-
size="sm"
|
|
480
|
-
variant="ghost"
|
|
481
|
-
asChild
|
|
482
|
-
>
|
|
483
|
-
<NavLink to="/import">
|
|
484
|
-
<IconLink className="h-4 w-4" />
|
|
485
|
-
{t("preRecord.importLoom")}
|
|
486
|
-
</NavLink>
|
|
487
|
-
</Button>
|
|
450
|
+
className="mt-1.5 w-full"
|
|
451
|
+
/>
|
|
488
452
|
</div>
|
|
489
453
|
|
|
490
454
|
<nav className="mt-3 space-y-0.5 px-2">
|
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
DropdownMenuItem,
|
|
8
8
|
DropdownMenuTrigger,
|
|
9
9
|
} from "@/components/ui/dropdown-menu";
|
|
10
|
+
import {
|
|
11
|
+
Tooltip,
|
|
12
|
+
TooltipContent,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
} from "@/components/ui/tooltip";
|
|
10
15
|
|
|
11
16
|
export type SortKey = "recent" | "views" | "oldest";
|
|
12
17
|
|
|
@@ -24,12 +29,21 @@ interface SortMenuProps {
|
|
|
24
29
|
export function SortMenu({ value, onChange }: SortMenuProps) {
|
|
25
30
|
return (
|
|
26
31
|
<DropdownMenu>
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
<Tooltip>
|
|
33
|
+
<TooltipTrigger asChild>
|
|
34
|
+
<DropdownMenuTrigger asChild>
|
|
35
|
+
<Button
|
|
36
|
+
variant="outline"
|
|
37
|
+
size="icon"
|
|
38
|
+
className="h-9 w-9"
|
|
39
|
+
aria-label={LABELS[value]}
|
|
40
|
+
>
|
|
41
|
+
<IconArrowsSort className="h-3.5 w-3.5" />
|
|
42
|
+
</Button>
|
|
43
|
+
</DropdownMenuTrigger>
|
|
44
|
+
</TooltipTrigger>
|
|
45
|
+
<TooltipContent>{LABELS[value]}</TooltipContent>
|
|
46
|
+
</Tooltip>
|
|
33
47
|
<DropdownMenuContent align="end" className="w-44">
|
|
34
48
|
{(Object.keys(LABELS) as SortKey[]).map((key) => (
|
|
35
49
|
<DropdownMenuItem
|
|
@@ -7,16 +7,14 @@ import {
|
|
|
7
7
|
IconChevronDown,
|
|
8
8
|
IconDeviceDesktop,
|
|
9
9
|
IconDeviceScreen,
|
|
10
|
-
IconLink,
|
|
11
10
|
IconMicrophone,
|
|
12
11
|
IconPlayerRecord,
|
|
13
|
-
IconUpload,
|
|
14
12
|
IconVideo,
|
|
15
13
|
} from "@tabler/icons-react";
|
|
16
14
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
17
|
-
import { Link } from "react-router";
|
|
18
15
|
|
|
19
16
|
import { CaptureInstallInlineLink } from "@/components/capture-install-options";
|
|
17
|
+
import { ImportMenu } from "@/components/import-menu";
|
|
20
18
|
import { Button } from "@/components/ui/button";
|
|
21
19
|
import {
|
|
22
20
|
Collapsible,
|
|
@@ -66,15 +64,14 @@ export interface PreRecordPanelProps {
|
|
|
66
64
|
initialDisplaySurface?: DisplaySurface | null;
|
|
67
65
|
/** Called when the user picks a local video file to upload. */
|
|
68
66
|
onUpload?: (file: File) => void;
|
|
69
|
-
/** When set,
|
|
70
|
-
* Loom-import page instead of an inline import form. */
|
|
67
|
+
/** When set, includes an "Import Loom" option in the shared import menu. */
|
|
71
68
|
importLoomHref?: string;
|
|
72
69
|
onCancel?: () => void;
|
|
73
70
|
busy?: boolean;
|
|
74
71
|
cameraSize?: CameraBubbleSize;
|
|
75
72
|
onCameraSizeChange?: (size: CameraBubbleSize) => void;
|
|
76
73
|
/** Opens the file picker once on mount, e.g. when arriving from a
|
|
77
|
-
* dedicated
|
|
74
|
+
* dedicated upload entry point elsewhere in the app. */
|
|
78
75
|
autoOpenUpload?: boolean;
|
|
79
76
|
}
|
|
80
77
|
|
|
@@ -924,34 +921,13 @@ export function PreRecordPanel({
|
|
|
924
921
|
|
|
925
922
|
{(onUpload || importLoomHref) && (
|
|
926
923
|
<>
|
|
927
|
-
<
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
<button
|
|
935
|
-
type="button"
|
|
936
|
-
disabled={busy}
|
|
937
|
-
onClick={() => fileInputRef.current?.click()}
|
|
938
|
-
className="flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60"
|
|
939
|
-
>
|
|
940
|
-
<IconUpload className="h-4 w-4" />
|
|
941
|
-
{t("preRecord.uploadVideo")}
|
|
942
|
-
</button>
|
|
943
|
-
) : null}
|
|
944
|
-
|
|
945
|
-
{importLoomHref ? (
|
|
946
|
-
<Link
|
|
947
|
-
to={importLoomHref}
|
|
948
|
-
className="flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
949
|
-
>
|
|
950
|
-
<IconLink className="h-4 w-4" />
|
|
951
|
-
{t("preRecord.importLoom")}
|
|
952
|
-
</Link>
|
|
953
|
-
) : null}
|
|
954
|
-
</div>
|
|
924
|
+
<ImportMenu
|
|
925
|
+
onUpload={
|
|
926
|
+
onUpload ? () => fileInputRef.current?.click() : undefined
|
|
927
|
+
}
|
|
928
|
+
importLoomHref={importLoomHref}
|
|
929
|
+
disabled={busy}
|
|
930
|
+
/>
|
|
955
931
|
|
|
956
932
|
{onUpload ? (
|
|
957
933
|
<input
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
-
import { IconPlayerRecord } from "@tabler/icons-react";
|
|
3
2
|
import { NavLink } from "react-router";
|
|
4
3
|
|
|
5
4
|
import { LibraryGrid } from "@/components/library/library-grid";
|
|
@@ -38,7 +37,6 @@ export default function LibraryIndexRoute() {
|
|
|
38
37
|
asChild
|
|
39
38
|
>
|
|
40
39
|
<NavLink to="/record" aria-label={t("navigation.newRecording")}>
|
|
41
|
-
<IconPlayerRecord className="h-4 w-4" />
|
|
42
40
|
<span className="hidden sm:inline">
|
|
43
41
|
{t("navigation.newRecording")}
|
|
44
42
|
</span>
|
|
@@ -1399,6 +1399,9 @@ export async function deleteDatabaseDataForDocument(
|
|
|
1399
1399
|
db,
|
|
1400
1400
|
);
|
|
1401
1401
|
if (database) {
|
|
1402
|
+
await db
|
|
1403
|
+
.delete(schema.contentDatabaseItemKeyClaims)
|
|
1404
|
+
.where(eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id));
|
|
1402
1405
|
const definitions = await db
|
|
1403
1406
|
.select({ id: schema.documentPropertyDefinitions.id })
|
|
1404
1407
|
.from(schema.documentPropertyDefinitions)
|
|
@@ -1464,6 +1467,9 @@ export async function deleteDatabaseDataForDocument(
|
|
|
1464
1467
|
db,
|
|
1465
1468
|
);
|
|
1466
1469
|
if (item) {
|
|
1470
|
+
await db
|
|
1471
|
+
.delete(schema.contentDatabaseItemKeyClaims)
|
|
1472
|
+
.where(eq(schema.contentDatabaseItemKeyClaims.documentId, documentId));
|
|
1467
1473
|
await db
|
|
1468
1474
|
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
1469
1475
|
.where(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
3
|
-
import { and, eq, inArray, ne } from "drizzle-orm";
|
|
3
|
+
import { and, eq, inArray, isNull, ne, sql } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -8,7 +8,10 @@ import type {
|
|
|
8
8
|
BindContentDatabaseSourceFieldRequest,
|
|
9
9
|
ContentDatabaseResponse,
|
|
10
10
|
} from "../shared/api.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
serializePropertyValue,
|
|
13
|
+
type DocumentPropertyType,
|
|
14
|
+
} from "../shared/properties.js";
|
|
12
15
|
import { chunks } from "./_batch-utils.js";
|
|
13
16
|
import { resolveDatabaseForSourceMutation } from "./_database-source-utils.js";
|
|
14
17
|
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
@@ -71,40 +74,92 @@ export default defineAction({
|
|
|
71
74
|
|
|
72
75
|
// ── Unbind ────────────────────────────────────────────────────────────
|
|
73
76
|
if (args.propertyId === null) {
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
.where(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
await db.transaction(async (tx) => {
|
|
78
|
+
const [lockedDatabase] = await tx
|
|
79
|
+
.update(schema.contentDatabases)
|
|
80
|
+
.set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
|
|
81
|
+
.where(
|
|
82
|
+
and(
|
|
83
|
+
eq(schema.contentDatabases.id, database.id),
|
|
84
|
+
eq(schema.contentDatabases.ownerEmail, database.ownerEmail),
|
|
85
|
+
isNull(schema.contentDatabases.deletedAt),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
.returning({ id: schema.contentDatabases.id });
|
|
89
|
+
if (!lockedDatabase) throw new Error("Database is no longer active.");
|
|
90
|
+
|
|
91
|
+
const [lockedField] = await tx
|
|
92
|
+
.select()
|
|
93
|
+
.from(schema.contentDatabaseSourceFields)
|
|
94
|
+
.where(eq(schema.contentDatabaseSourceFields.id, field.id));
|
|
95
|
+
if (
|
|
96
|
+
!lockedField ||
|
|
97
|
+
lockedField.sourceId !== source.id ||
|
|
98
|
+
lockedField.mappingType === "title" ||
|
|
99
|
+
lockedField.mappingType === "system" ||
|
|
100
|
+
lockedField.writeOwner === "derived"
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"Source field changed or was deleted before it could be unbound.",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (lockedField.propertyId) {
|
|
107
|
+
const sourceRows = await tx
|
|
108
|
+
.select({
|
|
109
|
+
documentId: schema.contentDatabaseSourceRows.documentId,
|
|
110
|
+
})
|
|
111
|
+
.from(schema.contentDatabaseSourceRows)
|
|
112
|
+
.where(eq(schema.contentDatabaseSourceRows.sourceId, source.id));
|
|
113
|
+
const sourceDocumentIds = sourceRows
|
|
114
|
+
.map((row) => row.documentId)
|
|
115
|
+
.filter((id): id is string => Boolean(id));
|
|
116
|
+
if (sourceDocumentIds.length > 0) {
|
|
117
|
+
await tx
|
|
118
|
+
.delete(schema.documentPropertyValues)
|
|
119
|
+
.where(
|
|
120
|
+
and(
|
|
121
|
+
eq(
|
|
122
|
+
schema.documentPropertyValues.propertyId,
|
|
123
|
+
lockedField.propertyId,
|
|
124
|
+
),
|
|
125
|
+
inArray(
|
|
126
|
+
schema.documentPropertyValues.documentId,
|
|
127
|
+
sourceDocumentIds,
|
|
128
|
+
),
|
|
91
129
|
),
|
|
92
|
-
)
|
|
93
|
-
|
|
130
|
+
);
|
|
131
|
+
}
|
|
94
132
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
133
|
+
const [updatedField] = await tx
|
|
134
|
+
.update(schema.contentDatabaseSourceFields)
|
|
135
|
+
.set({
|
|
136
|
+
propertyId: null,
|
|
137
|
+
localFieldKey: lockedField.sourceFieldKey,
|
|
138
|
+
updatedAt: now,
|
|
139
|
+
})
|
|
140
|
+
.where(eq(schema.contentDatabaseSourceFields.id, lockedField.id))
|
|
141
|
+
.returning({ id: schema.contentDatabaseSourceFields.id });
|
|
142
|
+
if (!updatedField) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
"Source field was deleted before its unbinding could be saved.",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const [updatedSource] = await tx
|
|
148
|
+
.update(schema.contentDatabaseSources)
|
|
149
|
+
.set({ updatedAt: now })
|
|
150
|
+
.where(
|
|
151
|
+
and(
|
|
152
|
+
eq(schema.contentDatabaseSources.id, source.id),
|
|
153
|
+
eq(schema.contentDatabaseSources.databaseId, database.id),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
.returning({ id: schema.contentDatabaseSources.id });
|
|
157
|
+
if (!updatedSource) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"Source was deleted before its field unbinding could be saved.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
108
163
|
return getContentDatabaseResponse(database.id, { limit: 100, offset: 0 });
|
|
109
164
|
}
|
|
110
165
|
|
|
@@ -183,23 +238,6 @@ export default defineAction({
|
|
|
183
238
|
);
|
|
184
239
|
}
|
|
185
240
|
|
|
186
|
-
await db
|
|
187
|
-
.update(schema.contentDatabaseSourceFields)
|
|
188
|
-
.set({
|
|
189
|
-
propertyId: property.id,
|
|
190
|
-
localFieldKey: property.id,
|
|
191
|
-
mappingType: "property",
|
|
192
|
-
updatedAt: now,
|
|
193
|
-
})
|
|
194
|
-
.where(eq(schema.contentDatabaseSourceFields.id, field.id));
|
|
195
|
-
await db
|
|
196
|
-
.update(schema.contentDatabaseSources)
|
|
197
|
-
.set({ updatedAt: now })
|
|
198
|
-
.where(eq(schema.contentDatabaseSources.id, source.id));
|
|
199
|
-
|
|
200
|
-
// Backfill the column with this source's per-row values. A federated
|
|
201
|
-
// secondary's rows carry no local document (the read path overlays them),
|
|
202
|
-
// so only materialize for document-backed sources.
|
|
203
241
|
let federationRole: string | null = null;
|
|
204
242
|
try {
|
|
205
243
|
const parsed = JSON.parse(source.metadataJson ?? "{}") as {
|
|
@@ -209,57 +247,195 @@ export default defineAction({
|
|
|
209
247
|
} catch {
|
|
210
248
|
federationRole = null;
|
|
211
249
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
250
|
+
await db.transaction(async (tx) => {
|
|
251
|
+
// Share the database-row lock used by stable-key upserts. This makes the
|
|
252
|
+
// claim check and source binding one atomic ownership transition: either
|
|
253
|
+
// the property remains caller-managed, or binding fails before backfill.
|
|
254
|
+
const [lockedDatabase] = await tx
|
|
255
|
+
.update(schema.contentDatabases)
|
|
256
|
+
.set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
|
|
257
|
+
.where(
|
|
258
|
+
and(
|
|
259
|
+
eq(schema.contentDatabases.id, database.id),
|
|
260
|
+
eq(schema.contentDatabases.documentId, database.documentId),
|
|
261
|
+
eq(schema.contentDatabases.ownerEmail, database.ownerEmail),
|
|
262
|
+
isNull(schema.contentDatabases.deletedAt),
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
.returning({ id: schema.contentDatabases.id });
|
|
266
|
+
if (!lockedDatabase) throw new Error("Database is no longer active.");
|
|
267
|
+
|
|
268
|
+
const [lockedProperty] = await tx
|
|
269
|
+
.update(schema.documentPropertyDefinitions)
|
|
270
|
+
.set({
|
|
271
|
+
updatedAt: sql`${schema.documentPropertyDefinitions.updatedAt}`,
|
|
218
272
|
})
|
|
219
|
-
.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// Clear this column's values for ALL of this source's rows first — not
|
|
227
|
-
// just the rows that now have a value — so a row whose new bound field is
|
|
228
|
-
// empty doesn't keep showing a stale/previous value. Then write the
|
|
229
|
-
// non-empty ones. (This source owns these documents' values for the row-
|
|
230
|
-
// union, so clearing them is safe.)
|
|
231
|
-
const sourceDocumentIds = sourceRows
|
|
232
|
-
.map((row) => row.documentId)
|
|
233
|
-
.filter((id): id is string => Boolean(id));
|
|
234
|
-
if (sourceDocumentIds.length > 0) {
|
|
235
|
-
await db
|
|
236
|
-
.delete(schema.documentPropertyValues)
|
|
237
|
-
.where(
|
|
238
|
-
and(
|
|
239
|
-
eq(schema.documentPropertyValues.propertyId, property.id),
|
|
240
|
-
inArray(
|
|
241
|
-
schema.documentPropertyValues.documentId,
|
|
242
|
-
sourceDocumentIds,
|
|
243
|
-
),
|
|
273
|
+
.where(
|
|
274
|
+
and(
|
|
275
|
+
eq(schema.documentPropertyDefinitions.id, property.id),
|
|
276
|
+
eq(schema.documentPropertyDefinitions.databaseId, database.id),
|
|
277
|
+
eq(
|
|
278
|
+
schema.documentPropertyDefinitions.ownerEmail,
|
|
279
|
+
database.ownerEmail,
|
|
244
280
|
),
|
|
245
|
-
)
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
.returning();
|
|
284
|
+
if (
|
|
285
|
+
!lockedProperty ||
|
|
286
|
+
lockedProperty.type !== property.type ||
|
|
287
|
+
lockedProperty.systemRole !== property.systemRole ||
|
|
288
|
+
lockedProperty.name !== property.name
|
|
289
|
+
) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
"Target column changed or was deleted before the source field could be bound.",
|
|
292
|
+
);
|
|
246
293
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
294
|
+
|
|
295
|
+
const [lockedField] = await tx
|
|
296
|
+
.select()
|
|
297
|
+
.from(schema.contentDatabaseSourceFields)
|
|
298
|
+
.where(eq(schema.contentDatabaseSourceFields.id, field.id));
|
|
299
|
+
if (
|
|
300
|
+
!lockedField ||
|
|
301
|
+
lockedField.sourceId !== source.id ||
|
|
302
|
+
lockedField.mappingType === "title" ||
|
|
303
|
+
lockedField.mappingType === "system" ||
|
|
304
|
+
lockedField.writeOwner === "derived"
|
|
305
|
+
) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
"Source field changed or was deleted before it could be bound.",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (
|
|
311
|
+
lockedField.propertyId &&
|
|
312
|
+
lockedField.propertyId !== lockedProperty.id
|
|
313
|
+
) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
"This source field is already bound to another column. Unbind it first.",
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const [lockedConflictingField] = await tx
|
|
319
|
+
.select({ id: schema.contentDatabaseSourceFields.id })
|
|
320
|
+
.from(schema.contentDatabaseSourceFields)
|
|
321
|
+
.where(
|
|
322
|
+
and(
|
|
323
|
+
eq(schema.contentDatabaseSourceFields.sourceId, source.id),
|
|
324
|
+
eq(
|
|
325
|
+
schema.contentDatabaseSourceFields.propertyId,
|
|
326
|
+
lockedProperty.id,
|
|
327
|
+
),
|
|
328
|
+
ne(schema.contentDatabaseSourceFields.id, lockedField.id),
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
if (lockedConflictingField) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
"This source already feeds this column from another field. Unbind it first.",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const [activeClaim] = await tx
|
|
338
|
+
.select({ id: schema.contentDatabaseItemKeyClaims.id })
|
|
339
|
+
.from(schema.contentDatabaseItemKeyClaims)
|
|
340
|
+
.where(
|
|
341
|
+
and(
|
|
342
|
+
eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id),
|
|
343
|
+
eq(schema.contentDatabaseItemKeyClaims.propertyId, property.id),
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
.limit(1);
|
|
347
|
+
if (activeClaim) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
"A property with active stable-key claims cannot be bound to a source field.",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const [updatedField] = await tx
|
|
354
|
+
.update(schema.contentDatabaseSourceFields)
|
|
355
|
+
.set({
|
|
356
|
+
propertyId: property.id,
|
|
357
|
+
localFieldKey: property.id,
|
|
358
|
+
mappingType: "property",
|
|
359
|
+
updatedAt: now,
|
|
360
|
+
})
|
|
361
|
+
.where(eq(schema.contentDatabaseSourceFields.id, lockedField.id))
|
|
362
|
+
.returning({ id: schema.contentDatabaseSourceFields.id });
|
|
363
|
+
if (!updatedField) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
"Source field was deleted before its binding could be saved.",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
const [updatedSource] = await tx
|
|
369
|
+
.update(schema.contentDatabaseSources)
|
|
370
|
+
.set({ updatedAt: now })
|
|
371
|
+
.where(
|
|
372
|
+
and(
|
|
373
|
+
eq(schema.contentDatabaseSources.id, source.id),
|
|
374
|
+
eq(schema.contentDatabaseSources.databaseId, database.id),
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
.returning({ id: schema.contentDatabaseSources.id });
|
|
378
|
+
if (!updatedSource) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
"Source was deleted before its field binding could be saved.",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Backfill the column with this source's per-row values. A federated
|
|
385
|
+
// secondary's rows carry no local document (the read path overlays them),
|
|
386
|
+
// so only materialize for document-backed sources.
|
|
387
|
+
if (federationRole !== "secondary") {
|
|
388
|
+
const sourceRows = await tx
|
|
389
|
+
.select({
|
|
390
|
+
databaseItemId: schema.contentDatabaseSourceRows.databaseItemId,
|
|
391
|
+
documentId: schema.contentDatabaseSourceRows.documentId,
|
|
392
|
+
sourceValuesJson: schema.contentDatabaseSourceRows.sourceValuesJson,
|
|
393
|
+
})
|
|
394
|
+
.from(schema.contentDatabaseSourceRows)
|
|
395
|
+
.where(eq(schema.contentDatabaseSourceRows.sourceId, source.id));
|
|
396
|
+
const itemValues = sourceFieldPropertyValuesFromRows(
|
|
397
|
+
sourceRows,
|
|
398
|
+
lockedField.sourceFieldKey,
|
|
399
|
+
lockedProperty.type as DocumentPropertyType,
|
|
400
|
+
);
|
|
401
|
+
// Clear this column's values for ALL of this source's rows first — not
|
|
402
|
+
// just the rows that now have a value — so a row whose new bound field is
|
|
403
|
+
// empty doesn't keep showing a stale/previous value. Then write the
|
|
404
|
+
// non-empty ones. (This source owns these documents' values for the row-
|
|
405
|
+
// union, so clearing them is safe.)
|
|
406
|
+
const sourceDocumentIds = sourceRows
|
|
407
|
+
.map((row) => row.documentId)
|
|
408
|
+
.filter((id): id is string => Boolean(id));
|
|
409
|
+
if (sourceDocumentIds.length > 0) {
|
|
410
|
+
await tx
|
|
411
|
+
.delete(schema.documentPropertyValues)
|
|
412
|
+
.where(
|
|
413
|
+
and(
|
|
414
|
+
eq(schema.documentPropertyValues.propertyId, property.id),
|
|
415
|
+
inArray(
|
|
416
|
+
schema.documentPropertyValues.documentId,
|
|
417
|
+
sourceDocumentIds,
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (itemValues.length > 0) {
|
|
423
|
+
for (const chunk of chunks(itemValues, 200)) {
|
|
424
|
+
await tx.insert(schema.documentPropertyValues).values(
|
|
425
|
+
chunk.map((row) => ({
|
|
426
|
+
id: nanoid(),
|
|
427
|
+
ownerEmail: database.ownerEmail,
|
|
428
|
+
documentId: row.documentId,
|
|
429
|
+
propertyId: property.id,
|
|
430
|
+
valueJson: serializePropertyValue(row.value),
|
|
431
|
+
createdAt: now,
|
|
432
|
+
updatedAt: now,
|
|
433
|
+
})),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
260
436
|
}
|
|
261
437
|
}
|
|
262
|
-
}
|
|
438
|
+
});
|
|
263
439
|
|
|
264
440
|
return getContentDatabaseResponse(database.id, { limit: 100, offset: 0 });
|
|
265
441
|
},
|