diff --git a/electron/lib/core/appMenu.cjs b/electron/lib/core/appMenu.cjs index 91403ee..bf80b74 100644 --- a/electron/lib/core/appMenu.cjs +++ b/electron/lib/core/appMenu.cjs @@ -1,4 +1,4 @@ -const { Menu } = require("electron"); +const { Menu, app } = require("electron"); // Sends a menu-action signal to the renderer for the given window. function sendMenuAction(win, action) { @@ -69,9 +69,23 @@ function buildAppMenuTemplate(win, context = {}) { const fileSubmenu = screen === "document" ? [ { - label: "New Note", - accelerator: "CmdOrCtrl+N", - click: () => sendMenuAction(win, "new-note") + label: "New", + submenu: [ + { + label: "Note", + accelerator: "CmdOrCtrl+N", + click: () => sendMenuAction(win, "new-note") + }, + { + label: "Folder", + click: () => sendMenuAction(win, "new-folder") + }, + { type: "separator" }, + { + label: "Workspace", + click: () => sendMenuAction(win, "new-workspace") + } + ] }, { label: "Open Workspace", @@ -127,13 +141,31 @@ function buildAppMenuTemplate(win, context = {}) { click: () => sendMenuAction(win, "back-to-notes") }, { type: "separator" }, - { role: "quit" } + { + label: "Restart Notely", + click: () => sendMenuAction(win, "restart-app") + }, + { role: "quit", click: () => app.quit() } ] : [ { - label: "New Note", - accelerator: "CmdOrCtrl+N", - click: () => sendMenuAction(win, "new-note") + label: "New", + submenu: [ + { + label: "Note", + accelerator: "CmdOrCtrl+N", + click: () => sendMenuAction(win, "new-note") + }, + { + label: "Folder", + click: () => sendMenuAction(win, "new-folder") + }, + { type: "separator" }, + { + label: "Workspace", + click: () => sendMenuAction(win, "new-workspace") + } + ] }, { label: "Open Workspace", @@ -162,7 +194,11 @@ function buildAppMenuTemplate(win, context = {}) { click: () => sendMenuAction(win, "remove-folder") }, { type: "separator" }, - { role: "quit" } + { + label: "Restart Notely", + click: () => sendMenuAction(win, "restart-app") + }, + { role: "quit", click: () => app.quit() } ]; const editSubmenu = [ @@ -504,11 +540,23 @@ function buildAppMenuTemplate(win, context = {}) { { label: "Workspace", submenu: [ + { + label: "Workspace Information", + click: () => sendMenuAction(win, "open-workspace-info") + }, { label: "Workspace Activity", accelerator: "CmdOrCtrl+Shift+A", click: () => sendMenuAction(win, "open-workspace-activity") }, + { + label: "Assets Library", + click: () => sendMenuAction(win, "open-assets") + }, + { + label: "Trash / Removed Items", + click: () => sendMenuAction(win, "open-trash") + }, { label: "Reload Workspace", accelerator: "CmdOrCtrl+Alt+R", diff --git a/electron/lib/core/workspaceMetadata.cjs b/electron/lib/core/workspaceMetadata.cjs index 30e8363..ac3bf29 100644 --- a/electron/lib/core/workspaceMetadata.cjs +++ b/electron/lib/core/workspaceMetadata.cjs @@ -26,10 +26,25 @@ class WorkspaceMetadata { if (this.fs.existsSync(p)) { this.state = JSON.parse(this.fs.readFileSync(p, "utf8")); } else { - this.state = { items: {} }; + this.state = { items: {}, favorites: [], info: {} }; } } catch { - this.state = { items: {} }; + this.state = { items: {}, favorites: [], info: {} }; + } + if (!this.state.items) this.state.items = {}; + if (!Array.isArray(this.state.favorites)) this.state.favorites = []; + if (!this.state.info || typeof this.state.info !== "object") { + const root = this.getNotesRoot(); + const defaultName = root ? this.path.basename(root) : "Workspace"; + this.state.info = { + name: defaultName, + description: "", + domainTags: [], + projectType: "General", + primaryGoal: "", + icon: "", + color: "", + }; } } @@ -67,6 +82,44 @@ class WorkspaceMetadata { return this.state?.items || {}; } + getFavorites() { + this._load(); + return Array.isArray(this.state?.favorites) ? this.state.favorites : []; + } + + setFavorites(favoritesList) { + this._load(); + this.state.favorites = Array.isArray(favoritesList) ? favoritesList : []; + this._save(); + return this.state.favorites; + } + + getWorkspaceInfo() { + this._load(); + const root = this.getNotesRoot(); + const defaultName = root ? this.path.basename(root) : "Workspace"; + return { + name: this.state.info?.name || defaultName, + description: this.state.info?.description || "", + domainTags: Array.isArray(this.state.info?.domainTags) ? this.state.info.domainTags : [], + projectType: this.state.info?.projectType || "General", + primaryGoal: this.state.info?.primaryGoal || "", + icon: this.state.info?.icon || "", + color: this.state.info?.color || "", + }; + } + + updateWorkspaceInfo(infoPayload) { + this._load(); + const current = this.getWorkspaceInfo(); + this.state.info = { + ...current, + ...infoPayload, + }; + this._save(); + return this.state.info; + } + updateMetadata(absolutePath, { icon, color }) { this._load(); const relPath = this._getRelativePath(absolutePath); @@ -89,8 +142,18 @@ class WorkspaceMetadata { } } +function validateIsWorkspace(fs, path, dirPath) { + if (!dirPath || typeof dirPath !== "string") return false; + try { + const metaFolder = path.join(dirPath, ".notes-app"); + return fs.existsSync(metaFolder) && fs.statSync(metaFolder).isDirectory(); + } catch { + return false; + } +} + function createWorkspaceMetadata(deps) { return new WorkspaceMetadata(deps); } -module.exports = { createWorkspaceMetadata }; +module.exports = { createWorkspaceMetadata, validateIsWorkspace }; diff --git a/electron/lib/git/gitService.cjs b/electron/lib/git/gitService.cjs index 4eed479..f3921f6 100644 --- a/electron/lib/git/gitService.cjs +++ b/electron/lib/git/gitService.cjs @@ -111,17 +111,6 @@ async function initRepo(workspacePath) { await g.init(); - // README - const readmePath = path.join(workspacePath, "README.md"); - if (!fs.existsSync(readmePath)) { - const name = path.basename(workspacePath); - fs.writeFileSync( - readmePath, - `# ${name}\n\nNotes workspace managed by [Notely](https://github.com/WGLabz/notely).\n`, - "utf8" - ); - } - // .gitignore const gitignorePath = path.join(workspacePath, ".gitignore"); if (!fs.existsSync(gitignorePath)) { @@ -1049,13 +1038,10 @@ async function migrateFromLegacy(workspacePath, metadataStore = null) { return ok({ alreadyMigrated: false, migrated: 0, skipped: 0 }); } - // Ensure we have a git repo to commit into + // Check if workspace is a git repository const repoInfo = await getRepoInfo(workspacePath); - if (!repoInfo.ok) return repoInfo; - - if (!repoInfo.data.isRepo) { - const initResult = await initRepo(workspacePath); - if (!initResult.ok) return initResult; + if (!repoInfo.ok || !repoInfo.data.isRepo) { + return ok({ alreadyMigrated: false, migrated: 0, skipped: 0 }); } const repoRoot = repoInfo.data.repoRoot || (await findRepoRoot(workspacePath)); diff --git a/electron/lib/ipc/coreIpc.cjs b/electron/lib/ipc/coreIpc.cjs index f0d0d7d..d315358 100644 --- a/electron/lib/ipc/coreIpc.cjs +++ b/electron/lib/ipc/coreIpc.cjs @@ -542,6 +542,187 @@ function registerCoreIpcHandlers(ipcMain, deps) { return { success: false }; }); + registerTrustedHandler("app:restart", (event) => { + if (!app.isPackaged) { + console.log("\n======================================================="); + console.log("[Notely] Restarting Application & Re-initializing Services (Dev Mode)"); + console.log("=======================================================\n"); + + if (typeof applyNotesRoot === "function" && typeof getNotesRoot === "function") { + const root = getNotesRoot(); + if (root) { + try { + applyNotesRoot(root); + } catch (err) { + console.error("[Restart] Service re-initialization error:", err); + } + } + } + + const win = event?.sender ? BrowserWindow.fromWebContents(event.sender) : null; + if (win && !win.isDestroyed()) { + win.webContents.reload(); + } else { + const windows = BrowserWindow.getAllWindows(); + for (const w of windows) { + if (w && !w.isDestroyed()) { + w.webContents.reload(); + } + } + } + } else { + app.relaunch(); + app.exit(0); + } + return { success: true }; + }); + + registerTrustedHandler("workspace-metadata:get-favorites", () => { + if (typeof getWorkspaceMetadataStore === "function") { + const store = getWorkspaceMetadataStore(); + if (store) return store.getFavorites(); + } + return []; + }); + + registerTrustedHandler("workspace-metadata:set-favorites", (_event, payload) => { + const { favorites } = payload || {}; + if (typeof getWorkspaceMetadataStore === "function") { + const store = getWorkspaceMetadataStore(); + if (store) { + const nextFavorites = store.setFavorites(favorites); + for (const win of BrowserWindow.getAllWindows()) { + if (!win || win.isDestroyed()) continue; + win.webContents.send("workspace-metadata:favorites-changed", nextFavorites); + } + return { success: true, favorites: nextFavorites }; + } + } + return { success: false }; + }); + + registerTrustedHandler("workspace:validate", (_event, payload) => { + const { dirPath } = payload || {}; + const { validateIsWorkspace } = require("../core/workspaceMetadata.cjs"); + return { isWorkspace: validateIsWorkspace(fs, path, dirPath) }; + }); + + registerTrustedHandler("workspace-metadata:get-info", () => { + if (typeof getWorkspaceMetadataStore === "function") { + const store = getWorkspaceMetadataStore(); + if (store) return store.getWorkspaceInfo(); + } + return {}; + }); + + registerTrustedHandler("workspace-metadata:update-info", (_event, payload) => { + if (typeof getWorkspaceMetadataStore === "function") { + const store = getWorkspaceMetadataStore(); + if (store) { + const updated = store.updateWorkspaceInfo(payload || {}); + for (const win of BrowserWindow.getAllWindows()) { + if (!win || win.isDestroyed()) continue; + win.webContents.send("workspace-metadata:info-changed", updated); + } + return { success: true, info: updated }; + } + } + return { success: false }; + }); + + registerTrustedHandler("workspace:create-new", async (_event, payload) => { + const { + name, + parentLocation, + description = "", + domainTags = [], + projectType = "General", + primaryGoal = "", + icon = "", + color = "", + createWelcomeNote = true, + initGit = false, + } = payload || {}; + + if (!name || !name.trim()) { + throw new Error("Workspace name is required."); + } + if (!parentLocation || !parentLocation.trim()) { + throw new Error("Workspace parent location is required."); + } + + const sanitizedName = name.trim().replace(/[\\/:*?"<>|]/g, "_"); + const newWorkspacePath = path.join(parentLocation.trim(), sanitizedName); + const metaAppDir = path.join(newWorkspacePath, ".notes-app"); + + let isNestedWorkspace = false; + let parentWorkspacePath = null; + let curr = path.resolve(parentLocation.trim()); + while (curr) { + const checkMeta = path.join(curr, ".notes-app"); + try { + if (fs.existsSync(checkMeta) && fs.statSync(checkMeta).isDirectory()) { + isNestedWorkspace = true; + parentWorkspacePath = curr; + break; + } + } catch { + /* ignore */ + } + const parentDir = path.dirname(curr); + if (parentDir === curr) break; + curr = parentDir; + } + + ensureDir(newWorkspacePath); + ensureDir(metaAppDir); + + const initialInfo = { + name: name.trim(), + description: description.trim(), + domainTags: Array.isArray(domainTags) ? domainTags : [], + projectType: projectType || "General", + primaryGoal: primaryGoal.trim(), + icon: icon || "", + color: color || "", + }; + + const initialMeta = { + items: {}, + favorites: [], + info: initialInfo, + }; + + const metaJsonPath = path.join(metaAppDir, "metadata.json"); + fs.writeFileSync(metaJsonPath, JSON.stringify(initialMeta, null, 2), "utf8"); + + if (initGit) { + try { + const simpleGit = require("simple-git"); + const git = simpleGit(newWorkspacePath); + await git.init(); + } catch (gitErr) { + console.warn("[Workspace Creation] Git init warning:", gitErr?.message || gitErr); + } + } + + if (createWelcomeNote) { + const readmePath = path.join(newWorkspacePath, "README.md"); + if (!fs.existsSync(readmePath)) { + const readmeContent = `# ${name.trim()}\n\nThis is your Notely workspace.\n\n- **Project Type**: ${initialInfo.projectType}\n- **Description**: ${initialInfo.description || "No description provided."}\n\nStart writing notes or organizing folders!`; + fs.writeFileSync(readmePath, readmeContent, "utf8"); + } + } + + return { + success: true, + workspacePath: newWorkspacePath, + info: initialInfo, + isNestedWorkspace, + parentWorkspacePath, + }; + }); + registerTrustedHandler("shell:open-external", async (_event, payload) => { if (typeof payload?.url === "string" && /^https?:\/\//i.test(payload.url)) { await shell.openExternal(payload.url); diff --git a/electron/main.cjs b/electron/main.cjs index 45cca86..bddc02c 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -469,11 +469,38 @@ function applyNotesRoot(nextRootPath) { console.warn("[startup] Unable to clean up legacy thumbnails folder:", error?.message || error); } - if (previousNotesRoot && previousNotesRoot !== notesRoot) { - console.log(`[Workspace Switch] Re-initializing AI subsystem for new workspace root: ${notesRoot}`); - initializeAIForWorkspace().catch((err) => { + if (previousNotesRoot) { + console.log(`[App Service Reset] Re-initializing AI subsystem for workspace root: ${notesRoot}`); + try { + const workerManager = require("./ai/workerManager.cjs"); + workerManager.shutdownWorker(); + } catch (e) { + console.warn("[Workspace Switch] Failed to shutdown AI worker manager:", e?.message || e); + } + + initializeAIForWorkspace().then(() => { + try { + const workerManager = require("./ai/workerManager.cjs"); + const AIConfig = require("../ai/core/AIConfig"); + const config = new AIConfig(); + const hfToken = config.getAPIKey("huggingface"); + workerManager.startWorker(notesRoot, appDataDir, hfToken); + } catch (err) { + console.warn("[Workspace Switch] Failed to restart AI worker process:", err?.message || err); + } + }).catch((err) => { console.error("[Workspace Switch] Failed to re-initialize AI subsystem:", err?.message || err); }); + + try { + const { BrowserWindow } = require("electron"); + for (const win of BrowserWindow.getAllWindows()) { + if (!win || win.isDestroyed()) continue; + win.webContents.send("workspace:changed", { notesRoot }); + } + } catch (e) { + console.warn("[Workspace Switch] Failed to broadcast workspace:changed:", e?.message || e); + } } metadataStore = createMetadataStore({ @@ -1090,7 +1117,9 @@ ipcMain.on("window:execute-menu-item", (event, { indexPath, role, action }) => { } if (role) { - if (role === "toggleDevTools") { + if (role === "quit") { + app.quit(); + } else if (role === "toggleDevTools") { win.webContents.toggleDevTools(); } else if (role === "reload") { win.webContents.reload(); diff --git a/electron/preload.cjs b/electron/preload.cjs index 8cfa635..67fe3b7 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -10,6 +10,15 @@ contextBridge.exposeInMainWorld("notesApi", { ipcRenderer.on("app-menu:action", listener); return () => ipcRenderer.removeListener("app-menu:action", listener); }, + onAppMenuAction: (callback) => { + if (typeof callback !== "function") { + return () => {}; + } + + const listener = (_event, action) => callback(action); + ipcRenderer.on("app-menu:action", listener); + return () => ipcRenderer.removeListener("app-menu:action", listener); + }, notifyBootReady: () => ipcRenderer.send("app:boot-ready"), notifyBootProgress: (payload) => ipcRenderer.send("app:boot-progress", payload), updateMenuContext: (payload) => ipcRenderer.send("app-menu:update-context", payload), @@ -312,6 +321,31 @@ contextBridge.exposeInMainWorld("notesApi", { ipcRenderer.on("workspace-metadata:changed", listener); return () => ipcRenderer.removeListener("workspace-metadata:changed", listener); }, + getFavorites: () => ipcRenderer.invoke("workspace-metadata:get-favorites"), + setFavorites: (favorites) => ipcRenderer.invoke("workspace-metadata:set-favorites", { favorites }), + onFavoritesChanged: (callback) => { + if (typeof callback !== "function") return () => {}; + const listener = (_event, payload) => callback(payload); + ipcRenderer.on("workspace-metadata:favorites-changed", listener); + return () => ipcRenderer.removeListener("workspace-metadata:favorites-changed", listener); + }, + onWorkspaceChanged: (callback) => { + if (typeof callback !== "function") return () => {}; + const listener = (_event, payload) => callback(payload); + ipcRenderer.on("workspace:changed", listener); + return () => ipcRenderer.removeListener("workspace:changed", listener); + }, + validateWorkspace: (dirPath) => ipcRenderer.invoke("workspace:validate", { dirPath }), + getWorkspaceInfo: () => ipcRenderer.invoke("workspace-metadata:get-info"), + updateWorkspaceInfo: (payload) => ipcRenderer.invoke("workspace-metadata:update-info", payload), + createNewWorkspace: (payload) => ipcRenderer.invoke("workspace:create-new", payload), + onWorkspaceInfoChanged: (callback) => { + if (typeof callback !== "function") return () => {}; + const listener = (_event, payload) => callback(payload); + ipcRenderer.on("workspace-metadata:info-changed", listener); + return () => ipcRenderer.removeListener("workspace-metadata:info-changed", listener); + }, + restartApp: () => ipcRenderer.invoke("app:restart"), exportNotePackage: (payload) => ipcRenderer.invoke("note-package:export", payload), importNotePackage: (payload) => ipcRenderer.invoke("note-package:import", payload), browseExportDestination: (payload) => ipcRenderer.invoke("note-package:browse-export-destination", payload), diff --git a/src/App.jsx b/src/App.jsx index 043aa98..3bbe049 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -26,6 +26,7 @@ const AIHealthPage = lazy(() => import("./components/AIHealthPage")); const AppLogsPage = lazy(() => import("./components/AppLogsPage")); import { SettingsModal } from "./components/SettingsModal"; +import { WorkspaceModal } from "./components/WorkspaceModal"; import { LandingView } from "./components/layout/LandingView"; import { TitleBar } from "./components/layout/TitleBar"; import { TrashDialog } from "./components/TrashDialog"; @@ -291,6 +292,13 @@ function normalizePathLikeList(entries) { return normalized; } +function getCleanFilenameTitle(titleOrPath) { + const raw = String(titleOrPath || "").trim(); + if (!raw) return "Untitled"; + const baseName = raw.split(/[\\/]/).pop() || raw; + return baseName.replace(/\.(md|markdown)$/i, ""); +} + function getFavoriteDashboardNotes(favorites, recentNotes, continueNotes) { const favoriteSet = new Set(Array.isArray(favorites) ? favorites : []); const metadataMap = new Map( @@ -303,9 +311,10 @@ function getFavoriteDashboardNotes(favorites, recentNotes, continueNotes) { .map((filePath) => { const key = String(filePath || "").toLowerCase(); const item = metadataMap.get(key) || { filePath, title: filePath, entryType: "file" }; + const rawTitle = item.title || filePath; return { ...item, - displayName: item.title || filePath, + displayName: getCleanFilenameTitle(rawTitle), }; }) .filter((item) => item?.filePath) @@ -403,6 +412,30 @@ export default function App() { const [updateStatus, setUpdateStatus] = useState(null); const [updateDetails, setUpdateDetails] = useState(null); const [showUpdateModal, setShowUpdateModal] = useState(false); + + const [workspaceModalOpen, setWorkspaceModalOpen] = useState(false); + const [workspaceModalMode, setWorkspaceModalMode] = useState("create"); + const [workspaceInfoState, setWorkspaceInfoState] = useState({}); + + const handleRequireWorkspaceInitialization = useCallback((folderPath) => { + const normalized = String(folderPath || "").replace(/\\/g, "/").replace(/\/$/, ""); + const lastSlash = normalized.lastIndexOf("/"); + const name = lastSlash !== -1 ? normalized.slice(lastSlash + 1) : normalized || "Workspace"; + const parentLocation = lastSlash !== -1 ? normalized.slice(0, lastSlash) : normalized; + + setWorkspaceInfoState({ + name, + parentLocation, + description: "", + domainTags: [], + projectType: "General", + icon: "πŸ“", + color: "#3b82f6" + }); + setWorkspaceModalMode("create"); + setWorkspaceModalOpen(true); + }, []); + const landingLayoutRef = useRef(null); const { @@ -450,6 +483,8 @@ export default function App() { handleCreateFolder, handleOpenWorkspacePicker, handleOpenRecentWorkspace, + handleSaveNotesFolder, + handleRestartApp, handleGoHome, handleOpenCurrentInEditor, handleOpenWebsiteFromLanding, @@ -475,7 +510,7 @@ export default function App() { setSelectedParentFolder, initialLine, setInitialLine, - } = useDocumentManager({ notify }); + } = useDocumentManager({ notify, onRequireWorkspaceInitialization: handleRequireWorkspaceInitialization }); const handleCopyLinkPath = useCallback((target) => { const filePath = typeof target === "object" ? target?.filePath : target; @@ -605,13 +640,112 @@ export default function App() { normalize: normalizeDensityMode, fallbackKey: "notes:density-mode", }); - const [favoriteNotes, setFavoriteNotes] = useWorkspaceScopedStorage({ - workspaceScope: workspaceStorageScope, - key: "notes:favorites", - defaultValue: EMPTY_ARRAY, - normalize: normalizeFavoriteNotes, - fallbackKey: "notes:favorites", - }); + const [favoriteNotes, setFavoriteNotesState] = useState(EMPTY_ARRAY); + + const loadWorkspaceFavorites = useCallback(async () => { + try { + const api = window.electronAPI || window.notesApi; + if (api?.getFavorites) { + const favs = await api.getFavorites(); + setFavoriteNotesState(normalizeFavoriteNotes(favs)); + return; + } + } catch { + // Fallback + } + }, []); + + const setFavoriteNotes = useCallback((nextValueOrFn) => { + setFavoriteNotesState((current) => { + const next = typeof nextValueOrFn === "function" ? nextValueOrFn(current) : nextValueOrFn; + const normalized = normalizeFavoriteNotes(next); + const api = window.electronAPI || window.notesApi; + if (api?.setFavorites) { + api.setFavorites(normalized).catch(() => {}); + } + return normalized; + }); + }, []); + + useEffect(() => { + void loadWorkspaceFavorites(); + }, [workspaceStorageScope, notesFolderPath, loadWorkspaceFavorites]); + + useEffect(() => { + const api = window.electronAPI || window.notesApi; + if (api?.onFavoritesChanged) { + const unsub = api.onFavoritesChanged((favs) => { + setFavoriteNotesState(normalizeFavoriteNotes(favs)); + }); + return () => unsub(); + } + }, []); + + useEffect(() => { + if (window.notesApi?.getWorkspaceInfo) { + window.notesApi.getWorkspaceInfo().then((info) => { + if (info) setWorkspaceInfoState(info); + }).catch(() => {}); + } + if (window.notesApi?.onWorkspaceInfoChanged) { + const unsub = window.notesApi.onWorkspaceInfoChanged((updated) => { + if (updated) setWorkspaceInfoState(updated); + }); + return () => unsub(); + } + }, [notesFolderPath]); + + const handleOpenNewWorkspaceModal = () => { + setWorkspaceInfoState({}); + setWorkspaceModalMode("create"); + setWorkspaceModalOpen(true); + }; + + const handleOpenWorkspaceInfoModal = async () => { + if (window.notesApi?.getWorkspaceInfo) { + try { + const info = await window.notesApi.getWorkspaceInfo(); + if (info) setWorkspaceInfoState(info); + } catch { + /* ignore */ + } + } + setWorkspaceModalMode("info"); + setWorkspaceModalOpen(true); + }; + + const handleWorkspaceModalSubmit = async (payload) => { + if (workspaceModalMode === "create") { + if (!window.notesApi?.createNewWorkspace) { + throw new Error("Workspace creation is not supported."); + } + const res = await window.notesApi.createNewWorkspace(payload); + if (res?.success && res?.workspacePath) { + notify(`Workspace "${res.info?.name || payload.name}" created successfully.`, "success"); + if (res.isNestedWorkspace) { + notify(`Note: Created inside an existing parent workspace folder. Nested workspaces operate independently.`, "info"); + } + await handleSaveNotesFolder(res.workspacePath); + } + } else { + if (!window.notesApi?.updateWorkspaceInfo) { + throw new Error("Workspace info update is not supported."); + } + const res = await window.notesApi.updateWorkspaceInfo(payload); + if (res?.success) { + notify("Workspace information updated.", "success"); + setWorkspaceInfoState(res.info); + } + } + }; + + const handlePickWorkspaceParentLocation = async () => { + if (window.notesApi?.pickFolder) { + return await window.notesApi.pickFolder(); + } + return ""; + }; + const [showTerminal, setShowTerminal] = useWorkspaceScopedStorage({ workspaceScope: workspaceStorageScope, key: "notes:terminal-open", @@ -1011,6 +1145,26 @@ export default function App() { void refreshGitWorkspaceMeta(); }, [notesFolderPath, currentFilePath, dirty, refreshGitWorkspaceMeta]); + useEffect(() => { + const api = window.electronAPI || window.notesApi; + const subscribe = api?.onMenuAction || api?.onAppMenuAction; + if (!subscribe) return undefined; + return subscribe((action) => { + if (action === "restart-app") { + void handleRestartApp(); + } + }); + }, [handleRestartApp]); + + useEffect(() => { + const api = window.electronAPI || window.notesApi; + if (!api?.onWorkspaceChanged) return undefined; + return api.onWorkspaceChanged(() => { + void refreshGitWorkspaceMeta(); + void loadDocumentsData(); + }); + }, [refreshGitWorkspaceMeta, loadDocumentsData]); + useEffect(() => { function onGlobalKeyDown(event) { const isCmdK = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k"; @@ -1332,6 +1486,16 @@ export default function App() { return; } + if (action === "new-workspace") { + handleOpenNewWorkspaceModal(); + return; + } + + if (action === "open-workspace-info") { + void handleOpenWorkspaceInfoModal(); + return; + } + if (action === "open-workspace") { void handleOpenWorkspacePicker(); return; @@ -1353,6 +1517,16 @@ export default function App() { return; } + if (action === "open-assets") { + setLandingAssetsOpen(true); + return; + } + + if (action === "open-trash") { + setTrashDialogOpen(true); + return; + } + if (action.startsWith("open-recent-workspace:")) { const encodedPath = String(action).slice("open-recent-workspace:".length); const workspacePath = decodeURIComponent(encodedPath || ""); @@ -1982,6 +2156,7 @@ export default function App() { } const paletteCommandsBase = [ + { id: "restart-app", label: "Restart Notely", group: "App", aliases: "restart relaunch reboot app application" }, { id: "new-note", label: "Create New Note", group: "Notes", shortcut: "Ctrl/Cmd+N", aliases: "add note new document write jot capture" }, { id: "open-ai-palette", label: "Open AI Palette", group: "AI", shortcut: "Ctrl/Cmd+Shift+I", aliases: "assistant ask ai prompt summarize rewrite" }, { id: "open-help-center", label: "Open Help Center", group: "Help", shortcut: "F1", aliases: "help docs guide manual about" }, @@ -2334,6 +2509,21 @@ export default function App() { return; } + if (resolvedCommandId === "new-workspace") { + handleOpenNewWorkspaceModal(); + return; + } + + if (resolvedCommandId === "open-workspace-info") { + await handleOpenWorkspaceInfoModal(); + return; + } + + if (resolvedCommandId === "restart-app") { + await handleRestartApp(); + return; + } + if (resolvedCommandId === "open-workspace") { await handleOpenWorkspacePicker(); return; @@ -2532,6 +2722,11 @@ export default function App() { return; } + if (action === "reload-workspace" || action === "refresh") { + void handleReloadWorkspace(); + return; + } + if (action === "search") { setGlobalSearchQuery(""); setGlobalSearchOpen(true); @@ -3199,6 +3394,18 @@ export default function App() { ) : null} + {workspaceModalOpen ? ( + setWorkspaceModalOpen(false)} + onSubmit={handleWorkspaceModalSubmit} + onPickParentLocation={handlePickWorkspaceParentLocation} + /> + ) : null} + {settingsOpen ? (
- + {/back/i.test(confirmState.cancelLabel) ? : } {confirmState.cancelLabel} onAction("ai")} data-tooltip="AI Assistant" aria-label="AI Assistant"> - -
@@ -178,7 +173,7 @@ export function DashboardPanels({ documents, taskDocuments = documents, loading, data-tooltip={`Last edited: ${formatDate(note.updatedAt)}`} > - {note.displayName} + {getCleanFilename(note.displayName || note.title || note.filePath)} @@ -323,7 +318,7 @@ export function DashboardPanels({ documents, taskDocuments = documents, loading, {visibleFavorites.map((note) => (
  • diff --git a/src/components/GitCommitDialog.jsx b/src/components/GitCommitDialog.jsx index 0b84be5..05034fc 100644 --- a/src/components/GitCommitDialog.jsx +++ b/src/components/GitCommitDialog.jsx @@ -23,7 +23,6 @@ export function GitCommitDialog({ onClose, onCommit, stagedFiles = [], - _workspacePath, currentFilePath = null, }) { const [message, setMessage] = useState(""); diff --git a/src/components/GitVersionControlPage.jsx b/src/components/GitVersionControlPage.jsx index 78e45df..42afa13 100644 --- a/src/components/GitVersionControlPage.jsx +++ b/src/components/GitVersionControlPage.jsx @@ -67,7 +67,7 @@ const TABS = [ // ── Empty states ────────────────────────────────────────────────────────────── -function NoGitState({ _onInstallLink }) { +function NoGitState() { return (