Skip to content

Commit 800f746

Browse files
feat: add spaces, space-aware sync, and Math Notebook migration (#692)
1 parent 2f9719c commit 800f746

24 files changed

Lines changed: 548 additions & 78 deletions

AGENTS.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,41 @@ Composables get a `use` prefix. The file name matches the exported function name
7878
### D. System & IPC
7979

8080
- **File System/System Ops:** Use `ipc.invoke('channel:action', data)`.
81-
- **Channels:** `fs:*`, `system:*`, `db:*`, `main-menu:*`, `prettier:*`.
81+
- **Channels:** `fs:*`, `system:*`, `db:*`, `main-menu:*`, `prettier:*`, `spaces:*`.
8282
- **Renderer:** Access Electron only via `src/renderer/electron.ts`.
8383

84-
### E. Localization
84+
### E. Spaces Architecture
85+
86+
massCode uses a **Spaces** system to organize different functional areas:
87+
88+
| Space | ID | Description |
89+
|-------|----|-------------|
90+
| Code | `code` | Main snippet management (folders, snippets, tags) |
91+
| Tools | `tools` | Developer utilities (converters, generators) |
92+
| Math | `math` | Math Notebook with calculation sheets |
93+
94+
**Space Definitions:** `src/renderer/spaceDefinitions.ts``SpaceId`, `getSpaceDefinitions()`, `getActiveSpaceId()`.
95+
96+
**Space State Persistence (Markdown Engine):**
97+
- Each space can store its state in `__spaces__/{spaceId}/.state.yaml` inside the vault.
98+
- Runtime utilities: `src/main/storage/providers/markdown/runtime/spaces.ts``ensureSpaceDirectory()`, `getSpaceStatePath()`.
99+
- Generic YAML read/write: `src/main/storage/providers/markdown/runtime/spaceState.ts``readSpaceState<T>()`, `writeSpaceState()`.
100+
- Space state writes use the same debounce/flush infrastructure as `state.json` (`pendingStateWriteByPath` in `constants.ts`), so they flush automatically on app exit.
101+
- `__spaces__/` directory exists **only in markdown engine**. When engine is `sqlite`, spaces fall back to `electron-store`.
102+
103+
**Space IPC Channels:**
104+
- `spaces:math:read` — read Math Notebook state (auto-migrates from electron-store on first read in markdown mode).
105+
- `spaces:math:write` — persist Math Notebook state.
106+
- Handlers: `src/main/ipc/handlers/spaces.ts`.
107+
108+
**Space-Aware Sync:**
109+
- `system:storage-synced` event dispatches refresh based on `getActiveSpaceId()`:
110+
- `code` / `null` → refresh folders + snippets
111+
- `math``reloadFromDisk()` via `useMathNotebook()`
112+
- `tools` → no-op (no vault data)
113+
- Mutable operations must call `markPersistedStorageMutation()` to prevent sync loops.
114+
115+
### F. Localization
85116

86117
- **Primary Language:** English (EN) is the base language. All new keys **MUST** be added to `src/main/i18n/locales/en_US/` first.
87118
- **Strictly No Hardcoding:** Never use hardcoded strings in templates or logic. Always use the localization system.

src/main/ipc/handlers/spaces.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { MathNotebookStore } from '../../store/types'
2+
import { ipcMain } from 'electron'
3+
import {
4+
ensureSpaceDirectory,
5+
getSpaceStatePath,
6+
} from '../../storage/providers/markdown/runtime/spaces'
7+
import {
8+
readSpaceState,
9+
writeSpaceState,
10+
} from '../../storage/providers/markdown/runtime/spaceState'
11+
import { store } from '../../store'
12+
13+
function getVaultPath(): string | null {
14+
return store.preferences.get('storage.vaultPath') as string | null
15+
}
16+
17+
function isMarkdownEngine(): boolean {
18+
return store.preferences.get('storage.engine') === 'markdown'
19+
}
20+
21+
export function registerSpacesHandlers() {
22+
ipcMain.handle('spaces:math:read', () => {
23+
if (!isMarkdownEngine()) {
24+
return {
25+
sheets: store.mathNotebook.get('sheets') ?? [],
26+
activeSheetId: store.mathNotebook.get('activeSheetId') ?? null,
27+
} satisfies MathNotebookStore
28+
}
29+
30+
const vaultPath = getVaultPath()
31+
if (!vaultPath) {
32+
return {
33+
sheets: [],
34+
activeSheetId: null,
35+
} satisfies MathNotebookStore
36+
}
37+
38+
ensureSpaceDirectory(vaultPath, 'math')
39+
const statePath = getSpaceStatePath(vaultPath, 'math')
40+
const state = readSpaceState<MathNotebookStore>(statePath)
41+
42+
if (state) {
43+
return {
44+
sheets: Array.isArray(state.sheets) ? state.sheets : [],
45+
activeSheetId: state.activeSheetId ?? null,
46+
} satisfies MathNotebookStore
47+
}
48+
49+
// Migration: read from electron-store, write to vault
50+
const legacy: MathNotebookStore = {
51+
sheets: store.mathNotebook.get('sheets') ?? [],
52+
activeSheetId: store.mathNotebook.get('activeSheetId') ?? null,
53+
}
54+
55+
if (legacy.sheets.length > 0) {
56+
writeSpaceState(statePath, legacy)
57+
}
58+
59+
return legacy
60+
})
61+
62+
ipcMain.handle('spaces:math:write', (_, data: MathNotebookStore) => {
63+
if (!isMarkdownEngine()) {
64+
store.mathNotebook.set('sheets', data.sheets)
65+
store.mathNotebook.set('activeSheetId', data.activeSheetId)
66+
return
67+
}
68+
69+
const vaultPath = getVaultPath()
70+
if (!vaultPath) {
71+
return
72+
}
73+
74+
ensureSpaceDirectory(vaultPath, 'math')
75+
const statePath = getSpaceStatePath(vaultPath, 'math')
76+
writeSpaceState(statePath, data)
77+
})
78+
}

src/main/ipc/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { registerDBHandlers } from './handlers/db'
44
import { registerDialogHandlers } from './handlers/dialog'
55
import { registerFsHandlers } from './handlers/fs'
66
import { registerPrettierHandlers } from './handlers/prettier'
7+
import { registerSpacesHandlers } from './handlers/spaces'
78
import { registerSystemHandlers } from './handlers/system'
89
import { registerThemeHandlers } from './handlers/theme'
910

@@ -18,4 +19,5 @@ export function registerIPC() {
1819
registerPrettierHandlers()
1920
registerFsHandlers()
2021
registerThemeHandlers()
22+
registerSpacesHandlers()
2123
}

src/main/storage/providers/markdown/runtime/constants.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ export const META_DIR_NAME = '.masscode'
44
export const STATE_FILE_NAME = 'state.json'
55
export const INBOX_DIR_NAME = 'inbox'
66
export const TRASH_DIR_NAME = 'trash'
7-
export const FOLDER_META_FILE_NAME = '.masscode-folder.yml'
7+
export const SPACES_DIR_NAME = '__spaces__'
8+
export const META_FILE_NAME = '.meta.yaml'
9+
export const SPACE_STATE_FILE_NAME = '.state.yaml'
10+
export const LEGACY_FOLDER_META_FILE_NAME = '.masscode-folder.yml'
811

912
export const INBOX_RELATIVE_PATH = `${META_DIR_NAME}/${INBOX_DIR_NAME}`
1013
export const TRASH_RELATIVE_PATH = `${META_DIR_NAME}/${TRASH_DIR_NAME}`
1114
export const LEGACY_INBOX_RELATIVE_PATH = INBOX_DIR_NAME
1215
export const LEGACY_TRASH_RELATIVE_PATH = TRASH_DIR_NAME
1316

14-
export const RESERVED_ROOT_NAMES = new Set([INBOX_DIR_NAME, TRASH_DIR_NAME])
17+
export const RESERVED_ROOT_NAMES = new Set([
18+
INBOX_DIR_NAME,
19+
TRASH_DIR_NAME,
20+
SPACES_DIR_NAME,
21+
])
1522
export const NEW_LINE_SPLIT_RE = /\r?\n/
1623
export const SEARCH_DIACRITICS_RE = /[\u0300-\u036F]/g
1724
export const SEARCH_WORD_RE = /[\p{L}\p{N}_]+/gu

src/main/storage/providers/markdown/runtime/index.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22
export {
33
INBOX_DIR_NAME,
44
INVALID_NAME_CHARS_RE,
5+
LEGACY_FOLDER_META_FILE_NAME,
56
META_DIR_NAME,
7+
META_FILE_NAME,
68
peekRuntimeCache,
9+
SPACE_STATE_FILE_NAME,
10+
SPACES_DIR_NAME,
711
TRASH_DIR_NAME,
812
WINDOWS_RESERVED_NAME_RE,
913
} from './constants'
1014

1115
// Normalizers
12-
export { normalizeFlag } from './normalizers'
16+
export { normalizeFlag, normalizeFolderOrderIndices } from './normalizers'
1317

1418
// Parser
15-
export { writeFolderMetadataFile } from './parser'
19+
export {
20+
readYamlObjectFile,
21+
writeFolderMetadataFile,
22+
writeYamlObjectFile,
23+
} from './parser'
1624

1725
// Paths
1826
export {
@@ -42,6 +50,20 @@ export {
4250
writeSnippetToFile,
4351
} from './snippets'
4452

53+
// Spaces
54+
export {
55+
ensureSpaceDirectory,
56+
getSpaceDirPath,
57+
getSpaceStatePath,
58+
} from './spaces'
59+
60+
// Space State
61+
export {
62+
readSpaceState,
63+
writeSpaceState,
64+
writeSpaceStateImmediate,
65+
} from './spaceState'
66+
4567
// State
4668
export {
4769
createDefaultState,

src/main/storage/providers/markdown/runtime/normalizers.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { FolderRecord } from '../../../contracts'
12
import type { MarkdownFolderUIState } from './types'
23

34
export function normalizeNumber(value: unknown, fallback = 0): number {
@@ -60,3 +61,30 @@ export function normalizeFolderUiState(
6061

6162
return normalized
6263
}
64+
65+
export function normalizeFolderOrderIndices(folders: FolderRecord[]): void {
66+
const childrenByParent = new Map<number | null, FolderRecord[]>()
67+
68+
for (const folder of folders) {
69+
const siblings = childrenByParent.get(folder.parentId)
70+
if (siblings) {
71+
siblings.push(folder)
72+
}
73+
else {
74+
childrenByParent.set(folder.parentId, [folder])
75+
}
76+
}
77+
78+
for (const siblings of childrenByParent.values()) {
79+
siblings.sort((a, b) => {
80+
if (a.orderIndex !== b.orderIndex) {
81+
return a.orderIndex - b.orderIndex
82+
}
83+
return a.id - b.id
84+
})
85+
86+
for (let i = 0; i < siblings.length; i++) {
87+
siblings[i].orderIndex = i
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)