@agent-native/core 0.164.12 → 0.164.17

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 (90) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/netlify.toml +4 -0
  3. package/corpus/templates/clips/actions/request-transcript.ts +10 -6
  4. package/corpus/templates/content/actions/_database-source-utils.ts +25 -2
  5. package/corpus/templates/content/actions/_database-utils.ts +2 -0
  6. package/corpus/templates/content/actions/_local-folder-source.ts +186 -2
  7. package/corpus/templates/content/actions/connect-local-folder-source.ts +23 -8
  8. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +3 -3
  9. package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +2 -2
  10. package/corpus/templates/content/actions/sync-local-folder-source.ts +130 -12
  11. package/corpus/templates/content/app/components/LocalFolderLiveSync.tsx +77 -0
  12. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +298 -3
  13. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +354 -8
  14. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +23 -1
  15. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +81 -1
  16. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +36 -0
  17. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +18 -4
  18. package/corpus/templates/content/app/components/editor/local-source-write-state.ts +52 -0
  19. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +244 -6
  20. package/corpus/templates/content/app/components/sidebar/local-source-hierarchy.ts +189 -0
  21. package/corpus/templates/content/app/hooks/use-create-page.ts +26 -4
  22. package/corpus/templates/content/app/hooks/use-documents.ts +83 -0
  23. package/corpus/templates/content/app/i18n-data.ts +239 -0
  24. package/corpus/templates/content/app/lib/desktop-content-files.ts +57 -1
  25. package/corpus/templates/content/app/lib/local-content-source-files.ts +210 -34
  26. package/corpus/templates/content/app/lib/local-folder-live-sync.ts +191 -0
  27. package/corpus/templates/content/app/root.tsx +2 -1
  28. package/corpus/templates/content/app/routes/_app.local-files.tsx +103 -38
  29. package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +2 -2
  30. package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +3 -3
  31. package/corpus/templates/content/docs/product/roadmap.md +2 -2
  32. package/corpus/templates/content/docs/solutions/2026-08-18-colored-text-highlighting-shape.md +693 -0
  33. package/corpus/templates/content/docs/solutions/2026-08-18-content-navigation-coherence-shape.md +482 -0
  34. package/corpus/templates/content/package.json +1 -0
  35. package/corpus/templates/content/shared/api.ts +25 -0
  36. package/corpus/templates/content/shared/nfm.ts +14 -3
  37. package/corpus/templates/factory/AGENTS.md +24 -29
  38. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +8 -28
  39. package/corpus/templates/factory/actions/get-triage-config.ts +1 -0
  40. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +67 -59
  41. package/corpus/templates/factory/actions/list-triage-items.ts +6 -1
  42. package/corpus/templates/factory/actions/reconcile-triage-run.ts +7 -1
  43. package/corpus/templates/factory/actions/save-triage-config.ts +27 -7
  44. package/corpus/templates/factory/actions/start-builder-for-item.ts +113 -31
  45. package/corpus/templates/factory/actions/view-screen.ts +12 -0
  46. package/corpus/templates/factory/app/components/factory/FactorySettingsView.tsx +457 -0
  47. package/corpus/templates/factory/app/components/factory/FactoryWorkspaceActions.tsx +53 -0
  48. package/corpus/templates/factory/app/components/layout/Layout.tsx +2 -0
  49. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +3 -1
  50. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +10 -7
  51. package/corpus/templates/factory/app/i18n/en-US.ts +3 -0
  52. package/corpus/templates/factory/app/routes/agents.tsx +22 -1
  53. package/corpus/templates/factory/app/routes/factory-settings.tsx +34 -0
  54. package/corpus/templates/factory/app/routes/factory.tsx +33 -323
  55. package/corpus/templates/factory/server/connectors/slack.ts +62 -0
  56. package/corpus/templates/factory/server/db/schema.ts +1 -0
  57. package/corpus/templates/factory/server/lib/slack-feedback-prompt.ts +30 -0
  58. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -8
  59. package/corpus/templates/factory/server/plugins/factory-migrations.ts +7 -0
  60. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +43 -25
  61. package/corpus/templates/factory/server/triage/ai-services-git.ts +2 -0
  62. package/corpus/templates/factory/server/triage/pr-monitor.ts +2 -1
  63. package/corpus/templates/factory/server/triage/pr-policy.ts +197 -19
  64. package/corpus/templates/factory/server/triage/review-skill-alignment.ts +127 -0
  65. package/corpus/templates/factory/server/triage/slack-client.ts +79 -6
  66. package/dist/app-config/auth.d.ts +4 -0
  67. package/dist/app-config/auth.js +7 -0
  68. package/dist/app-config/schema.d.ts +3 -0
  69. package/dist/app-config/schema.js +2 -0
  70. package/dist/cli/code-agent-executor.js +6 -1
  71. package/dist/cli/code-agent-mcp-config.d.ts +2 -2
  72. package/dist/cli/code-agent-mcp-config.js +34 -9
  73. package/dist/client/MultiTabAssistantChat.js +28 -1
  74. package/dist/client/org/OrgSwitcher.js +8 -4
  75. package/dist/client/use-chat-threads.d.ts +1 -0
  76. package/dist/client/use-chat-threads.js +13 -3
  77. package/dist/collab/awareness.d.ts +2 -2
  78. package/dist/collab/routes.d.ts +1 -1
  79. package/dist/mcp-client/config.d.ts +11 -0
  80. package/dist/mcp-client/config.js +33 -26
  81. package/dist/notifications/routes.d.ts +3 -3
  82. package/dist/observability/routes.d.ts +5 -5
  83. package/dist/progress/routes.d.ts +1 -1
  84. package/dist/resources/handlers.d.ts +2 -2
  85. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  86. package/dist/server/auth.js +17 -3
  87. package/dist/server/realtime-token.d.ts +1 -1
  88. package/dist/server/transcribe-voice.d.ts +1 -1
  89. package/dist/vite/client.js +2 -0
  90. package/package.json +1 -1
@@ -76,6 +76,7 @@ import {
76
76
  LocalMdxComponentNode,
77
77
  } from "./extensions/LocalMdxComponentNode";
78
78
  import {
79
+ CompatibleCode,
79
80
  createNotionEditorExtensions,
80
81
  focusMostRecentEmptyToggleSummary,
81
82
  type NotionPageLink,
@@ -771,6 +772,20 @@ interface VisualEditorProps {
771
772
  * whose type has no NFM analog (via the shared registry-block side-map).
772
773
  */
773
774
  notionPageId?: string | null;
775
+ onHistoryControllerChange?: (
776
+ controller: VisualEditorHistoryController | null,
777
+ ) => void;
778
+ onHistoryStateChange?: (state: VisualEditorHistoryState) => void;
779
+ }
780
+
781
+ export interface VisualEditorHistoryState {
782
+ canUndo: boolean;
783
+ canRedo: boolean;
784
+ }
785
+
786
+ export interface VisualEditorHistoryController {
787
+ undo: () => boolean;
788
+ redo: () => boolean;
774
789
  }
775
790
 
776
791
  export type { NotionPageLink };
@@ -968,6 +983,27 @@ function isActiveSlashCommandDraft(editor: CoreEditor): boolean {
968
983
  return /^\s*\/[a-zA-Z0-9]*$/.test(textBefore);
969
984
  }
970
985
 
986
+ export function runPersistableHistoryCommand(
987
+ editor: CoreEditor,
988
+ command: "undo" | "redo",
989
+ ): boolean {
990
+ let applied = false;
991
+ for (let index = 0; index < 20; index += 1) {
992
+ const changed = editor
993
+ .chain()
994
+ .focus()
995
+ .command(({ tr }) => {
996
+ tr.setMeta(LOCAL_FILE_USER_EDIT_META, true);
997
+ return true;
998
+ })
999
+ [command]()
1000
+ .run();
1001
+ applied ||= changed;
1002
+ if (!changed || !isActiveSlashCommandDraft(editor)) break;
1003
+ }
1004
+ return applied;
1005
+ }
1006
+
971
1007
  interface VisualEditorExtensionOptions {
972
1008
  documentId?: string;
973
1009
  ydoc?: YDoc | null;
@@ -1610,6 +1646,7 @@ export function createVisualEditorExtensions({
1610
1646
  },
1611
1647
  starterKit: {
1612
1648
  blockquote: false,
1649
+ code: false,
1613
1650
  paragraph: false,
1614
1651
  heading: { levels: [1, 2, 3, 4, 5, 6] },
1615
1652
  horizontalRule: {},
@@ -1618,6 +1655,7 @@ export function createVisualEditorExtensions({
1618
1655
  collab:
1619
1656
  ydoc || localAwareness ? { ydoc, awareness: localAwareness, user } : null,
1620
1657
  extraExtensions: [
1658
+ CompatibleCode,
1621
1659
  EmptyLineParagraph,
1622
1660
  NotionBlockquote,
1623
1661
  CodeBlock,
@@ -1957,6 +1995,8 @@ export function VisualEditor({
1957
1995
  notionPageLinks = [],
1958
1996
  onOpenNotionPageLink,
1959
1997
  notionPageId,
1998
+ onHistoryControllerChange,
1999
+ onHistoryStateChange,
1960
2000
  }: VisualEditorProps) {
1961
2001
  const t = useT();
1962
2002
  const [isDraggingMedia, setIsDraggingMedia] = useState(false);
@@ -1967,6 +2007,8 @@ export function VisualEditor({
1967
2007
  onChangeRef.current = onChange;
1968
2008
  const onSaveContentRef = useRef(onSaveContent);
1969
2009
  onSaveContentRef.current = onSaveContent;
2010
+ const onHistoryStateChangeRef = useRef(onHistoryStateChange);
2011
+ onHistoryStateChangeRef.current = onHistoryStateChange;
1970
2012
  const notionPageLinksRef = useRef(notionPageLinks);
1971
2013
  notionPageLinksRef.current = notionPageLinks;
1972
2014
  const onMediaSourceCommittedRef = useRef<
@@ -2150,6 +2192,7 @@ export function VisualEditor({
2150
2192
  }
2151
2193
  };
2152
2194
 
2195
+ const historyEditorRef = useRef<CoreEditor | null>(null);
2153
2196
  const editor = useEditor({
2154
2197
  extensions,
2155
2198
  // With Collaboration (ydoc) active, content is owned by the Y.XmlFragment —
@@ -2240,8 +2283,22 @@ export function VisualEditor({
2240
2283
  if (view.editable) markUserEditIntent();
2241
2284
  return false;
2242
2285
  },
2243
- keydown(view) {
2286
+ keydown(view, event) {
2244
2287
  if (view.editable) markUserEditIntent();
2288
+ if (
2289
+ view.editable &&
2290
+ (event.metaKey || event.ctrlKey) &&
2291
+ !event.altKey &&
2292
+ event.key.toLowerCase() === "z" &&
2293
+ historyEditorRef.current
2294
+ ) {
2295
+ event.preventDefault();
2296
+ runPersistableHistoryCommand(
2297
+ historyEditorRef.current,
2298
+ event.shiftKey ? "redo" : "undo",
2299
+ );
2300
+ return true;
2301
+ }
2245
2302
  return false;
2246
2303
  },
2247
2304
  cut(view) {
@@ -2276,6 +2333,12 @@ export function VisualEditor({
2276
2333
  },
2277
2334
  },
2278
2335
  editable,
2336
+ onTransaction: ({ editor }) => {
2337
+ onHistoryStateChangeRef.current?.({
2338
+ canUndo: editor.can().undo(),
2339
+ canRedo: editor.can().redo(),
2340
+ });
2341
+ },
2279
2342
  onUpdate: ({ editor, transaction }) => {
2280
2343
  const guards = guardsRef.current;
2281
2344
  // `shouldIgnoreUpdate` covers: not editable, mid-programmatic setContent,
@@ -2326,6 +2389,23 @@ export function VisualEditor({
2326
2389
  });
2327
2390
  },
2328
2391
  });
2392
+ historyEditorRef.current = editor;
2393
+
2394
+ useEffect(() => {
2395
+ if (!editor) {
2396
+ onHistoryControllerChange?.(null);
2397
+ return;
2398
+ }
2399
+ onHistoryControllerChange?.({
2400
+ undo: () => runPersistableHistoryCommand(editor, "undo"),
2401
+ redo: () => runPersistableHistoryCommand(editor, "redo"),
2402
+ });
2403
+ onHistoryStateChange?.({
2404
+ canUndo: editor.can().undo(),
2405
+ canRedo: editor.can().redo(),
2406
+ });
2407
+ return () => onHistoryControllerChange?.(null);
2408
+ }, [editor, onHistoryControllerChange, onHistoryStateChange]);
2329
2409
 
2330
2410
  const handleImageFileInputChange = useCallback(
2331
2411
  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">
@@ -1,4 +1,5 @@
1
1
  import { findTrailingPlainInlineMath } from "@shared/inline-math";
2
+ import { NFM_COLORS } from "@shared/nfm";
2
3
  import {
3
4
  escapeHtml,
4
5
  indentMarkdown,
@@ -12,6 +13,7 @@ import {
12
13
  IconFileText,
13
14
  } from "@tabler/icons-react";
14
15
  import { InputRule, type Editor } from "@tiptap/core";
16
+ import { Code } from "@tiptap/extension-code";
15
17
  import type { Fragment, Node as ProseMirrorNode } from "@tiptap/pm/model";
16
18
  import { Selection } from "@tiptap/pm/state";
17
19
  import {
@@ -745,12 +747,19 @@ export const NotionSpanMark = Mark.create({
745
747
  renderHTML({ HTMLAttributes }) {
746
748
  const attrs = parseAttrsJson(HTMLAttributes.attrsJson as string);
747
749
  const style: string[] = [];
750
+ const classes: string[] = [];
748
751
 
749
- if (HTMLAttributes.color) {
750
- style.push(`color: ${HTMLAttributes.color}`);
752
+ if (
753
+ NFM_COLORS.has(HTMLAttributes.color) &&
754
+ !HTMLAttributes.color.endsWith("_bg")
755
+ ) {
756
+ classes.push(`notion-block-color--${HTMLAttributes.color}`);
751
757
  }
752
- if (HTMLAttributes.bgColor) {
753
- style.push(`background-color: ${HTMLAttributes.bgColor}`);
758
+ if (
759
+ NFM_COLORS.has(HTMLAttributes.bgColor) &&
760
+ HTMLAttributes.bgColor.endsWith("_bg")
761
+ ) {
762
+ classes.push(`notion-block-bg--${HTMLAttributes.bgColor.slice(0, -3)}`);
754
763
  }
755
764
  if (HTMLAttributes.underline === "true") {
756
765
  style.push("text-decoration: underline");
@@ -764,6 +773,7 @@ export const NotionSpanMark = Mark.create({
764
773
  bg_color: HTMLAttributes.bgColor || undefined,
765
774
  underline: HTMLAttributes.underline || undefined,
766
775
  href: HTMLAttributes.href || undefined,
776
+ class: classes.length ? classes.join(" ") : undefined,
767
777
  style: style.length ? style.join("; ") : undefined,
768
778
  }),
769
779
  0,
@@ -787,6 +797,10 @@ export const NotionSpanMark = Mark.create({
787
797
  },
788
798
  });
789
799
 
800
+ export const CompatibleCode = Code.extend({
801
+ excludes: "",
802
+ });
803
+
790
804
  export const NotionToggle = Node.create({
791
805
  name: "notionToggle",
792
806
  priority: 1000,
@@ -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
  ],