@agent-native/core 0.114.15 → 0.114.16

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.
@@ -5,6 +5,8 @@ import { existsSync, statSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
+ import { shouldUseSourceFallback } from "./launcher.js";
9
+
8
10
  const binDir = dirname(fileURLToPath(import.meta.url));
9
11
  const distEntry = join(binDir, "../dist/cli/index.js");
10
12
  const sourceEntry = join(binDir, "../src/cli/index.ts");
@@ -16,31 +18,50 @@ const freshnessChecks = [
16
18
  ],
17
19
  ];
18
20
 
19
- function shouldUseSourceFallback() {
20
- if (!existsSync(sourceEntry)) return false;
21
- if (!existsSync(distEntry)) return true;
21
+ // The tsx source fallback and mtime freshness check are for local monorepo
22
+ // development only. Published installs ship both src and dist, and tarball
23
+ // extraction can leave .ts files newer than .js, which must not trigger tsx
24
+ // (not a runtime dependency). Only consider the fallback in a source checkout.
25
+ const isSourceCheckout = existsSync(join(binDir, "../tsconfig.cli.json"));
26
+
27
+ function statMtimeMs(path) {
22
28
  try {
23
- return freshnessChecks.some(
24
- ([source, dist]) =>
25
- existsSync(source) &&
26
- existsSync(dist) &&
27
- statSync(source).mtimeMs > statSync(dist).mtimeMs,
28
- );
29
+ return statSync(path).mtimeMs;
29
30
  } catch {
30
- return false;
31
+ return 0;
31
32
  }
32
33
  }
33
34
 
34
- if (!shouldUseSourceFallback() && existsSync(distEntry)) {
35
- await import(pathToFileURL(distEntry).href);
36
- } else {
37
- if (!existsSync(sourceEntry)) {
35
+ const freshness = freshnessChecks.map(([source, dist]) => {
36
+ const sourceExists = existsSync(source);
37
+ const distExists = existsSync(dist);
38
+ return {
39
+ sourceExists,
40
+ distExists,
41
+ sourceMtimeMs: sourceExists ? statMtimeMs(source) : 0,
42
+ distMtimeMs: distExists ? statMtimeMs(dist) : 0,
43
+ };
44
+ });
45
+
46
+ const useSourceFallback = shouldUseSourceFallback({
47
+ isSourceCheckout,
48
+ sourceEntryExists: existsSync(sourceEntry),
49
+ distEntryExists: existsSync(distEntry),
50
+ freshness,
51
+ });
52
+
53
+ if (!useSourceFallback) {
54
+ // Installed packages (and up-to-date checkouts) always run the shipped build.
55
+ // tsx is not a runtime dependency, so it must never be invoked here.
56
+ if (!existsSync(distEntry)) {
38
57
  console.error(
39
58
  "agent-native CLI build output is missing. Run `pnpm --filter @agent-native/core build` and try again.",
40
59
  );
41
60
  process.exit(1);
42
61
  }
43
62
 
63
+ await import(pathToFileURL(distEntry).href);
64
+ } else {
44
65
  const child = spawn("tsx", [sourceEntry, ...process.argv.slice(2)], {
45
66
  stdio: "inherit",
46
67
  env: process.env,
@@ -0,0 +1,34 @@
1
+ // Pure decision logic for the agent-native CLI launcher shim, kept dependency
2
+ // free so it can be unit tested without touching the filesystem.
3
+ //
4
+ // The tsx source fallback and mtime freshness check exist for local monorepo
5
+ // development only. Published installs ship both src and dist, and tarball
6
+ // extraction can leave .ts files newer than .js — that must never route to tsx
7
+ // (not a runtime dependency), or `npx @agent-native/core ...` fails with
8
+ // `spawn tsx ENOENT`. The `isSourceCheckout` gate is what keeps installed
9
+ // packages on the shipped dist build.
10
+
11
+ /**
12
+ * @param {object} input
13
+ * @param {boolean} input.isSourceCheckout Whether we run from a monorepo checkout.
14
+ * @param {boolean} input.sourceEntryExists Whether the .ts CLI entry exists.
15
+ * @param {boolean} input.distEntryExists Whether the compiled .js CLI entry exists.
16
+ * @param {Array<{sourceExists: boolean, distExists: boolean, sourceMtimeMs: number, distMtimeMs: number}>} [input.freshness]
17
+ * @returns {boolean}
18
+ */
19
+ export function shouldUseSourceFallback({
20
+ isSourceCheckout,
21
+ sourceEntryExists,
22
+ distEntryExists,
23
+ freshness = [],
24
+ }) {
25
+ if (!isSourceCheckout) return false;
26
+ if (!sourceEntryExists) return false;
27
+ if (!distEntryExists) return true;
28
+ return freshness.some(
29
+ (pair) =>
30
+ pair.sourceExists &&
31
+ pair.distExists &&
32
+ pair.sourceMtimeMs > pair.distMtimeMs,
33
+ );
34
+ }
package/corpus/README.md CHANGED
@@ -28,6 +28,6 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
 
29
29
  ## Generated Counts
30
30
 
31
- - core files: 1520
31
+ - core files: 1521
32
32
  - toolkit files: 137
33
33
  - template files: 6194
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.114.16
4
+
5
+ ### Patch Changes
6
+
7
+ - 3a3eb9a: Fix `npx @agent-native/core create` failing with `spawn tsx ENOENT`. The CLI launcher's tsx source fallback is now scoped to monorepo source checkouts, so installed packages always run the shipped `dist` build even when tarball extraction leaves `.ts` files newer than their compiled `.js`.
8
+
3
9
  ## 0.114.15
4
10
 
5
11
  ### Patch Changes
@@ -5,6 +5,8 @@ import { existsSync, statSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
+ import { shouldUseSourceFallback } from "./launcher.js";
9
+
8
10
  const binDir = dirname(fileURLToPath(import.meta.url));
9
11
  const distEntry = join(binDir, "../dist/cli/index.js");
10
12
  const sourceEntry = join(binDir, "../src/cli/index.ts");
@@ -16,31 +18,50 @@ const freshnessChecks = [
16
18
  ],
17
19
  ];
18
20
 
19
- function shouldUseSourceFallback() {
20
- if (!existsSync(sourceEntry)) return false;
21
- if (!existsSync(distEntry)) return true;
21
+ // The tsx source fallback and mtime freshness check are for local monorepo
22
+ // development only. Published installs ship both src and dist, and tarball
23
+ // extraction can leave .ts files newer than .js, which must not trigger tsx
24
+ // (not a runtime dependency). Only consider the fallback in a source checkout.
25
+ const isSourceCheckout = existsSync(join(binDir, "../tsconfig.cli.json"));
26
+
27
+ function statMtimeMs(path) {
22
28
  try {
23
- return freshnessChecks.some(
24
- ([source, dist]) =>
25
- existsSync(source) &&
26
- existsSync(dist) &&
27
- statSync(source).mtimeMs > statSync(dist).mtimeMs,
28
- );
29
+ return statSync(path).mtimeMs;
29
30
  } catch {
30
- return false;
31
+ return 0;
31
32
  }
32
33
  }
33
34
 
34
- if (!shouldUseSourceFallback() && existsSync(distEntry)) {
35
- await import(pathToFileURL(distEntry).href);
36
- } else {
37
- if (!existsSync(sourceEntry)) {
35
+ const freshness = freshnessChecks.map(([source, dist]) => {
36
+ const sourceExists = existsSync(source);
37
+ const distExists = existsSync(dist);
38
+ return {
39
+ sourceExists,
40
+ distExists,
41
+ sourceMtimeMs: sourceExists ? statMtimeMs(source) : 0,
42
+ distMtimeMs: distExists ? statMtimeMs(dist) : 0,
43
+ };
44
+ });
45
+
46
+ const useSourceFallback = shouldUseSourceFallback({
47
+ isSourceCheckout,
48
+ sourceEntryExists: existsSync(sourceEntry),
49
+ distEntryExists: existsSync(distEntry),
50
+ freshness,
51
+ });
52
+
53
+ if (!useSourceFallback) {
54
+ // Installed packages (and up-to-date checkouts) always run the shipped build.
55
+ // tsx is not a runtime dependency, so it must never be invoked here.
56
+ if (!existsSync(distEntry)) {
38
57
  console.error(
39
58
  "agent-native CLI build output is missing. Run `pnpm --filter @agent-native/core build` and try again.",
40
59
  );
41
60
  process.exit(1);
42
61
  }
43
62
 
63
+ await import(pathToFileURL(distEntry).href);
64
+ } else {
44
65
  const child = spawn("tsx", [sourceEntry, ...process.argv.slice(2)], {
45
66
  stdio: "inherit",
46
67
  env: process.env,
@@ -0,0 +1,34 @@
1
+ // Pure decision logic for the agent-native CLI launcher shim, kept dependency
2
+ // free so it can be unit tested without touching the filesystem.
3
+ //
4
+ // The tsx source fallback and mtime freshness check exist for local monorepo
5
+ // development only. Published installs ship both src and dist, and tarball
6
+ // extraction can leave .ts files newer than .js — that must never route to tsx
7
+ // (not a runtime dependency), or `npx @agent-native/core ...` fails with
8
+ // `spawn tsx ENOENT`. The `isSourceCheckout` gate is what keeps installed
9
+ // packages on the shipped dist build.
10
+
11
+ /**
12
+ * @param {object} input
13
+ * @param {boolean} input.isSourceCheckout Whether we run from a monorepo checkout.
14
+ * @param {boolean} input.sourceEntryExists Whether the .ts CLI entry exists.
15
+ * @param {boolean} input.distEntryExists Whether the compiled .js CLI entry exists.
16
+ * @param {Array<{sourceExists: boolean, distExists: boolean, sourceMtimeMs: number, distMtimeMs: number}>} [input.freshness]
17
+ * @returns {boolean}
18
+ */
19
+ export function shouldUseSourceFallback({
20
+ isSourceCheckout,
21
+ sourceEntryExists,
22
+ distEntryExists,
23
+ freshness = [],
24
+ }) {
25
+ if (!isSourceCheckout) return false;
26
+ if (!sourceEntryExists) return false;
27
+ if (!distEntryExists) return true;
28
+ return freshness.some(
29
+ (pair) =>
30
+ pair.sourceExists &&
31
+ pair.distExists &&
32
+ pair.sourceMtimeMs > pair.distMtimeMs,
33
+ );
34
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.114.15",
3
+ "version": "0.114.16",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -27,6 +27,8 @@ export interface BulkMoveTarget {
27
27
 
28
28
  interface BulkActionToolbarProps {
29
29
  count: number;
30
+ allSelected?: boolean;
31
+ onSelectAll?: () => void;
30
32
  onArchive?: () => void;
31
33
  onMove?: (folderId: string | null) => void;
32
34
  onTrash?: () => void;
@@ -38,6 +40,8 @@ interface BulkActionToolbarProps {
38
40
 
39
41
  export function BulkActionToolbar({
40
42
  count,
43
+ allSelected = false,
44
+ onSelectAll,
41
45
  onArchive,
42
46
  onMove,
43
47
  onTrash,
@@ -53,17 +57,30 @@ export function BulkActionToolbar({
53
57
  return (
54
58
  <div
55
59
  className={cn(
56
- "flex w-fit max-w-full items-center gap-1 rounded-xl border border-border bg-popover px-3 py-2 shadow-lg",
60
+ "flex w-fit max-w-full items-center gap-1 rounded-xl bg-foreground px-3 py-2 text-background shadow-2xl ring-1 ring-black/10 dark:ring-white/10",
57
61
  )}
58
62
  >
59
- <span className="pe-2 text-xs font-medium text-foreground">
63
+ <span className="pe-2 text-xs font-medium">
60
64
  {t("clipsFinalRaw.selectedCount", { count })}
61
65
  </span>
62
- <div className="h-4 w-px bg-border" />
66
+ {onSelectAll && (
67
+ <Button
68
+ variant="ghost"
69
+ size="sm"
70
+ className="h-8 gap-1.5 text-background hover:bg-background/15 hover:text-background"
71
+ onClick={onSelectAll}
72
+ disabled={isPending}
73
+ >
74
+ {allSelected
75
+ ? t("clipsFinalRaw.deselectAll")
76
+ : t("clipsFinalRaw.selectAll")}
77
+ </Button>
78
+ )}
79
+ <div className="h-4 w-px bg-background/20" />
63
80
  <Button
64
81
  variant="ghost"
65
82
  size="sm"
66
- className="h-8 gap-1.5"
83
+ className="h-8 gap-1.5 text-background hover:bg-background/15 hover:text-background"
67
84
  onClick={onArchive}
68
85
  disabled={isPending}
69
86
  >
@@ -75,7 +92,7 @@ export function BulkActionToolbar({
75
92
  <Button
76
93
  variant="ghost"
77
94
  size="sm"
78
- className="h-8 gap-1.5"
95
+ className="h-8 gap-1.5 text-background hover:bg-background/15 hover:text-background"
79
96
  disabled={isPending}
80
97
  >
81
98
  <IconFolder className="h-3.5 w-3.5" /> {t("clipsFinalRaw.move")}
@@ -121,17 +138,17 @@ export function BulkActionToolbar({
121
138
  <Button
122
139
  variant="ghost"
123
140
  size="sm"
124
- className="h-8 gap-1.5 text-destructive hover:text-destructive"
141
+ className="h-8 gap-1.5 text-red-400 hover:bg-background/15 hover:text-red-400"
125
142
  onClick={onTrash}
126
143
  disabled={isPending}
127
144
  >
128
145
  <IconTrash className="h-3.5 w-3.5" /> {t("navigation.trash")}
129
146
  </Button>
130
- <div className="h-4 w-px bg-border mx-1" />
147
+ <div className="mx-1 h-4 w-px bg-background/20" />
131
148
  <button
132
149
  type="button"
133
150
  onClick={onClear}
134
- className="rounded p-1 text-muted-foreground hover:bg-accent"
151
+ className="rounded p-1 text-background/70 hover:bg-background/15 hover:text-background"
135
152
  aria-label={t("clipsFinalRaw.clearSelection")}
136
153
  >
137
154
  <IconX className="h-3.5 w-3.5" />
@@ -3,8 +3,12 @@ import {
3
3
  setClientAppState,
4
4
  } from "@agent-native/core/client/hooks";
5
5
  import { useT } from "@agent-native/core/client/i18n";
6
- import { IconAlertTriangle } from "@tabler/icons-react";
7
- import { useEffect, useMemo, useState } from "react";
6
+ import {
7
+ IconAlertTriangle,
8
+ IconChevronLeft,
9
+ IconChevronRight,
10
+ } from "@tabler/icons-react";
11
+ import { useCallback, useEffect, useMemo, useState } from "react";
8
12
  import { toast } from "sonner";
9
13
 
10
14
  import { CreateFolderDialog } from "@/components/library/create-folder-dialog";
@@ -13,6 +17,7 @@ import { Button } from "@/components/ui/button";
13
17
  import {
14
18
  useFolders,
15
19
  useRecordings,
20
+ useRecordingsCount,
16
21
  useTrashRecording,
17
22
  useArchiveRecording,
18
23
  useRestoreRecording,
@@ -54,6 +59,8 @@ function Skeleton() {
54
59
  );
55
60
  }
56
61
 
62
+ const PAGE_SIZE = 100;
63
+
57
64
  interface FolderTargetRow {
58
65
  id: string;
59
66
  parentId: string | null;
@@ -105,13 +112,38 @@ export function LibraryGrid({
105
112
  const t = useT();
106
113
  const [sort, setSort] = useState<SortKey>("recent");
107
114
  const [selected, setSelected] = useState<Set<string>>(new Set());
115
+ const [lastSelectedId, setLastSelectedId] = useState<string | null>(null);
108
116
  const selectionMode = selected.size > 0;
109
117
  const [sharingRec, setSharingRec] = useState<RecordingSummary | null>(null);
110
118
  const [createFolderTarget, setCreateFolderTarget] =
111
119
  useState<CreateFolderTarget | null>(null);
112
120
  const [isBulkPending, setIsBulkPending] = useState(false);
121
+ const [page, setPage] = useState(1);
113
122
  const selectionStateKey = useMemo(() => `selection:${getBrowserTabId()}`, []);
114
123
 
124
+ useEffect(() => {
125
+ setPage(1);
126
+ setSelected(new Set());
127
+ setLastSelectedId(null);
128
+ }, [view, folderId, spaceId, tagFilter, sort]);
129
+
130
+ const countArgs = useMemo(
131
+ () => ({
132
+ view,
133
+ folderId: folderId ?? null,
134
+ spaceId: spaceId ?? null,
135
+ tag: tagFilter ?? null,
136
+ }),
137
+ [view, folderId, spaceId, tagFilter],
138
+ );
139
+ const { data: totalCount } = useRecordingsCount(countArgs);
140
+ const total = totalCount ?? 0;
141
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
142
+
143
+ useEffect(() => {
144
+ if (page > totalPages) setPage(totalPages);
145
+ }, [page, totalPages]);
146
+
115
147
  const args: ListRecordingsArgs = useMemo(
116
148
  () => ({
117
149
  view,
@@ -119,8 +151,10 @@ export function LibraryGrid({
119
151
  spaceId: spaceId ?? null,
120
152
  tag: tagFilter ?? null,
121
153
  sort,
154
+ limit: PAGE_SIZE,
155
+ offset: (page - 1) * PAGE_SIZE,
122
156
  }),
123
- [view, folderId, spaceId, tagFilter, sort],
157
+ [view, folderId, spaceId, tagFilter, sort, page],
124
158
  );
125
159
 
126
160
  const { data, isLoading, isError, refetch, isRefetching } =
@@ -189,17 +223,53 @@ export function LibraryGrid({
189
223
  };
190
224
  }, [selectionStateKey]);
191
225
 
192
- const toggleSelect = (id: string) => {
226
+ const handleToggleSelect = useCallback(
227
+ (id: string, shiftKey = false) => {
228
+ setSelected((prev) => {
229
+ if (shiftKey && lastSelectedId && lastSelectedId !== id) {
230
+ const ids = recordings.map((r) => r.id);
231
+ const fromIndex = ids.indexOf(lastSelectedId);
232
+ const toIndex = ids.indexOf(id);
233
+ if (fromIndex !== -1 && toIndex !== -1) {
234
+ const [start, end] =
235
+ fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex];
236
+ const next = new Set(prev);
237
+ for (let i = start; i <= end; i++) next.add(ids[i]);
238
+ return next;
239
+ }
240
+ }
241
+ const next = new Set(prev);
242
+ if (next.has(id)) next.delete(id);
243
+ else next.add(id);
244
+ return next;
245
+ });
246
+ setLastSelectedId(id);
247
+ },
248
+ [lastSelectedId, recordings],
249
+ );
250
+
251
+ const allSelected =
252
+ recordings.length > 0 && recordings.every(({ id }) => selected.has(id));
253
+
254
+ const toggleSelectAll = useCallback(() => {
193
255
  setSelected((prev) => {
194
256
  const next = new Set(prev);
195
- if (next.has(id)) next.delete(id);
196
- else next.add(id);
257
+ const visibleIds = recordings.map(({ id }) => id);
258
+ const allVisibleSelected = visibleIds.every((id) => prev.has(id));
259
+
260
+ for (const id of visibleIds) {
261
+ if (allVisibleSelected) next.delete(id);
262
+ else next.add(id);
263
+ }
264
+
197
265
  return next;
198
266
  });
199
- };
267
+ setLastSelectedId(null);
268
+ }, [recordings]);
200
269
 
201
270
  const clearSelection = () => {
202
271
  setSelected(new Set());
272
+ setLastSelectedId(null);
203
273
  };
204
274
 
205
275
  const moveRecordings = async (
@@ -321,7 +391,7 @@ export function LibraryGrid({
321
391
  )}
322
392
 
323
393
  {/* Grid body */}
324
- <div className="relative flex min-h-0 flex-1 overflow-hidden">
394
+ <div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
325
395
  <div
326
396
  className={cn(
327
397
  "min-h-0 flex-1 overflow-y-auto",
@@ -371,7 +441,7 @@ export function LibraryGrid({
371
441
  }
372
442
  selectionMode={canManageRecordings && selectionMode}
373
443
  onToggleSelect={
374
- canManageRecordings ? toggleSelect : undefined
444
+ canManageRecordings ? handleToggleSelect : undefined
375
445
  }
376
446
  onShare={(rec) => setSharingRec(rec)}
377
447
  moveTargets={moveTargets}
@@ -426,12 +496,51 @@ export function LibraryGrid({
426
496
  </div>
427
497
  </div>
428
498
 
499
+ {!isLoading && recordings.length > 0 && totalPages > 1 && (
500
+ <div className="flex shrink-0 items-center justify-between gap-3 border-t border-border px-5 py-2.5">
501
+ <span className="text-xs text-muted-foreground">
502
+ {t("libraryGrid.paginationRange", {
503
+ start: (page - 1) * PAGE_SIZE + 1,
504
+ end: (page - 1) * PAGE_SIZE + recordings.length,
505
+ total,
506
+ })}
507
+ </span>
508
+ <div className="flex items-center gap-2">
509
+ <Button
510
+ variant="outline"
511
+ size="sm"
512
+ className="gap-1"
513
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
514
+ disabled={page <= 1}
515
+ >
516
+ <IconChevronLeft className="h-3.5 w-3.5" />
517
+ {t("libraryGrid.paginationPrevious")}
518
+ </Button>
519
+ <span className="text-xs text-muted-foreground">
520
+ {t("libraryGrid.paginationPage", { page, totalPages })}
521
+ </span>
522
+ <Button
523
+ variant="outline"
524
+ size="sm"
525
+ className="gap-1"
526
+ onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
527
+ disabled={page >= totalPages}
528
+ >
529
+ {t("libraryGrid.paginationNext")}
530
+ <IconChevronRight className="h-3.5 w-3.5" />
531
+ </Button>
532
+ </div>
533
+ </div>
534
+ )}
535
+
429
536
  {/* Keep selected-library actions visible while the recording list scrolls. */}
430
537
  {canManageRecordings && selected.size > 0 && (
431
538
  <div className="pointer-events-none absolute inset-x-0 bottom-0 z-30 flex justify-center px-4 pb-4">
432
539
  <div className="pointer-events-auto max-w-full overflow-x-auto">
433
540
  <BulkActionToolbar
434
541
  count={selected.size}
542
+ allSelected={allSelected}
543
+ onSelectAll={toggleSelectAll}
435
544
  moveTargets={moveTargets}
436
545
  onArchive={async () => {
437
546
  setIsBulkPending(true);
@@ -566,7 +566,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
566
566
  browserTabId={getBrowserTabId()}
567
567
  >
568
568
  {/* Main content area */}
569
- <div className="flex min-w-0 flex-1 flex-col">
569
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col">
570
570
  {!pageOwnsToolbar && (
571
571
  <header className="flex shrink-0 items-center gap-3 border-b border-border px-5 py-3">
572
572
  <button
@@ -65,7 +65,7 @@ interface RecordingCardProps {
65
65
  recording: RecordingSummary;
66
66
  selected?: boolean;
67
67
  selectionMode?: boolean;
68
- onToggleSelect?: (id: string) => void;
68
+ onToggleSelect?: (id: string, shiftKey: boolean) => void;
69
69
  onShare?: (rec: RecordingSummary) => void;
70
70
  onMove?: (rec: RecordingSummary, folderId: string | null) => void;
71
71
  moveTargets?: BulkMoveTarget[];
@@ -147,18 +147,18 @@ export function RecordingCard({
147
147
  const handleLinkClick = useCallback(
148
148
  (event: React.MouseEvent<HTMLAnchorElement>) => {
149
149
  if (
150
- !selectionMode ||
150
+ !onToggleSelect ||
151
151
  event.button !== 0 ||
152
152
  event.metaKey ||
153
153
  event.ctrlKey ||
154
- event.shiftKey ||
155
- event.altKey
154
+ event.altKey ||
155
+ (!selectionMode && !event.shiftKey)
156
156
  ) {
157
157
  return;
158
158
  }
159
159
 
160
160
  event.preventDefault();
161
- onToggleSelect?.(recording.id);
161
+ onToggleSelect(recording.id, event.shiftKey);
162
162
  },
163
163
  [onToggleSelect, recording.id, selectionMode],
164
164
  );
@@ -166,7 +166,7 @@ export function RecordingCard({
166
166
  const handleCheckbox = useCallback(
167
167
  (e: React.MouseEvent) => {
168
168
  e.stopPropagation();
169
- onToggleSelect?.(recording.id);
169
+ onToggleSelect?.(recording.id, e.shiftKey);
170
170
  },
171
171
  [onToggleSelect, recording.id],
172
172
  );
@@ -243,8 +243,10 @@ export function RecordingCard({
243
243
  >
244
244
  <Checkbox
245
245
  checked={selected}
246
- onCheckedChange={() => onToggleSelect?.(recording.id)}
247
- onClick={(e) => e.stopPropagation()}
246
+ onClick={(e) => {
247
+ e.stopPropagation();
248
+ onToggleSelect?.(recording.id, e.shiftKey);
249
+ }}
248
250
  className="h-3.5 w-3.5"
249
251
  />
250
252
  </div>
@@ -833,6 +833,10 @@ All notable user-facing changes to Clips are documented here. Open it any time f
833
833
  loadFailedBody:
834
834
  "Something went wrong while loading this list. Your recordings are safe — try again.",
835
835
  retry: "Retry",
836
+ paginationRange: "{{start}}–{{end}} of {{total}}",
837
+ paginationPrevious: "Previous",
838
+ paginationNext: "Next",
839
+ paginationPage: "Page {{page}} of {{totalPages}}",
836
840
  },
837
841
  notificationsRoute: {
838
842
  pageTitle: "Notifications · Clips",
@@ -1370,6 +1374,8 @@ All notable user-facing changes to Clips are documented here. Open it any time f
1370
1374
  clipsFinalRaw: {
1371
1375
  splitAtPlayhead: "Split at playhead (S)",
1372
1376
  selectedCount: "{{count}} selected",
1377
+ selectAll: "Select all",
1378
+ deselectAll: "Deselect all",
1373
1379
  move: "Move",
1374
1380
  moveSelected: "Move {{count}} selected",
1375
1381
  current: "Current",
@@ -46,6 +46,7 @@ export default function TrashRoute() {
46
46
  const t = useT();
47
47
  const [sort, setSort] = useState<SortKey>("recent");
48
48
  const [selected, setSelected] = useState<Set<string>>(new Set());
49
+ const [lastSelectedId, setLastSelectedId] = useState<string | null>(null);
49
50
  const [confirmPurge, setConfirmPurge] = useState(false);
50
51
  const [singlePurgeId, setSinglePurgeId] = useState<string | null>(null);
51
52
  const [isBulkPending, setIsBulkPending] = useState(false);
@@ -60,13 +61,26 @@ export default function TrashRoute() {
60
61
  "delete-recording-permanent",
61
62
  );
62
63
 
63
- const toggleSelect = (id: string) => {
64
+ const toggleSelect = (id: string, shiftKey = false) => {
64
65
  setSelected((prev) => {
66
+ if (shiftKey && lastSelectedId && lastSelectedId !== id) {
67
+ const ids = recordings.map((r) => r.id);
68
+ const fromIndex = ids.indexOf(lastSelectedId);
69
+ const toIndex = ids.indexOf(id);
70
+ if (fromIndex !== -1 && toIndex !== -1) {
71
+ const [start, end] =
72
+ fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex];
73
+ const next = new Set(prev);
74
+ for (let i = start; i <= end; i++) next.add(ids[i]);
75
+ return next;
76
+ }
77
+ }
65
78
  const next = new Set(prev);
66
79
  if (next.has(id)) next.delete(id);
67
80
  else next.add(id);
68
81
  return next;
69
82
  });
83
+ setLastSelectedId(id);
70
84
  };
71
85
 
72
86
  const restoreAll = async (ids: string[]) => {
@@ -170,6 +184,7 @@ export default function TrashRoute() {
170
184
  ? new Set()
171
185
  : new Set(recordings.map((r) => r.id)),
172
186
  );
187
+ setLastSelectedId(null);
173
188
  };
174
189
 
175
190
  return (
@@ -572,6 +572,21 @@ function openPrivacySettings(pane: MacosPrivacyPane): void {
572
572
  }
573
573
  }
574
574
 
575
+ // Same explicit-drag pattern the toolbar/bubble overlays use —
576
+ // `data-tauri-drag-region` has been unreliable, so we call `startDragging()`
577
+ // directly on mousedown. Clicks on buttons/inputs still reach their handlers
578
+ // since we only start a drag when the mousedown target isn't inside one.
579
+ function handlePopoverHeaderMouseDown(event: React.MouseEvent) {
580
+ if (event.button !== 0) return;
581
+ const target = event.target as HTMLElement;
582
+ if (target.closest("button, a, input, select, textarea")) return;
583
+ getCurrentWindow()
584
+ .startDragging()
585
+ .catch((err) => {
586
+ console.warn("[clips-popover] startDragging failed:", err);
587
+ });
588
+ }
589
+
575
590
  function nativeVoiceProvider(): VoiceProvider {
576
591
  return isMacPlatform() ? "macos-native" : "browser";
577
592
  }
@@ -4248,7 +4263,10 @@ function Header({
4248
4263
  // close button lives top-right as an absolute-positioned sibling, so the
4249
4264
  // tabs aren't offset by the close button's width.
4250
4265
  return (
4251
- <div className="header header-centered">
4266
+ <div
4267
+ className="header header-centered"
4268
+ onMouseDown={handlePopoverHeaderMouseDown}
4269
+ >
4252
4270
  <FeedbackButton submitterEmail={submitterEmail} />
4253
4271
  <div
4254
4272
  className="mode-toggle"
@@ -4444,7 +4462,10 @@ function PopoverSubViewHeader({
4444
4462
  action?: ReactNode;
4445
4463
  }) {
4446
4464
  return (
4447
- <div className="setup-header popover-view-header">
4465
+ <div
4466
+ className="setup-header popover-view-header"
4467
+ onMouseDown={handlePopoverHeaderMouseDown}
4468
+ >
4448
4469
  <button
4449
4470
  type="button"
4450
4471
  className="setup-back"
@@ -6376,7 +6397,7 @@ function Setup({
6376
6397
 
6377
6398
  return (
6378
6399
  <div className="setup">
6379
- <div className="setup-header">
6400
+ <div className="setup-header" onMouseDown={handlePopoverHeaderMouseDown}>
6380
6401
  {onCancel ? (
6381
6402
  <button
6382
6403
  type="button"
@@ -4143,25 +4143,6 @@ function DatabaseItemPreview({
4143
4143
  draftVersionsRef.current.set(documentId, null);
4144
4144
  else if (result.draft) {
4145
4145
  draftVersionsRef.current.set(documentId, result.draft.version);
4146
- if (
4147
- previewDraftNeedsConflict({
4148
- returnedDraft: result.draft,
4149
- pending: controller.pending,
4150
- })
4151
- ) {
4152
- controller.notifyDraftConflict({
4153
- lastSaved: snapshot.lastSaved,
4154
- pending: {
4155
- title: result.draft.title,
4156
- content: result.draft.content,
4157
- loadedUpdatedAt:
4158
- result.draft.baseDocumentUpdatedAt ?? undefined,
4159
- loadedContentWasEmpty:
4160
- result.draft.loadedContentWasEmpty === 1,
4161
- },
4162
- deferredReason: "conflict",
4163
- });
4164
- }
4165
4146
  } else {
4166
4147
  // Another tab already removed the row. Treat the stale delete as
4167
4148
  // converged instead of retaining a version that can never match.
@@ -4186,24 +4167,6 @@ function DatabaseItemPreview({
4186
4167
  draftVersionsRef.current.set(documentId, result.draft.version);
4187
4168
  } else if (result.draft) {
4188
4169
  draftVersionsRef.current.set(documentId, result.draft.version);
4189
- if (
4190
- previewDraftNeedsConflict({
4191
- returnedDraft: result.draft,
4192
- pending: controller.pending,
4193
- })
4194
- ) {
4195
- controller.notifyDraftConflict({
4196
- lastSaved: snapshot.lastSaved,
4197
- pending: {
4198
- title: result.draft.title,
4199
- content: result.draft.content,
4200
- loadedUpdatedAt:
4201
- result.draft.baseDocumentUpdatedAt ?? undefined,
4202
- loadedContentWasEmpty: result.draft.loadedContentWasEmpty === 1,
4203
- },
4204
- deferredReason: "conflict",
4205
- });
4206
- }
4207
4170
  } else {
4208
4171
  // A competing tab deleted the version we tried to update. Reset to
4209
4172
  // create mode and enqueue the latest pending payload once; keeping
@@ -4298,7 +4261,13 @@ function DatabaseItemPreview({
4298
4261
  );
4299
4262
  return;
4300
4263
  }
4301
- resolve(undefined);
4264
+ resolve({
4265
+ outcome: "saved" as const,
4266
+ loadedUpdatedAt: result.updatedAt,
4267
+ loadedContentWasEmpty: isEffectivelyEmptyDocumentContent(
4268
+ result.content,
4269
+ ),
4270
+ });
4302
4271
  },
4303
4272
  onError: reject,
4304
4273
  },
@@ -4410,6 +4379,8 @@ function DatabaseItemPreview({
4410
4379
  }
4411
4380
  return;
4412
4381
  }
4382
+ const previouslyExpectedVersion =
4383
+ draftVersionsRef.current.get(documentId) ?? null;
4413
4384
  draftVersionsRef.current.set(documentId, draft.version);
4414
4385
  const controller = peekPreviewDocumentSaveController(documentId);
4415
4386
  if (!controller) return;
@@ -4431,8 +4402,19 @@ function DatabaseItemPreview({
4431
4402
  ? draft.deferredReason
4432
4403
  : null,
4433
4404
  } as const;
4405
+ // A poll tick simply confirming a version this client's own debounced
4406
+ // write already advanced the ref to is not an external change — it is
4407
+ // this client's typing racing ahead of its own last round trip. Comparing
4408
+ // that echo against the still-live `pending` would flag a conflict on
4409
+ // almost every keystroke. Only treat the draft as having moved out from
4410
+ // under the user when its version is newer than what this client itself
4411
+ // last recorded.
4412
+ const draftAdvancedExternally =
4413
+ previouslyExpectedVersion !== null &&
4414
+ draft.version > previouslyExpectedVersion;
4434
4415
  if (controllerDirty) {
4435
4416
  if (
4417
+ draftAdvancedExternally &&
4436
4418
  previewDraftNeedsConflict({
4437
4419
  returnedDraft: draft,
4438
4420
  pending: controller.pending,
@@ -67,6 +67,19 @@ export interface PreviewDocumentSaveDeferred {
67
67
  conflictSnapshot?: PreviewDocumentDraftSnapshot;
68
68
  }
69
69
 
70
+ /**
71
+ * A successful save can report the server's fresh `updatedAt`/emptiness back
72
+ * to the controller so its baseline stops looking stale. Without this, a
73
+ * baseline seeded as empty (a brand-new page) stays flagged empty forever, so
74
+ * a later poll of content the user just saved themselves gets mistaken for a
75
+ * non-empty body arriving externally over an empty one.
76
+ */
77
+ export interface PreviewDocumentSaveSuccess {
78
+ outcome: "saved";
79
+ loadedUpdatedAt?: string;
80
+ loadedContentWasEmpty?: boolean;
81
+ }
82
+
70
83
  export interface PreviewDocumentSaveAdapter {
71
84
  save: (
72
85
  documentId: string,
@@ -151,6 +164,18 @@ function payloadsEqual(a: PreviewDocumentPayload, b: PreviewDocumentPayload) {
151
164
  return a.title === b.title && a.content === b.content;
152
165
  }
153
166
 
167
+ function asSaveSuccess(result: unknown): PreviewDocumentSaveSuccess | null {
168
+ if (
169
+ result &&
170
+ typeof result === "object" &&
171
+ "outcome" in result &&
172
+ (result as { outcome?: unknown }).outcome === "saved"
173
+ ) {
174
+ return result as PreviewDocumentSaveSuccess;
175
+ }
176
+ return null;
177
+ }
178
+
154
179
  export function createPreviewDocumentSaveController(
155
180
  args: PreviewDocumentSaveAdapter & {
156
181
  /**
@@ -230,7 +255,21 @@ export function createPreviewDocumentSaveController(
230
255
  }
231
256
  return;
232
257
  }
233
- lastSaved = attempted;
258
+ // Adopt the server's fresh metadata (if the adapter reported it) into
259
+ // the confirmed baseline. Otherwise `loadedUpdatedAt`/
260
+ // `loadedContentWasEmpty` would keep carrying whatever was true when
261
+ // the controller was created/last marked — e.g. "empty" for a
262
+ // brand-new page — forever, even after real content has been saved.
263
+ const success = asSaveSuccess(result);
264
+ lastSaved = {
265
+ ...attempted,
266
+ ...(success?.loadedUpdatedAt !== undefined
267
+ ? { loadedUpdatedAt: success.loadedUpdatedAt }
268
+ : {}),
269
+ ...(success?.loadedContentWasEmpty !== undefined
270
+ ? { loadedContentWasEmpty: success.loadedContentWasEmpty }
271
+ : {}),
272
+ };
234
273
  hasSavedLocally = true;
235
274
  deferredReason = null;
236
275
  inFlight = null;
@@ -13,13 +13,13 @@
13
13
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
14
14
  count: number;
15
15
  updated?: undefined;
16
- ok?: undefined;
17
16
  error?: undefined;
17
+ ok?: undefined;
18
18
  } | {
19
19
  count?: undefined;
20
20
  updated: number;
21
- ok?: undefined;
22
21
  error?: undefined;
22
+ ok?: undefined;
23
23
  } | {
24
24
  count?: undefined;
25
25
  updated?: undefined;
@@ -28,7 +28,7 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
28
28
  } | {
29
29
  count?: undefined;
30
30
  updated?: undefined;
31
- ok: boolean;
32
31
  error?: undefined;
32
+ ok: boolean;
33
33
  }>>;
34
34
  //# sourceMappingURL=routes.d.ts.map
@@ -41,28 +41,28 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
41
41
  thumbsUpRate: number;
42
42
  avgEvalScore: number;
43
43
  } | {
44
+ error?: undefined;
45
+ ok?: undefined;
44
46
  summary: import("./types.js").TraceSummary;
45
47
  spans: import("./types.js").TraceSpan[];
46
48
  id?: undefined;
49
+ } | {
47
50
  error?: undefined;
48
51
  ok?: undefined;
49
- } | {
50
52
  summary?: undefined;
51
53
  spans?: undefined;
52
54
  id: string;
53
- error?: undefined;
54
- ok?: undefined;
55
55
  } | {
56
+ ok?: undefined;
56
57
  summary?: undefined;
57
58
  spans?: undefined;
58
59
  id?: undefined;
59
60
  error: any;
60
- ok?: undefined;
61
61
  } | {
62
+ error?: undefined;
62
63
  summary?: undefined;
63
64
  spans?: undefined;
64
65
  id?: undefined;
65
- error?: undefined;
66
66
  ok: boolean;
67
67
  }>>;
68
68
  //# sourceMappingURL=routes.d.ts.map
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- status?: undefined;
38
37
  ok?: undefined;
38
+ status?: undefined;
39
39
  } | {
40
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — re-run the validator against the
54
54
  * current stored value without changing anything. Useful for the "Test" button.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
+ ok?: undefined;
57
58
  error: string;
58
59
  note?: undefined;
59
- ok?: undefined;
60
60
  } | {
61
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,12 +95,12 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
106
106
  //# sourceMappingURL=routes.d.ts.map
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
37
37
  //# sourceMappingURL=agent-engine-api-key-route.d.ts.map
@@ -20,7 +20,7 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- text: string;
24
23
  error?: undefined;
24
+ text: string;
25
25
  }>>;
26
26
  //# sourceMappingURL=transcribe-voice.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.114.15",
3
+ "version": "0.114.16",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {