@aprovan/patchwork-web 0.1.0-dev.4d82df8 → 0.1.0-dev.879ed82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -9,8 +9,8 @@
9
9
  href="https://raw.githubusercontent.com/AprovanLabs/aprovan.com/main/docs/assets/social-labs.png"
10
10
  />
11
11
  <title>Patchwork</title>
12
- <script type="module" crossorigin src="/assets/index-B_lEiI-9.js"></script>
13
- <link rel="stylesheet" crossorigin href="/assets/index-CQT9NtoK.css">
12
+ <script type="module" crossorigin src="/assets/index-j3SEx7i5.js"></script>
13
+ <link rel="stylesheet" crossorigin href="/assets/index-Bc9C0wOZ.css">
14
14
  </head>
15
15
  <body class="min-h-screen bg-background font-sans antialiased">
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aprovan/patchwork-web",
3
- "version": "0.1.0-dev.4d82df8",
3
+ "version": "0.1.0-dev.879ed82",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@ai-sdk/react": "^3.0.69",
@@ -26,9 +26,9 @@
26
26
  "remark-gfm": "^4.0.1",
27
27
  "tailwind-merge": "^3.4.0",
28
28
  "tiptap-markdown": "^0.9.0",
29
- "@aprovan/bobbin": "0.1.0-dev.4d82df8",
30
- "@aprovan/patchwork-compiler": "0.1.2-dev.4d82df8",
31
- "@aprovan/patchwork-editor": "0.1.2-dev.4d82df8"
29
+ "@aprovan/bobbin": "0.1.0-dev.879ed82",
30
+ "@aprovan/patchwork-compiler": "0.1.2-dev.879ed82",
31
+ "@aprovan/patchwork-editor": "0.1.2-dev.879ed82"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@tailwindcss/typography": "^0.5.19",
@@ -21,11 +21,6 @@ function getStore(): VFSStore {
21
21
  return storeInstance;
22
22
  }
23
23
 
24
- /** Tear down the current VFS store instance so the next call to getStore() creates a fresh one. */
25
- export function resetStore(): void {
26
- storeInstance = null;
27
- }
28
-
29
24
  function normalizePath(path: string): string {
30
25
  return path.replace(/^\/+|\/+$/g, '');
31
26
  }
@@ -52,7 +52,6 @@ import {
52
52
  } from "@/components/ui/dialog";
53
53
  import { Input } from "@/components/ui/input";
54
54
  import { ScrollArea } from "@/components/ui/scroll-area";
55
- import WorkspaceSwitcher from "@/components/WorkspaceSwitcher";
56
55
  import {
57
56
  listWorkspaceEntries,
58
57
  listWorkspacePaths,
@@ -62,7 +61,6 @@ import {
62
61
  createSingleWorkspaceFileProject,
63
62
  saveWorkspaceProject,
64
63
  subscribeToWorkspaceChanges,
65
- resetStore,
66
64
  } from "@/lib/workspace-vfs";
67
65
 
68
66
  const APROVAN_LOGO =
@@ -363,16 +361,11 @@ function toProjectRelativePath(projectId: string, path: string): string {
363
361
  return normalizedPath;
364
362
  }
365
363
 
366
- const TABS_KEY_PREFIX = 'patchwork:open-tabs';
367
- const ACTIVE_WORKSPACE_KEY = 'patchwork:active-workspace';
364
+ const TABS_STORAGE_KEY = 'patchwork:open-tabs';
368
365
 
369
- function getTabsStorageKey(workspaceId: string | null): string {
370
- return workspaceId ? `${TABS_KEY_PREFIX}:${workspaceId}` : TABS_KEY_PREFIX;
371
- }
372
-
373
- function loadPersistedTabState(workspaceId: string | null): { paths: string[]; activePath: string | null } {
366
+ function loadPersistedTabState(): { paths: string[]; activePath: string | null } {
374
367
  try {
375
- const raw = localStorage.getItem(getTabsStorageKey(workspaceId));
368
+ const raw = localStorage.getItem(TABS_STORAGE_KEY);
376
369
  if (!raw) return { paths: [], activePath: null };
377
370
  const parsed = JSON.parse(raw);
378
371
  return {
@@ -384,8 +377,8 @@ function loadPersistedTabState(workspaceId: string | null): { paths: string[]; a
384
377
  }
385
378
  }
386
379
 
387
- function persistTabState(paths: string[], activePath: string | null, workspaceId: string | null) {
388
- localStorage.setItem(getTabsStorageKey(workspaceId), JSON.stringify({ paths, activePath }));
380
+ function persistTabState(paths: string[], activePath: string | null) {
381
+ localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify({ paths, activePath }));
389
382
  }
390
383
 
391
384
  export default function ChatPage() {
@@ -401,9 +394,6 @@ export default function ChatPage() {
401
394
  const [workspaceTreeVersion, setWorkspaceTreeVersion] = useState(0);
402
395
  const [workspaceLoading, setWorkspaceLoading] = useState(false);
403
396
  const [workspaceError, setWorkspaceError] = useState<string | null>(null);
404
- const [activeWorkspaceId, setActiveWorkspaceId] = useState<string | null>(
405
- () => localStorage.getItem(ACTIVE_WORKSPACE_KEY),
406
- );
407
397
  const [chatContainer, setChatContainer] = useState<HTMLDivElement | null>(
408
398
  null,
409
399
  );
@@ -415,13 +405,11 @@ export default function ChatPage() {
415
405
  const [openTabs, setOpenTabs] = useState<
416
406
  Map<string, { code: string; loading: boolean; error: string | null }>
417
407
  >(() => {
418
- const wsId = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
419
- const { paths } = loadPersistedTabState(wsId);
408
+ const { paths } = loadPersistedTabState();
420
409
  return new Map(paths.map((p) => [p, { code: '', loading: true, error: null }]));
421
410
  });
422
411
  const [activeTabPath, setActiveTabPath] = useState<string | null>(() => {
423
- const wsId = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
424
- const { paths, activePath } = loadPersistedTabState(wsId);
412
+ const { paths, activePath } = loadPersistedTabState();
425
413
  if (activePath && paths.includes(activePath)) return activePath;
426
414
  return paths[0] ?? null;
427
415
  });
@@ -563,10 +551,10 @@ export default function ChatPage() {
563
551
  });
564
552
  }, []);
565
553
 
566
- // Persist open tabs to localStorage (scoped by active workspace)
554
+ // Persist open tabs to localStorage
567
555
  useEffect(() => {
568
- persistTabState([...openTabs.keys()], activeTabPath, activeWorkspaceId);
569
- }, [openTabs, activeTabPath, activeWorkspaceId]);
556
+ persistTabState([...openTabs.keys()], activeTabPath);
557
+ }, [openTabs, activeTabPath]);
570
558
 
571
559
  // Fix activeTabPath when its tab is removed
572
560
  useEffect(() => {
@@ -699,37 +687,6 @@ export default function ChatPage() {
699
687
  setActiveTabPath(null);
700
688
  }, []);
701
689
 
702
- const handleWorkspaceSwitch = useCallback(
703
- (newWorkspaceId: string) => {
704
- localStorage.setItem(ACTIVE_WORKSPACE_KEY, newWorkspaceId);
705
- setActiveWorkspaceId(newWorkspaceId);
706
- setOpenTabs(new Map());
707
- setActiveTabPath(null);
708
- setPinnedPaths(new Map());
709
- setEditSession(null);
710
- resetStore();
711
- void refreshWorkspace();
712
- },
713
- [refreshWorkspace],
714
- );
715
-
716
- const handleWorkspaceLoad = useCallback(
717
- (serverActiveId: string | null) => {
718
- if (!serverActiveId) return;
719
- const storedId = localStorage.getItem(ACTIVE_WORKSPACE_KEY);
720
- if (serverActiveId === storedId) return;
721
- // Server and localStorage disagree — trust the server
722
- localStorage.setItem(ACTIVE_WORKSPACE_KEY, serverActiveId);
723
- setActiveWorkspaceId(serverActiveId);
724
- setOpenTabs(new Map());
725
- setActiveTabPath(null);
726
- setPinnedPaths(new Map());
727
- resetStore();
728
- void refreshWorkspace();
729
- },
730
- [refreshWorkspace],
731
- );
732
-
733
690
  const filteredWorkspaceFiles = useMemo(() => {
734
691
  const query = workspaceFilter.trim().toLowerCase();
735
692
  if (!query) return workspaceFiles;
@@ -791,10 +748,6 @@ export default function ChatPage() {
791
748
  className="h-8 w-8 rounded-full"
792
749
  />
793
750
  <span className="text-lg">patchwork</span>
794
- <WorkspaceSwitcher
795
- onLoad={handleWorkspaceLoad}
796
- onSwitch={handleWorkspaceSwitch}
797
- />
798
751
  <ServicesInspector
799
752
  namespaces={namespaces}
800
753
  services={services}
@@ -1,125 +0,0 @@
1
- import { ChevronDown, Loader2 } from "lucide-react";
2
- import { useEffect, useRef, useState } from "react";
3
-
4
- export interface Workspace {
5
- workspaceId: string;
6
- name: string;
7
- plan: string;
8
- active: boolean;
9
- }
10
-
11
- interface WorkspaceSwitcherProps {
12
- onLoad?: (activeWorkspaceId: string | null) => void;
13
- onSwitch: (workspaceId: string) => void;
14
- }
15
-
16
- export default function WorkspaceSwitcher({ onLoad, onSwitch }: WorkspaceSwitcherProps) {
17
- const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
18
- const [activeId, setActiveId] = useState<string | null>(null);
19
- const [open, setOpen] = useState(false);
20
- const [loading, setLoading] = useState(true);
21
- const [switching, setSwitching] = useState(false);
22
- const containerRef = useRef<HTMLDivElement>(null);
23
- const onLoadRef = useRef(onLoad);
24
- onLoadRef.current = onLoad;
25
-
26
- useEffect(() => {
27
- void fetch("/api/workspaces")
28
- .then((r) => r.json())
29
- .then((data: { workspaces: Workspace[]; activeWorkspaceId: string | null }) => {
30
- setWorkspaces(data.workspaces ?? []);
31
- setActiveId(data.activeWorkspaceId);
32
- onLoadRef.current?.(data.activeWorkspaceId);
33
- })
34
- .catch(() => { onLoadRef.current?.(null); })
35
- .finally(() => setLoading(false));
36
- }, []);
37
-
38
- useEffect(() => {
39
- function handleClick(e: MouseEvent) {
40
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
41
- setOpen(false);
42
- }
43
- }
44
- document.addEventListener("mousedown", handleClick);
45
- return () => document.removeEventListener("mousedown", handleClick);
46
- }, []);
47
-
48
- const active = workspaces.find((w) => w.workspaceId === activeId) ?? workspaces[0];
49
-
50
- async function handleSelect(workspaceId: string) {
51
- if (workspaceId === activeId || switching) return;
52
- setOpen(false);
53
- setSwitching(true);
54
- try {
55
- await fetch("/api/workspaces/active", {
56
- method: "PUT",
57
- headers: { "Content-Type": "application/json" },
58
- body: JSON.stringify({ workspaceId }),
59
- });
60
- setActiveId(workspaceId);
61
- setWorkspaces((prev) =>
62
- prev.map((w) => ({ ...w, active: w.workspaceId === workspaceId })),
63
- );
64
- onSwitch(workspaceId);
65
- } catch {
66
- // silently ignore — the current workspace stays active
67
- } finally {
68
- setSwitching(false);
69
- }
70
- }
71
-
72
- if (loading) {
73
- return (
74
- <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
75
- <Loader2 className="h-3 w-3 animate-spin" />
76
- </div>
77
- );
78
- }
79
-
80
- if (workspaces.length <= 1) {
81
- return (
82
- <span className="text-xs text-muted-foreground truncate max-w-[160px]">
83
- {active?.name ?? ""}
84
- </span>
85
- );
86
- }
87
-
88
- return (
89
- <div ref={containerRef} className="relative">
90
- <button
91
- onClick={() => setOpen((o) => !o)}
92
- disabled={switching}
93
- className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors max-w-[160px]"
94
- title={active?.name}
95
- >
96
- {switching ? (
97
- <Loader2 className="h-3 w-3 animate-spin shrink-0" />
98
- ) : null}
99
- <span className="truncate">{active?.name ?? ""}</span>
100
- <ChevronDown className="h-3 w-3 shrink-0" />
101
- </button>
102
-
103
- {open && (
104
- <div className="absolute left-0 top-full mt-1 z-50 min-w-[180px] rounded-md border bg-popover shadow-md">
105
- {workspaces.map((w) => (
106
- <button
107
- key={w.workspaceId}
108
- onClick={() => void handleSelect(w.workspaceId)}
109
- className={`w-full text-left px-3 py-2 text-xs hover:bg-muted transition-colors first:rounded-t-md last:rounded-b-md ${
110
- w.workspaceId === activeId
111
- ? "font-medium text-foreground"
112
- : "text-muted-foreground"
113
- }`}
114
- >
115
- <div className="truncate">{w.name}</div>
116
- <div className="text-[10px] text-muted-foreground/60 uppercase tracking-wide">
117
- {w.plan}
118
- </div>
119
- </button>
120
- ))}
121
- </div>
122
- )}
123
- </div>
124
- );
125
- }