@agent-native/core 0.164.11 → 0.164.15

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 (106) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +84 -0
  3. package/corpus/templates/calendar/AGENTS.md +5 -0
  4. package/corpus/templates/calendar/actions/delete-event.ts +1 -1
  5. package/corpus/templates/calendar/actions/delete-events.ts +508 -0
  6. package/corpus/templates/calendar/actions/event-action-helpers.ts +1 -1
  7. package/corpus/templates/calendar/actions/list-events.ts +53 -0
  8. package/corpus/templates/calendar/changelog/2026-08-18-the-agent-can-now-remove-many-meetings-at-once-such-as-every.md +6 -0
  9. package/corpus/templates/calendar/server/lib/event-weekday.ts +112 -0
  10. package/corpus/templates/content/actions/_database-source-utils.ts +25 -2
  11. package/corpus/templates/content/actions/_database-utils.ts +2 -0
  12. package/corpus/templates/content/actions/_local-folder-source.ts +186 -2
  13. package/corpus/templates/content/actions/connect-local-folder-source.ts +23 -8
  14. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +3 -3
  15. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +2 -2
  16. package/corpus/templates/content/actions/sync-local-folder-source.ts +130 -12
  17. package/corpus/templates/content/app/components/LocalFolderLiveSync.tsx +77 -0
  18. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +354 -8
  19. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -1
  20. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +78 -1
  21. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +36 -0
  22. package/corpus/templates/content/app/components/editor/local-source-write-state.ts +52 -0
  23. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +244 -6
  24. package/corpus/templates/content/app/components/sidebar/local-source-hierarchy.ts +189 -0
  25. package/corpus/templates/content/app/hooks/use-create-page.ts +26 -4
  26. package/corpus/templates/content/app/hooks/use-documents.ts +83 -0
  27. package/corpus/templates/content/app/i18n-data.ts +79 -0
  28. package/corpus/templates/content/app/lib/desktop-content-files.ts +57 -1
  29. package/corpus/templates/content/app/lib/local-content-source-files.ts +210 -34
  30. package/corpus/templates/content/app/lib/local-folder-live-sync.ts +191 -0
  31. package/corpus/templates/content/app/root.tsx +2 -1
  32. package/corpus/templates/content/app/routes/_app.local-files.tsx +103 -38
  33. package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +2 -2
  34. package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +3 -3
  35. package/corpus/templates/content/docs/product/roadmap.md +2 -2
  36. package/corpus/templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md +482 -0
  37. package/corpus/templates/content/shared/api.ts +25 -0
  38. package/corpus/templates/factory/AGENTS.md +2 -1
  39. package/corpus/templates/factory/actions/get-factory-graph-version.ts +69 -0
  40. package/corpus/templates/factory/actions/list-factory-graph-versions.ts +82 -0
  41. package/corpus/templates/factory/actions/restore-factory-graph-version.ts +163 -0
  42. package/corpus/templates/factory/actions/save-factory-graph.ts +93 -60
  43. package/corpus/templates/factory/app/components/factory/FactoryHistoryView.tsx +709 -0
  44. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +62 -2
  45. package/corpus/templates/factory/app/components/ui/alert-dialog.tsx +1 -0
  46. package/corpus/templates/factory/app/i18n/en-US.ts +43 -0
  47. package/corpus/templates/factory/app/routes/factory.tsx +115 -15
  48. package/corpus/templates/factory/server/plugins/agent-chat.ts +3 -1
  49. package/dist/app-config/auth.d.ts +4 -0
  50. package/dist/app-config/auth.js +7 -0
  51. package/dist/app-config/schema.d.ts +3 -0
  52. package/dist/app-config/schema.js +2 -0
  53. package/dist/cli/code-agent-executor.js +6 -1
  54. package/dist/cli/code-agent-mcp-config.d.ts +2 -2
  55. package/dist/cli/code-agent-mcp-config.js +34 -9
  56. package/dist/client/org/OrgSwitcher.js +8 -4
  57. package/dist/collab/awareness.d.ts +2 -2
  58. package/dist/collab/routes.d.ts +1 -1
  59. package/dist/collab/struct-routes.d.ts +1 -1
  60. package/dist/mcp-client/config.d.ts +11 -0
  61. package/dist/mcp-client/config.js +33 -26
  62. package/dist/notifications/routes.d.ts +3 -3
  63. package/dist/observability/routes.d.ts +3 -3
  64. package/dist/progress/routes.d.ts +1 -1
  65. package/dist/resources/handlers.d.ts +2 -2
  66. package/dist/secrets/routes.d.ts +9 -9
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/auth.js +15 -1
  69. package/dist/server/realtime-token.d.ts +1 -1
  70. package/docs/content/actions-access-control.mdx +136 -2
  71. package/docs/content/actions-agent-tools.mdx +101 -0
  72. package/docs/content/actions-defining.mdx +2 -2
  73. package/docs/content/audit-log.mdx +1 -1
  74. package/docs/content/authentication.mdx +1 -1
  75. package/docs/content/cloudflare.mdx +43 -0
  76. package/docs/content/creating-templates.mdx +1 -1
  77. package/docs/content/deploy-an-app.mdx +109 -0
  78. package/docs/content/deployment-environment-variables.mdx +101 -0
  79. package/docs/content/deployment.mdx +125 -792
  80. package/docs/content/docs-components.mdx +2 -2
  81. package/docs/content/environment-variables.mdx +41 -41
  82. package/docs/content/file-uploads.mdx +74 -40
  83. package/docs/content/harness-agents.mdx +2 -2
  84. package/docs/content/internationalization.mdx +61 -65
  85. package/docs/content/key-concepts.mdx +3 -3
  86. package/docs/content/multi-app-workspace.mdx +2 -2
  87. package/docs/content/netlify.mdx +47 -0
  88. package/docs/content/node-docker.mdx +138 -0
  89. package/docs/content/organizations-teams-permissions.mdx +5 -5
  90. package/docs/content/other-platforms.mdx +27 -0
  91. package/docs/content/ssr-caching.mdx +45 -0
  92. package/docs/content/template-assets-developers.mdx +2 -2
  93. package/docs/content/template-chat-developers.mdx +2 -2
  94. package/docs/content/template-content-developers.mdx +2 -2
  95. package/docs/content/{local-file-mode.mdx → template-content-local-files.mdx} +2 -1
  96. package/docs/content/template-content-sync.mdx +2 -2
  97. package/docs/content/toolkit-agent-ux.mdx +1 -1
  98. package/docs/content/toolkit-resources.mdx +2 -2
  99. package/docs/content/updating-ui-in-production.mdx +35 -0
  100. package/docs/content/vercel.mdx +38 -0
  101. package/docs/content/what-is-agent-native.mdx +1 -1
  102. package/docs/content/workspace-deployment.mdx +350 -0
  103. package/package.json +1 -1
  104. package/docs/content/agents.mdx +0 -9
  105. package/docs/content/database.mdx +0 -317
  106. package/docs/content/human-approval.mdx +0 -156
@@ -771,6 +771,20 @@ interface VisualEditorProps {
771
771
  * whose type has no NFM analog (via the shared registry-block side-map).
772
772
  */
773
773
  notionPageId?: string | null;
774
+ onHistoryControllerChange?: (
775
+ controller: VisualEditorHistoryController | null,
776
+ ) => void;
777
+ onHistoryStateChange?: (state: VisualEditorHistoryState) => void;
778
+ }
779
+
780
+ export interface VisualEditorHistoryState {
781
+ canUndo: boolean;
782
+ canRedo: boolean;
783
+ }
784
+
785
+ export interface VisualEditorHistoryController {
786
+ undo: () => boolean;
787
+ redo: () => boolean;
774
788
  }
775
789
 
776
790
  export type { NotionPageLink };
@@ -968,6 +982,27 @@ function isActiveSlashCommandDraft(editor: CoreEditor): boolean {
968
982
  return /^\s*\/[a-zA-Z0-9]*$/.test(textBefore);
969
983
  }
970
984
 
985
+ export function runPersistableHistoryCommand(
986
+ editor: CoreEditor,
987
+ command: "undo" | "redo",
988
+ ): boolean {
989
+ let applied = false;
990
+ for (let index = 0; index < 20; index += 1) {
991
+ const changed = editor
992
+ .chain()
993
+ .focus()
994
+ .command(({ tr }) => {
995
+ tr.setMeta(LOCAL_FILE_USER_EDIT_META, true);
996
+ return true;
997
+ })
998
+ [command]()
999
+ .run();
1000
+ applied ||= changed;
1001
+ if (!changed || !isActiveSlashCommandDraft(editor)) break;
1002
+ }
1003
+ return applied;
1004
+ }
1005
+
971
1006
  interface VisualEditorExtensionOptions {
972
1007
  documentId?: string;
973
1008
  ydoc?: YDoc | null;
@@ -1957,6 +1992,8 @@ export function VisualEditor({
1957
1992
  notionPageLinks = [],
1958
1993
  onOpenNotionPageLink,
1959
1994
  notionPageId,
1995
+ onHistoryControllerChange,
1996
+ onHistoryStateChange,
1960
1997
  }: VisualEditorProps) {
1961
1998
  const t = useT();
1962
1999
  const [isDraggingMedia, setIsDraggingMedia] = useState(false);
@@ -1967,6 +2004,8 @@ export function VisualEditor({
1967
2004
  onChangeRef.current = onChange;
1968
2005
  const onSaveContentRef = useRef(onSaveContent);
1969
2006
  onSaveContentRef.current = onSaveContent;
2007
+ const onHistoryStateChangeRef = useRef(onHistoryStateChange);
2008
+ onHistoryStateChangeRef.current = onHistoryStateChange;
1970
2009
  const notionPageLinksRef = useRef(notionPageLinks);
1971
2010
  notionPageLinksRef.current = notionPageLinks;
1972
2011
  const onMediaSourceCommittedRef = useRef<
@@ -2150,6 +2189,7 @@ export function VisualEditor({
2150
2189
  }
2151
2190
  };
2152
2191
 
2192
+ const historyEditorRef = useRef<CoreEditor | null>(null);
2153
2193
  const editor = useEditor({
2154
2194
  extensions,
2155
2195
  // With Collaboration (ydoc) active, content is owned by the Y.XmlFragment —
@@ -2240,8 +2280,22 @@ export function VisualEditor({
2240
2280
  if (view.editable) markUserEditIntent();
2241
2281
  return false;
2242
2282
  },
2243
- keydown(view) {
2283
+ keydown(view, event) {
2244
2284
  if (view.editable) markUserEditIntent();
2285
+ if (
2286
+ view.editable &&
2287
+ (event.metaKey || event.ctrlKey) &&
2288
+ !event.altKey &&
2289
+ event.key.toLowerCase() === "z" &&
2290
+ historyEditorRef.current
2291
+ ) {
2292
+ event.preventDefault();
2293
+ runPersistableHistoryCommand(
2294
+ historyEditorRef.current,
2295
+ event.shiftKey ? "redo" : "undo",
2296
+ );
2297
+ return true;
2298
+ }
2245
2299
  return false;
2246
2300
  },
2247
2301
  cut(view) {
@@ -2276,6 +2330,12 @@ export function VisualEditor({
2276
2330
  },
2277
2331
  },
2278
2332
  editable,
2333
+ onTransaction: ({ editor }) => {
2334
+ onHistoryStateChangeRef.current?.({
2335
+ canUndo: editor.can().undo(),
2336
+ canRedo: editor.can().redo(),
2337
+ });
2338
+ },
2279
2339
  onUpdate: ({ editor, transaction }) => {
2280
2340
  const guards = guardsRef.current;
2281
2341
  // `shouldIgnoreUpdate` covers: not editable, mid-programmatic setContent,
@@ -2326,6 +2386,23 @@ export function VisualEditor({
2326
2386
  });
2327
2387
  },
2328
2388
  });
2389
+ historyEditorRef.current = editor;
2390
+
2391
+ useEffect(() => {
2392
+ if (!editor) {
2393
+ onHistoryControllerChange?.(null);
2394
+ return;
2395
+ }
2396
+ onHistoryControllerChange?.({
2397
+ undo: () => runPersistableHistoryCommand(editor, "undo"),
2398
+ redo: () => runPersistableHistoryCommand(editor, "redo"),
2399
+ });
2400
+ onHistoryStateChange?.({
2401
+ canUndo: editor.can().undo(),
2402
+ canRedo: editor.can().redo(),
2403
+ });
2404
+ return () => onHistoryControllerChange?.(null);
2405
+ }, [editor, onHistoryControllerChange, onHistoryStateChange]);
2329
2406
 
2330
2407
  const handleImageFileInputChange = useCallback(
2331
2408
  async (event: Event) => {
@@ -14,6 +14,8 @@ import {
14
14
  IconDatabase,
15
15
  IconDots,
16
16
  IconFileText,
17
+ IconFolder,
18
+ IconFolderOpen,
17
19
  IconPlus,
18
20
  IconStar,
19
21
  IconTrash,
@@ -266,6 +268,7 @@ export function ContentFilesSidebarView({
266
268
  : undefined;
267
269
  const manualReorderEnabled =
268
270
  Boolean(manualReorder) &&
271
+ !items.some((item) => item.document.source?.kind === "folder") &&
269
272
  (sidebarOrder?.mode ?? "custom") === "custom" &&
270
273
  activeView.sorts.length === 0 &&
271
274
  activeView.filters.length === 0 &&
@@ -746,6 +749,39 @@ function DatabaseSidebarRow({
746
749
 
747
750
  const title = item.document.title || untitledLabel;
748
751
 
752
+ if (item.document.source?.kind === "folder") {
753
+ return (
754
+ <div className="group relative min-w-0">
755
+ <button
756
+ type="button"
757
+ className="flex h-7 w-full min-w-0 items-center gap-1.5 rounded pe-1.5 text-start text-sm text-foreground/85 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
758
+ style={{
759
+ paddingInlineStart: `${databaseSidebarRowIndent(depth, hasChildren)}px`,
760
+ }}
761
+ aria-label={`${expanded ? t("sidebar.collapse") : t("sidebar.expand")} ${title}`}
762
+ aria-expanded={expanded}
763
+ onClick={() => onToggleExpanded?.(!expanded)}
764
+ onPointerUp={(event) => event.currentTarget.blur()}
765
+ >
766
+ <span className="flex size-7 shrink-0 items-center justify-center text-muted-foreground">
767
+ <IconChevronRight
768
+ className={cn(
769
+ "size-3.5 transition-transform",
770
+ expanded && "rotate-90",
771
+ )}
772
+ />
773
+ </span>
774
+ {expanded ? (
775
+ <IconFolderOpen className="size-3.5 shrink-0 text-muted-foreground" />
776
+ ) : (
777
+ <IconFolder className="size-3.5 shrink-0 text-muted-foreground" />
778
+ )}
779
+ <span className="min-w-0 flex-1 truncate">{title}</span>
780
+ </button>
781
+ </div>
782
+ );
783
+ }
784
+
749
785
  return (
750
786
  <>
751
787
  <div className="group relative min-w-0">
@@ -0,0 +1,52 @@
1
+ export interface PendingLocalSourceWrite {
2
+ title: string;
3
+ content: string;
4
+ }
5
+
6
+ export type LocalSourceReadDisposition =
7
+ | "pending-self-write"
8
+ | "conflict"
9
+ | "external-change"
10
+ | "unchanged";
11
+
12
+ export function localSourceRevisionForSave(
13
+ queuedRevision: string | null | undefined,
14
+ currentRevision: string | undefined,
15
+ ) {
16
+ return queuedRevision === undefined ? currentRevision : queuedRevision;
17
+ }
18
+
19
+ export function localSourceRevisionForQueuedEdit(
20
+ pendingRevision: string | null | undefined,
21
+ currentRevision: string | undefined,
22
+ ) {
23
+ return pendingRevision === undefined
24
+ ? (currentRevision ?? null)
25
+ : pendingRevision;
26
+ }
27
+
28
+ export function classifyLocalSourceRead(args: {
29
+ diskTitle: string;
30
+ diskContent: string;
31
+ localContent: string;
32
+ lastSavedTitle: string;
33
+ lastSavedContent: string;
34
+ pendingWrite: PendingLocalSourceWrite | null;
35
+ hasPendingSave?: boolean;
36
+ }): LocalSourceReadDisposition {
37
+ if (
38
+ args.pendingWrite?.title === args.diskTitle &&
39
+ args.pendingWrite.content === args.diskContent
40
+ ) {
41
+ return "pending-self-write";
42
+ }
43
+
44
+ const hasUnsavedEdit =
45
+ args.hasPendingSave === true || args.localContent !== args.lastSavedContent;
46
+ const diskChanged =
47
+ args.diskContent !== args.lastSavedContent ||
48
+ args.diskTitle !== args.lastSavedTitle;
49
+
50
+ if (hasUnsavedEdit && diskChanged) return "conflict";
51
+ return diskChanged ? "external-change" : "unchanged";
52
+ }
@@ -51,6 +51,7 @@ import {
51
51
  IconChevronRight,
52
52
  IconDots,
53
53
  IconTrash,
54
+ IconGitBranch,
54
55
  } from "@tabler/icons-react";
55
56
  import { useQueryClient } from "@tanstack/react-query";
56
57
  import {
@@ -127,8 +128,20 @@ import {
127
128
  buildDocumentTree,
128
129
  filterDocumentTreeDocuments,
129
130
  documentQueryFilter,
131
+ rollbackOptimisticCreatedDocument,
132
+ restoreDeletedDocumentSnapshots,
130
133
  } from "@/hooks/use-documents";
131
134
  import { useLocalStorage } from "@/hooks/use-local-storage";
135
+ import {
136
+ getDesktopContentFiles,
137
+ type DesktopContentFilesFolder,
138
+ } from "@/lib/desktop-content-files";
139
+ import {
140
+ consumeLiveLocalFolderActivation,
141
+ liveLocalFolderSourceId,
142
+ pendingLiveLocalFolderActivation,
143
+ subscribeLiveLocalFolderActivation,
144
+ } from "@/lib/local-folder-live-sync";
132
145
  import {
133
146
  markDocumentCreationPending,
134
147
  shouldCreateDocumentOptimistically,
@@ -140,6 +153,11 @@ import {
140
153
  isDirectLocalDocument,
141
154
  } from "./document-sidebar-sections";
142
155
  import { DocumentSidebarIcon, DocumentTreeItem } from "./DocumentTreeItem";
156
+ import {
157
+ firstLocalSourceDocumentId,
158
+ localSourceItemIdentity,
159
+ projectLocalSourceHierarchy,
160
+ } from "./local-source-hierarchy";
143
161
  import {
144
162
  contentSpaceAvailability,
145
163
  contentSpaceForStoredSelection,
@@ -433,6 +451,57 @@ function WorkspaceSidebarItem({
433
451
  onToggleFavorite: (item: ContentDatabaseItem) => void;
434
452
  }) {
435
453
  const t = useT();
454
+ const [localWorkingCopies, setLocalWorkingCopies] = useState<
455
+ DesktopContentFilesFolder[]
456
+ >([]);
457
+ const [selectedWorkingCopyId, setSelectedWorkingCopyId] = useState<
458
+ string | null
459
+ >(null);
460
+ useEffect(() => {
461
+ const desktop = getDesktopContentFiles();
462
+ if (!desktop) return;
463
+ let active = true;
464
+ const refresh = async (preferFolderId?: string) => {
465
+ const result = await desktop.getFolder(
466
+ preferFolderId ? { folderId: preferFolderId } : undefined,
467
+ );
468
+ if (!active || !result.ok) return;
469
+ const folders = result.folders ?? [result.folder];
470
+ const main = folders.find((folder) => folder.kind !== "temporary");
471
+ const repositoryId = main?.repository?.localId;
472
+ const related = repositoryId
473
+ ? folders.filter(
474
+ (folder) => folder.repository?.localId === repositoryId,
475
+ )
476
+ : folders;
477
+ setLocalWorkingCopies(related);
478
+ setSelectedWorkingCopyId((current) =>
479
+ preferFolderId && related.some((folder) => folder.id === preferFolderId)
480
+ ? preferFolderId
481
+ : current && related.some((folder) => folder.id === current)
482
+ ? current
483
+ : (main?.id ?? related[0]?.id ?? null),
484
+ );
485
+ if (
486
+ preferFolderId &&
487
+ related.some((folder) => folder.id === preferFolderId)
488
+ ) {
489
+ consumeLiveLocalFolderActivation(preferFolderId);
490
+ }
491
+ };
492
+ void refresh(pendingLiveLocalFolderActivation() ?? undefined);
493
+ const remove = desktop.onChange?.((event) => {
494
+ void refresh(event.reason === "attached" ? event.folderId : undefined);
495
+ });
496
+ const removeActivation = subscribeLiveLocalFolderActivation((folderId) => {
497
+ void refresh(folderId);
498
+ });
499
+ return () => {
500
+ active = false;
501
+ remove?.();
502
+ removeActivation();
503
+ };
504
+ }, []);
436
505
  const activeFilesDatabaseId = useDeferredFilesDatabaseId(
437
506
  space.filesDatabaseId,
438
507
  expanded,
@@ -442,6 +511,89 @@ function WorkspaceSidebarItem({
442
511
  const filesDatabaseData = isContentDatabaseUnavailable(filesDatabase.data)
443
512
  ? undefined
444
513
  : filesDatabase.data;
514
+ const workspaceSourceIds = new Set(
515
+ filesDatabaseData?.items.flatMap((item) => {
516
+ const identity = localSourceItemIdentity(item);
517
+ return identity ? [identity] : [];
518
+ }) ?? [],
519
+ );
520
+ const workspaceRootPaths = new Set(
521
+ filesDatabaseData?.items.flatMap((item) => {
522
+ const rootPath = item.document.source?.rootPath;
523
+ return rootPath ? [rootPath] : [];
524
+ }) ?? [],
525
+ );
526
+ const relatedWorkingCopies = localWorkingCopies.filter((folder) => {
527
+ const sourceId = folder.id ? liveLocalFolderSourceId(folder.id) : null;
528
+ const rootPath =
529
+ folder.kind === "temporary"
530
+ ? folder.name
531
+ : (folder.sourcePrefix ?? folder.name);
532
+ return (
533
+ (typeof sourceId === "string" && workspaceSourceIds.has(sourceId)) ||
534
+ workspaceRootPaths.has(folder.id ?? rootPath) ||
535
+ workspaceRootPaths.has(rootPath)
536
+ );
537
+ });
538
+ const selectedWorkingCopy =
539
+ relatedWorkingCopies.find(
540
+ (folder) => folder.id === selectedWorkingCopyId,
541
+ ) ??
542
+ relatedWorkingCopies.find((folder) => folder.kind !== "temporary") ??
543
+ relatedWorkingCopies[0];
544
+ const selectedSourceRoot = selectedWorkingCopy
545
+ ? (selectedWorkingCopy.id ??
546
+ (selectedWorkingCopy.kind === "temporary"
547
+ ? selectedWorkingCopy.name
548
+ : (selectedWorkingCopy.sourcePrefix ?? selectedWorkingCopy.name)))
549
+ : undefined;
550
+ const selectedSourceId = selectedWorkingCopy?.id
551
+ ? liveLocalFolderSourceId(selectedWorkingCopy.id)
552
+ : undefined;
553
+ const workingCopySourceIds = new Set(
554
+ relatedWorkingCopies.flatMap((folder) => {
555
+ const sourceId = folder.id ? liveLocalFolderSourceId(folder.id) : null;
556
+ return sourceId ? [sourceId] : [];
557
+ }),
558
+ );
559
+ const hasRelatedLocalFiles = filesDatabaseData?.items.some(
560
+ (item) =>
561
+ item.document.source?.mode === "local-files" &&
562
+ (workingCopySourceIds.has(localSourceItemIdentity(item) ?? "") ||
563
+ relatedWorkingCopies.some(
564
+ (folder) =>
565
+ item.document.source?.rootPath ===
566
+ (folder.id ??
567
+ (folder.kind === "temporary"
568
+ ? folder.name
569
+ : (folder.sourcePrefix ?? folder.name))),
570
+ )),
571
+ );
572
+ const visibleFilesDatabaseData = filesDatabaseData
573
+ ? {
574
+ ...filesDatabaseData,
575
+ items: projectLocalSourceHierarchy(filesDatabaseData.items, {
576
+ sourceId: selectedSourceId,
577
+ rootPath: selectedSourceRoot,
578
+ }),
579
+ }
580
+ : undefined;
581
+ const openedWorkingCopyIdRef = useRef<string | null>(null);
582
+ useEffect(() => {
583
+ if (
584
+ selectedWorkingCopy?.kind !== "temporary" ||
585
+ !selectedWorkingCopy.id ||
586
+ openedWorkingCopyIdRef.current === selectedWorkingCopy.id
587
+ ) {
588
+ return;
589
+ }
590
+ const firstDocumentId = firstLocalSourceDocumentId(
591
+ visibleFilesDatabaseData?.items ?? [],
592
+ );
593
+ if (!firstDocumentId) return;
594
+ openedWorkingCopyIdRef.current = selectedWorkingCopy.id;
595
+ onActivate(space, firstDocumentId);
596
+ }, [onActivate, selectedWorkingCopy, space, visibleFilesDatabaseData]);
445
597
  const resolvedFilesDatabaseId = filesDatabaseData?.database.id ?? null;
446
598
  const filesPersonalView = useContentDatabasePersonalView(
447
599
  resolvedFilesDatabaseId,
@@ -605,6 +757,62 @@ function WorkspaceSidebarItem({
605
757
  </div>
606
758
  {expanded ? (
607
759
  <div className="min-w-0 pb-1 ps-4">
760
+ {relatedWorkingCopies.some((folder) => folder.kind === "temporary") &&
761
+ selectedWorkingCopy &&
762
+ hasRelatedLocalFiles ? (
763
+ <DropdownMenu>
764
+ <DropdownMenuTrigger asChild>
765
+ <button
766
+ type="button"
767
+ className="mb-1 flex min-h-8 w-full min-w-0 items-center gap-2 rounded-md px-2 text-start text-xs hover:bg-accent/40"
768
+ title={selectedWorkingCopy.updatedAt}
769
+ data-working-copy-switcher
770
+ >
771
+ <span className="min-w-0 flex-1">
772
+ <span className="block truncate font-medium">
773
+ {selectedWorkingCopy.kind === "temporary"
774
+ ? selectedWorkingCopy.name
775
+ : t("localFiles.mainFolder")}
776
+ </span>
777
+ {selectedWorkingCopy.repository?.branch ||
778
+ selectedWorkingCopy.repository?.commit ? (
779
+ <span className="flex items-center gap-1 truncate font-mono text-[10px] text-muted-foreground">
780
+ <IconGitBranch size={11} />
781
+ {selectedWorkingCopy.repository.branch ??
782
+ selectedWorkingCopy.repository.commit?.slice(0, 8)}
783
+ </span>
784
+ ) : null}
785
+ </span>
786
+ <IconChevronDown size={13} className="shrink-0" />
787
+ </button>
788
+ </DropdownMenuTrigger>
789
+ <DropdownMenuContent align="start" className="w-64">
790
+ {relatedWorkingCopies.map((folder) => (
791
+ <DropdownMenuItem
792
+ key={folder.id}
793
+ onSelect={() => setSelectedWorkingCopyId(folder.id ?? null)}
794
+ title={folder.updatedAt}
795
+ >
796
+ <span className="min-w-0">
797
+ <span className="block truncate">
798
+ {folder.kind === "temporary"
799
+ ? folder.name
800
+ : t("localFiles.mainFolder")}
801
+ </span>
802
+ {folder.repository?.branch ||
803
+ folder.repository?.commit ? (
804
+ <span className="flex items-center gap-1 truncate font-mono text-[10px] text-muted-foreground">
805
+ <IconGitBranch size={11} />
806
+ {folder.repository.branch ??
807
+ folder.repository.commit?.slice(0, 8)}
808
+ </span>
809
+ ) : null}
810
+ </span>
811
+ </DropdownMenuItem>
812
+ ))}
813
+ </DropdownMenuContent>
814
+ </DropdownMenu>
815
+ ) : null}
608
816
  {failed ? (
609
817
  <QueryErrorState
610
818
  compact
@@ -618,7 +826,7 @@ function WorkspaceSidebarItem({
618
826
  />
619
827
  ) : (
620
828
  <ContentFilesSidebarView
621
- data={filesDatabaseData}
829
+ data={visibleFilesDatabaseData}
622
830
  overrides={filesPersonalView.data?.overrides}
623
831
  isLoading={filesDatabase.isLoading || filesPersonalView.isLoading}
624
832
  activeDocumentId={activeDocumentId}
@@ -1153,12 +1361,16 @@ export function DocumentSidebar({
1153
1361
  createdAt: now,
1154
1362
  updatedAt: now,
1155
1363
  });
1364
+ const previousDocuments = queryClient.getQueryData(
1365
+ LIST_DOCUMENTS_QUERY_KEY,
1366
+ );
1367
+ const previousPath = `${location.pathname}${location.search}${location.hash}`;
1156
1368
 
1157
1369
  // Optimistically inject into caches so UI updates immediately
1158
1370
  queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: any) => {
1159
1371
  const docs: Document[] =
1160
1372
  old?.documents ?? (Array.isArray(old) ? old : []);
1161
- return { documents: [...docs, tempDoc] };
1373
+ return withDocumentsCacheShape(old, [...docs, tempDoc]);
1162
1374
  });
1163
1375
  queryClient.setQueryData(["action", "get-document", { id }], tempDoc);
1164
1376
  if (rootFilesDatabaseId) {
@@ -1207,7 +1419,11 @@ export function DocumentSidebar({
1207
1419
  });
1208
1420
  }
1209
1421
  } catch (err) {
1210
- // Revert optimistic updates
1422
+ rollbackOptimisticCreatedDocument(
1423
+ queryClient,
1424
+ id,
1425
+ previousDocuments !== undefined,
1426
+ );
1211
1427
  queryClient.invalidateQueries({
1212
1428
  queryKey: ["action", "list-documents"],
1213
1429
  });
@@ -1218,7 +1434,10 @@ export function DocumentSidebar({
1218
1434
  (current) => removeOptimisticItemFromContentDatabase(current, id),
1219
1435
  );
1220
1436
  }
1221
- navigate("/");
1437
+ navigate(previousPath, {
1438
+ replace: true,
1439
+ flushSync: true,
1440
+ });
1222
1441
  toast.error(t("sidebar.failedCreatePage"), {
1223
1442
  description:
1224
1443
  err instanceof Error ? err.message : t("empty.genericError"),
@@ -1228,6 +1447,9 @@ export function DocumentSidebar({
1228
1447
  [
1229
1448
  createDocument,
1230
1449
  localFileMode,
1450
+ location.hash,
1451
+ location.pathname,
1452
+ location.search,
1231
1453
  navigate,
1232
1454
  navigateToDocument,
1233
1455
  onNavigate,
@@ -1284,6 +1506,13 @@ export function DocumentSidebar({
1284
1506
  navigationCandidates.find((doc) => doc.isFavorite) ??
1285
1507
  [...navigationCandidates].sort(compareDocumentsByPosition)[0] ??
1286
1508
  null;
1509
+ const previousDocuments = queryClient.getQueryData(
1510
+ LIST_DOCUMENTS_QUERY_KEY,
1511
+ );
1512
+ const previousDocumentQueries = [...deletedIds].flatMap((deletedId) =>
1513
+ queryClient.getQueriesData(documentQueryFilter(deletedId)),
1514
+ );
1515
+ const previousPath = `${location.pathname}${location.search}${location.hash}`;
1287
1516
 
1288
1517
  queryClient.setQueryData(LIST_DOCUMENTS_QUERY_KEY, (old: unknown) => {
1289
1518
  const cachedDocs: Document[] =
@@ -1317,11 +1546,17 @@ export function DocumentSidebar({
1317
1546
  queryKey: ["action", "list-documents"],
1318
1547
  });
1319
1548
  } catch (err) {
1549
+ restoreDeletedDocumentSnapshots(
1550
+ queryClient,
1551
+ previousDocuments,
1552
+ previousDocumentQueries,
1553
+ deletedIds,
1554
+ );
1320
1555
  queryClient.invalidateQueries({
1321
1556
  queryKey: ["action", "list-documents"],
1322
1557
  });
1323
- if (activeDeleted && activeDocumentId) {
1324
- navigate(`/page/${activeDocumentId}`, {
1558
+ if (activeDeleted) {
1559
+ navigate(previousPath, {
1325
1560
  replace: true,
1326
1561
  flushSync: true,
1327
1562
  });
@@ -1338,6 +1573,9 @@ export function DocumentSidebar({
1338
1573
  deleteDocument,
1339
1574
  documents,
1340
1575
  localFileMode,
1576
+ location.hash,
1577
+ location.pathname,
1578
+ location.search,
1341
1579
  navigate,
1342
1580
  queryClient,
1343
1581
  ],