@aprovan/patchwork-web 0.1.0-dev.4d82df8 → 0.1.0-dev.78c0b14
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/.turbo/turbo-build.log +5 -5
- package/dist/assets/{index-CQT9NtoK.css → index-Bc9C0wOZ.css} +1 -1
- package/dist/assets/{index-B_lEiI-9.js → index-j3SEx7i5.js} +188 -188
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/lib/workspace-vfs.ts +0 -5
- package/src/pages/ChatPage.tsx +10 -57
- package/src/components/WorkspaceSwitcher.tsx +0 -125
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-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
3
|
+
"version": "0.1.0-dev.78c0b14",
|
|
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/
|
|
30
|
-
"@aprovan/patchwork-compiler": "0.1.2-dev.
|
|
31
|
-
"@aprovan/
|
|
29
|
+
"@aprovan/patchwork-editor": "0.1.2-dev.78c0b14",
|
|
30
|
+
"@aprovan/patchwork-compiler": "0.1.2-dev.78c0b14",
|
|
31
|
+
"@aprovan/bobbin": "0.1.0-dev.78c0b14"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@tailwindcss/typography": "^0.5.19",
|
package/src/lib/workspace-vfs.ts
CHANGED
|
@@ -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
|
}
|
package/src/pages/ChatPage.tsx
CHANGED
|
@@ -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
|
|
367
|
-
const ACTIVE_WORKSPACE_KEY = 'patchwork:active-workspace';
|
|
364
|
+
const TABS_STORAGE_KEY = 'patchwork:open-tabs';
|
|
368
365
|
|
|
369
|
-
function
|
|
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(
|
|
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
|
|
388
|
-
localStorage.setItem(
|
|
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
|
|
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
|
|
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
|
|
554
|
+
// Persist open tabs to localStorage
|
|
567
555
|
useEffect(() => {
|
|
568
|
-
persistTabState([...openTabs.keys()], activeTabPath
|
|
569
|
-
}, [openTabs, activeTabPath
|
|
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
|
-
}
|