From c580a7e4181e03a4bb7a35f84d83e65628fa30bb Mon Sep 17 00:00:00 2001 From: David Montero Date: Tue, 9 Jun 2026 15:49:14 +0200 Subject: [PATCH] feat(library-manager): read-only libraries.json file + drop Uninstall for shared index libs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (1) The explorer's per-board manifest entry is renamed velxio.json -> libraries.json and clicking it now opens a READ-ONLY JSON view of that board's declared libraries (board.libraries) in the editor, instead of the modal. New editor state manifestViewBoardId: when set, CodeEditor renders a read-only Monaco showing {libraries:[...]} live; opening/activating any real file clears it. No file is added to the workspace, so nothing touches compile or save. Library actions are done in the Library Manager modal (toolbar button). (2) Drop the 'Uninstall' button for shared index/cache libraries — you can't uninstall a copy everyone shares (content-addressed cache). Only your own custom .zip uploads keep a 'Remove' (per-user store). Index libs: just Add to / In project. --- frontend/src/components/editor/CodeEditor.tsx | 34 ++++++++++++++++++- .../src/components/editor/FileExplorer.tsx | 16 ++++++--- .../simulator/LibraryManagerModal.tsx | 34 +++++-------------- frontend/src/store/useEditorStore.ts | 10 ++++++ 4 files changed, 62 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/editor/CodeEditor.tsx b/frontend/src/components/editor/CodeEditor.tsx index b9536757..6efaa17b 100644 --- a/frontend/src/components/editor/CodeEditor.tsx +++ b/frontend/src/components/editor/CodeEditor.tsx @@ -1,5 +1,6 @@ import Editor from '@monaco-editor/react'; import { useEditorStore } from '../../store/useEditorStore'; +import { useSimulatorStore } from '../../store/useSimulatorStore'; import { registerRetroAsm, LANGUAGE_ID as RETRO_ASM_ID } from './retroAsmLanguage'; function getLanguage(filename: string): string { @@ -14,9 +15,40 @@ function getLanguage(filename: string): string { } export const CodeEditor = () => { - const { files, activeFileId, setFileContent, theme, fontSize } = useEditorStore(); + const { files, activeFileId, setFileContent, theme, fontSize, manifestViewBoardId } = + useEditorStore(); + const boards = useSimulatorStore((s) => s.boards); const activeFile = files.find((f) => f.id === activeFileId); + // READ-ONLY libraries.json view (the file explorer's libraries.json entry). + // Shows the active board's declared library manifest as plain-text JSON, live. + // It is read-only on purpose: adding/removing libraries is done from the + // Library Manager modal, which edits board.libraries (this just reflects it). + if (manifestViewBoardId) { + const b = boards.find((x) => x.id === manifestViewBoardId); + const content = JSON.stringify({ libraries: b?.libraries ?? [] }, null, 2); + return ( +
+ +
+ ); + } + return (
= ({ onSaveClick, onNewCl deleteFile, renameFile, setActiveGroup, + manifestViewBoardId, + setManifestView, } = useEditorStore(); const boards = useSimulatorStore((s) => s.boards); const activeBoardId = useSimulatorStore((s) => s.activeBoardId); @@ -741,17 +743,21 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl Clicking switches to the board and opens the Library Manager on its list. */}
{ switchToBoard(board.id, groupId); - window.dispatchEvent(new CustomEvent('velxio-open-library-manager')); + // Open the READ-ONLY libraries.json view (not the modal). + // Library actions happen in the Library Manager modal. + setManifestView(board.id); }} - title={`Libraries for ${boardDisplayName(board)} — click to manage (compile scope)`} + title={`libraries.json — ${boardDisplayName(board)}'s declared libraries (read-only; manage from the Library Manager)`} > - + - velxio.json + libraries.json = ({ isOpen [install, addToManifest, activeBoard], ); - const uninstall = useCallback( - async (name: string) => { - setBusyLib(name); - setStatusMsg(null); - try { - const result = await uninstallLibrary(name); - if (result.success) { - setStatusMsg({ type: 'success', text: `"${name}" uninstalled.` }); - fetchInstalled(); - } else { - setStatusMsg({ type: 'error', text: result.error || `Failed to uninstall "${name}"` }); - } - } catch (e: unknown) { - setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Uninstall failed' }); - } finally { - setBusyLib(null); - } - }, - [fetchInstalled], - ); - // A CUSTOM lib lives in the user's per-user store, so removing it hits the // per-user delete endpoint and also drops it from the manifest. const removeCustom = useCallback( @@ -446,14 +424,18 @@ export const LibraryManagerModal: React.FC = ({ isOpen {busy ? '…' : '+ Add to project'} )} - {row.installed && ( + {/* Only CUSTOM uploads can be removed (per-user store). Index + libraries live in the shared content-addressed cache — you + add/remove them from THIS project, but never "uninstall" a + copy everyone shares, so no Uninstall button for them. */} + {row.custom && ( )}
diff --git a/frontend/src/store/useEditorStore.ts b/frontend/src/store/useEditorStore.ts index 4871b5b8..94df5221 100644 --- a/frontend/src/store/useEditorStore.ts +++ b/frontend/src/store/useEditorStore.ts @@ -104,6 +104,12 @@ interface EditorState { files: WorkspaceFile[]; activeFileId: string; openFileIds: string[]; + /** When set, the editor shows a READ-ONLY `libraries.json` view of this + * board's library manifest (board.libraries) instead of the active file. + * Cleared whenever a real file is opened/activated. Managed by the explorer's + * libraries.json entry; the Library Manager modal is what edits the manifest. */ + manifestViewBoardId: string | null; + setManifestView: (boardId: string | null) => void; theme: 'vs-dark' | 'light'; fontSize: number; viewMode: EditorViewMode; @@ -159,6 +165,8 @@ export const useEditorStore = create((set, get) => ({ files: [DEFAULT_FILE], activeFileId: MAIN_ID, openFileIds: [MAIN_ID], + manifestViewBoardId: null, + setManifestView: (boardId: string | null) => set({ manifestViewBoardId: boardId }), theme: 'vs-dark', fontSize: 14, viewMode: 'both', @@ -266,6 +274,7 @@ export const useEditorStore = create((set, get) => ({ return { openFileIds: s.openFileIds.includes(id) ? s.openFileIds : [...s.openFileIds, id], activeFileId: id, + manifestViewBoardId: null, // opening a real file exits the libraries.json view openGroupFileIds: { ...s.openGroupFileIds, [groupId]: groupOpenIds.includes(id) ? groupOpenIds : [...groupOpenIds, id], @@ -299,6 +308,7 @@ export const useEditorStore = create((set, get) => ({ const groupId = s.activeGroupId; return { activeFileId: id, + manifestViewBoardId: null, // activating a real file exits the libraries.json view activeGroupFileId: { ...s.activeGroupFileId, [groupId]: id }, }; });