Skip to content

Commit d7af34b

Browse files
committed
feat: local workspace save
1 parent 398b780 commit d7af34b

7 files changed

Lines changed: 388 additions & 215 deletions

File tree

app/components/EditorArea.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,26 @@ const props = defineProps<{
4646
activePaneId: string;
4747
isMobile: boolean;
4848
loadingPaneId?: string | null;
49+
initialSplitRatio?: number;
4950
}>();
5051
5152
const emit = defineEmits<{
5253
(e: "update:pane", paneId: string, field: string, value: string): void;
5354
(e: "set-active", paneId: string): void;
5455
(e: "drop", zone: "left" | "center" | "right", filePath: string): void;
5556
(e: "close-pane", paneId: string): void;
57+
(e: "update:splitRatio", ratio: number): void;
5658
}>();
5759
5860
const skeletonWidths = [10, 20, 33, 45, 72, 30, 60, 70, 55, 65, 20, 33, 10];
5961
const containerRef = ref<HTMLDivElement | null>(null);
6062
const dragging = ref(false);
6163
const dropZone = ref<"left" | "center" | "right" | null>(null);
62-
const splitRatio = ref(
63-
import.meta.client ? parseInt(localStorage.getItem("locode:splitRatio") || "50") : 50
64-
);
64+
const splitRatio = ref(props.initialSplitRatio ?? 50);
65+
66+
watch(() => props.initialSplitRatio, (val) => {
67+
if (val !== undefined) splitRatio.value = val;
68+
});
6569
let dragCounter = 0;
6670
6771
// --- Editor refs for focus ---
@@ -142,7 +146,7 @@ function startSplitResize() {
142146
const cleanup = () => {
143147
document.body.style.cursor = "";
144148
document.body.style.userSelect = "";
145-
localStorage.setItem("locode:splitRatio", String(Math.round(splitRatio.value)));
149+
emit("update:splitRatio", Math.round(splitRatio.value));
146150
document.removeEventListener("mousemove", onMouseMove);
147151
document.removeEventListener("mouseup", cleanup);
148152
splitCleanup = null;

app/components/FileExplorer.vue

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,20 @@
138138
</style>
139139

140140
<script setup lang="ts">
141-
const props = defineProps<{ openFiles: string[], rootPath: string }>();
142-
143-
const storageKey = computed(() =>
144-
props.rootPath ? `locode:openFolders:${props.rootPath}` : "locode:openFolders"
145-
);
141+
import type { SkeletonNode } from '~/composables/useLocodeConfig'
142+
import { DEFAULT_SKELETON } from '~/composables/useLocodeConfig'
143+
144+
const props = defineProps<{
145+
openFiles: string[];
146+
rootPath: string;
147+
initialOpenFolders?: string[];
148+
initialSkeleton?: SkeletonNode[];
149+
}>();
146150
const emit = defineEmits<{
147151
(e: "select-file", path: string): void,
148-
(e: "select-root", path: string): void
152+
(e: "select-root", path: string): void,
153+
(e: "update:openFolders", folders: string[]): void,
154+
(e: "update:skeleton", skeleton: SkeletonNode[]): void,
149155
}>();
150156
151157
const tree = ref<any[]>([]);
@@ -154,28 +160,12 @@ const browsing = ref(false);
154160
const treeLoading = ref(false);
155161
156162
// --- Skeleton blueprint ---
157-
type SkeletonNode = { depth: number; type: string; width: number };
158-
const DEFAULT_SKELETON: SkeletonNode[] = [
159-
{ depth: 0, type: "dir", width: 55 },
160-
{ depth: 1, type: "file", width: 45 },
161-
{ depth: 1, type: "file", width: 50 },
162-
{ depth: 0, type: "dir", width: 60 },
163-
{ depth: 0, type: "file", width: 40 },
164-
];
165-
166-
function readSkeletonFromStorage(): SkeletonNode[] {
167-
if (!import.meta.client || !props.rootPath) return DEFAULT_SKELETON;
168-
try {
169-
const saved = localStorage.getItem(`locode:skeleton:${props.rootPath}`);
170-
if (saved) {
171-
const parsed = JSON.parse(saved);
172-
if (Array.isArray(parsed) && parsed.length > 0) return parsed;
173-
}
174-
} catch {}
163+
function readSkeletonFromProps(): SkeletonNode[] {
164+
if (props.initialSkeleton && props.initialSkeleton.length > 0) return props.initialSkeleton;
175165
return DEFAULT_SKELETON;
176166
}
177167
178-
const skeletonBlueprint = ref<SkeletonNode[]>(readSkeletonFromStorage());
168+
const skeletonBlueprint = ref<SkeletonNode[]>(readSkeletonFromProps());
179169
180170
function flattenTree(nodes: any[], depth = 0): SkeletonNode[] {
181171
const result: SkeletonNode[] = [];
@@ -193,7 +183,7 @@ function saveSkeletonBlueprint() {
193183
if (!props.rootPath) return;
194184
const blueprint = flattenTree(tree.value);
195185
if (blueprint.length > 0) {
196-
localStorage.setItem(`locode:skeleton:${props.rootPath}`, JSON.stringify(blueprint));
186+
emit("update:skeleton", blueprint);
197187
}
198188
}
199189
@@ -237,7 +227,7 @@ function getOpenPaths(nodes: any[]): string[] {
237227
}
238228
239229
function saveOpenFolders() {
240-
localStorage.setItem(storageKey.value, JSON.stringify(getOpenPaths(tree.value)));
230+
emit("update:openFolders", getOpenPaths(tree.value));
241231
}
242232
243233
async function restoreOpenFolders(nodes: any[], openPaths: Set<string>) {
@@ -254,12 +244,9 @@ async function loadWorkTree() {
254244
treeLoading.value = true;
255245
try {
256246
tree.value = await loadTree(props.rootPath);
257-
const saved = localStorage.getItem(storageKey.value);
258-
if (saved) {
259-
try {
260-
const openPaths = new Set<string>(JSON.parse(saved));
261-
await restoreOpenFolders(tree.value, openPaths);
262-
} catch {}
247+
if (props.initialOpenFolders && props.initialOpenFolders.length > 0) {
248+
const openPaths = new Set<string>(props.initialOpenFolders);
249+
await restoreOpenFolders(tree.value, openPaths);
263250
}
264251
saveSkeletonBlueprint();
265252
} finally {
@@ -343,13 +330,28 @@ function onEscape(e: KeyboardEvent) {
343330
watch(() => props.rootPath, (newPath) => {
344331
if (newPath) {
345332
browsing.value = false;
346-
skeletonBlueprint.value = readSkeletonFromStorage();
333+
skeletonBlueprint.value = readSkeletonFromProps();
347334
loadWorkTree();
348335
}
349336
});
350337
338+
// When parent pushes fresh config values (workspace switch), update skeleton
339+
watch(() => props.initialSkeleton, (val) => {
340+
if (val && val.length > 0) skeletonBlueprint.value = val;
341+
});
342+
343+
// One-shot: re-apply open folders when config loads after the tree is already mounted
344+
// (happens on initial page load when rootPath is pre-set but config loads async)
345+
const unwatchInitialFolders = watch(() => props.initialOpenFolders, async (newVal, oldVal) => {
346+
// Only fire when going from empty → non-empty (config first load, not workspace switch)
347+
if (!newVal?.length || oldVal?.length || treeLoading.value || !tree.value.length) return;
348+
unwatchInitialFolders();
349+
await restoreOpenFolders(tree.value, new Set(newVal));
350+
saveSkeletonBlueprint();
351+
}, { immediate: false });
352+
351353
onMounted(async () => {
352-
skeletonBlueprint.value = readSkeletonFromStorage();
354+
skeletonBlueprint.value = readSkeletonFromProps();
353355
treeLoading.value = true;
354356
if (!props.rootPath) {
355357
browsing.value = true;

app/components/Terminal.client.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import "@xterm/xterm/css/xterm.css";
1111
const props = defineProps<{
1212
cwd: string;
1313
active: boolean;
14+
focused: boolean;
1415
}>();
1516
1617
const termContainer = ref<HTMLDivElement | null>(null);
@@ -105,8 +106,8 @@ onMounted(async () => {
105106
});
106107
resizeObserver.observe(termContainer.value);
107108
108-
// Auto-focus if mounted as the active terminal
109-
if (props.active) {
109+
// Auto-focus only for the explicitly focused terminal
110+
if (props.focused) {
110111
term.focus();
111112
}
112113
});
@@ -120,7 +121,6 @@ watch(() => props.active, (active) => {
120121
nextTick(() => {
121122
if (!termContainer.value || termContainer.value.offsetHeight === 0) return;
122123
fitAddon!.fit();
123-
term!.focus();
124124
});
125125
}
126126
});

0 commit comments

Comments
 (0)