Fable is a creative-writing desktop app: Electron + React 19 + Vite + TanStack Query + Plate.js frontend, Node.js + SQLite + custom indexed filesystem backend, Google ADK for AI features.
Every file can have children, not just folders. FileNode.children exists on ANY node regardless of category. A text document can contain child documents (chapters containing scenes). Never assume only folders have children.
parentId and siblingIndex are computed by buildFileTree() at runtime — never persist them.
dot
digraph code_placement {
rankdir=TB;
node [shape=diamond];
"Used by both\nfrontend & backend?" -> "src/shared/" [label="yes"];
"Used by both\nfrontend & backend?" -> "Runs in\nNode.js only?" [label="no"];
"Runs in\nNode.js only?" -> "IPC handler?" [label="yes"];
"Runs in\nNode.js only?" -> "React component\nor hook?" [label="no"];
"IPC handler?" -> "src/backend/handlers/" [label="yes"];
"IPC handler?" -> "src/backend/services/" [label="no"];
"React component\nor hook?" -> "src/frontend/components/" [label="component"];
"React component\nor hook?" -> "src/frontend/hooks/" [label="hook"];
"React component\nor hook?" -> "src/frontend/services/" [label="service/singleton"];
node [shape=box];
"src/shared/" [style=filled, fillcolor="#e8f5e9"];
"src/backend/handlers/" [style=filled, fillcolor="#e3f2fd"];
"src/backend/services/" [style=filled, fillcolor="#e3f2fd"];
"src/frontend/components/" [style=filled, fillcolor="#fff3e0"];
"src/frontend/hooks/" [style=filled, fillcolor="#fff3e0"];
"src/frontend/services/" [style=filled, fillcolor="#fff3e0"];
}
Shared code constraint: src/shared/ must NOT import browser APIs, Node.js APIs, or React.
dot
digraph data_access {
rankdir=TB;
node [shape=diamond];
"Inside React\ncomponent?" -> "Use query hooks" [label="yes"];
"Inside React\ncomponent?" -> "Use queryService\nsingleton" [label="no\n(command, service)"];
"Need to mutate\nfile tree?" -> "queryService.mutations.*\n(optimistic updates)" [label="always"];
"Need to read\nfile content?" -> "readFileContent()\nhelper (cache-first)" [label="preferred"];
node [shape=box];
"Use query hooks" [style=filled, fillcolor="#e8f5e9"];
"Use queryService\nsingleton" [style=filled, fillcolor="#e8f5e9"];
"queryService.mutations.*\n(optimistic updates)" [style=filled, fillcolor="#fff3e0"];
"readFileContent()\nhelper (cache-first)" [style=filled, fillcolor="#fff3e0"];
}
Never call window.api.* directly for tree mutations — always go through queryService.mutations.* which handles optimistic updates and rollback.
DirectoryWhat to findsrc/types.tsAll shared types (FileNode, FileMetadata, ChatSession, Agent, etc.)src/shared/Code shared by both processes (graph, agentFlow, toolDescriptors, search)src/backend/handlers/IPC handlers: file, project, agent, agentFlowsrc/backend/services/IndexedFsService, UserDataService, ReferenceIndexService, agent/src/backend/services/agent/AI system: AdkRunner, ToolFactory, providers, ChatDatabaseServicesrc/frontend/components/All React components (chat/, graph/, fileViewer/, collectionViewer/)src/frontend/services/commandInfra/VS Code-style commands, keybindings, context keyssrc/frontend/services/query/TanStack Query: queryService, mutations, treeTransformationssrc/frontend/hooks/Custom hooks: useFileTree, useChat, useAgentFlowRun, etc.src/frontend/styles/plate/Plate.js editor plugins and kitssrc/locales/{en,zh-CN}/i18n translations (8 namespaces)e2e/Playwright E2E testselectron/preload.tsIPC bridge — window.api.* definitionssrc/global.d.tsTypeScript declarations for Window.api
Read references/recipes.md for detailed step-by-step procedures including:
Read references/architecture.md for the full breakdown. Key mental model:
text
Frontend (React) --window.api.*--> Preload --IPC--> Backend handlers --> Services --> Disk
<--transport push events-- <--IPC-- ProjectManager <-- IndexedFsService events
staleTime: Infinity everywhere.useFileTree sets up listener.createTreeMutation factory: optimistic update -> IPC call -> rollback on error.CommandService + ContextService + KeybindingService.ContentViewer dispatches by subType first, then category.references/agent-system.md.common, fileExplorer, chat, project, editor, agentBuilder, graph, agentDefinitionconst { t } = useTranslation("namespace")import i18n from "@/src/i18n/i18n"; i18n.t("ns:key")en and zh-CNbash
npm run dev:electron # Dev mode (Vite + Electron)
npm run build # Production build
npm run check # Lint + format + TypeScript check
npm run test:e2e # Playwright E2E tests (headless)
npm run test:e2e:headed # E2E with visible browser
@/* maps to repo roottreeTransformations.ts returns new arrays, never mutateen and zh-CN#file-toolbar-portal in TopBar, not inside editor DOMuseKeybindings checks event.target tagsrc/global.d.ts when adding IPC channelssrc/shared/ cannot import browser or Node APIstoolDescriptors.ts serves both UI commands and LLM agentsReferenceUse whenreferences/architecture.mdUnderstanding layers, IPC channels, backend services, frontend providersreferences/command-system.mdWorking with commands, keybindings, context keys, when clausesreferences/workspace-structure.mdOn-disk file layout, metadata schema, storage guidelinesreferences/agent-system.mdAI features: providers, streaming, tools, planning mode, chat persistencereferences/recipes.mdStep-by-step procedures for common development tasks