diff --git a/docs/superpowers/specs/2026-06-08-notes-panel-design.md b/docs/superpowers/specs/2026-06-08-notes-panel-design.md new file mode 100644 index 00000000..c919a398 --- /dev/null +++ b/docs/superpowers/specs/2026-06-08-notes-panel-design.md @@ -0,0 +1,115 @@ +# Notes & To-dos Panel — Design + +**Date:** 2026-06-08 +**Goal:** Add a per-project Notes/To-dos panel to the right panel (iCloud Notes–style), +reusing a popular editor library, with the ability to spin up a new thread from a to-do +item or from selected note text. + +## Decisions (locked with user) + +- **Editor library:** TipTap v3 (headless ProseMirror; themes cleanly with Tailwind/HeroUI). +- **Structure:** Separate surfaces — a free-form rich-text **Notes** editor on top, a + structured **To-dos** list (add / check / edit / reorder / delete) below. +- **New thread:** Pre-fill a draft composer with the to-do/selected text so the user can + review/edit and pick model/mode before sending (does not auto-launch). +- **Storage:** App SQLite DB, scoped per project (private to the app, never touches the repo). + +## Dependencies + +- `@tiptap/react`, `@tiptap/starter-kit`, `@tiptap/pm` (ProseMirror peer), `@tiptap/extensions` + (Placeholder). BubbleMenu imported from `@tiptap/react/menus`. +- To-dos are a custom structured list (NOT TipTap TaskList) per the "separate" decision. + +## Data model + +`src/shared/contracts/notes.ts` + +```ts +NotesTodoItem = { id: string; text: string; done: boolean; createdAt: string } +ProjectNotes = { projectId: string; doc: unknown | null; todos: NotesTodoItem[]; updatedAt: string } +``` + +`doc` is the TipTap ProseMirror JSON document (opaque; stored as JSON), or `null` when empty. + +## Persistence (mirrors `thread_runtime_items` pattern) + +New SQLite table `project_notes` (one row per project), with its own targeted read/write +IPC — deliberately NOT folded into the app-store `dbSyncAll` snapshot, so large/frequently +edited note docs don't rewrite the entire projects+threads payload on each keystroke. + +- **Schema:** `project_notes(project_id TEXT PRIMARY KEY, doc TEXT, todos TEXT NOT NULL DEFAULT '[]', updated_at TEXT NOT NULL)`. + No FK; orphan cleanup is explicit (see below) to avoid an insert/sync FK race when a + brand-new project's notes are written before the project row syncs. +- **Migration:** `db.ts` adds `CREATE TABLE IF NOT EXISTS project_notes ...` to the init block + and a `storedVersion < 16` step; `SCHEMA_VERSION` bumped 15 → 16. +- **Functions:** `dbGetProjectNotes(projectId): ProjectNotes | null`, + `dbSetProjectNotes(notes: ProjectNotes): void` (upsert). +- **Cleanup:** `dbDeleteProject` and the project-deletion loop in `dbSyncAll` also + `DELETE FROM project_notes WHERE project_id = ?`. +- **IPC:** `dbGetProjectNotes` / `dbSetProjectNotes` added to `procedures/db.ts`, + `schemas.ts` (payloads), `procedureMap.ts` (`MAIN_LOCAL_PROCEDURE_NAMES`), and + `localHandlers.ts`. Preload bridge auto-wires via `createInvokeBridge`. + +## Renderer state — `src/renderer/state/notesStore.ts` + +Standalone Zustand store (NOT persisted through the app-store middleware). Per-project +cache `{ status, doc, todos }`. Actions: `ensureLoaded`, `setDoc`, `addTodo`, `toggleTodo`, +`updateTodoText`, `removeTodo`, `reorderTodos`, `flush`, `flushAll`. Mutations schedule a +~600ms debounced `dbSetProjectNotes`; `flush`/`flushAll` (panel close, beforeunload) write +immediately. Missing-bridge (tests) is handled gracefully — in-memory only. + +## New-thread seeding — `src/renderer/actions/notesActions.ts` + +`newThreadFromText(projectId, text)`: + +1. Merge `text` into the project's draft text (append to any in-progress prompt; preserve + file/attachment segments). +2. Push a one-shot **composer seed** (`draftSlice.setComposerSeed`) so the draft composer + applies it whether it mounts fresh or is already open. +3. `openNewThread(projectId)` (respects side-by-side `newThreadMode`). + +`ThreadDraftComposerArea` consumes a pending seed on mount and on nonce change via the +existing `MentionInput` ref (`restoreFromSegments`), then clears it. + +## UI + +`src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/` + +- `NotesPanel.tsx` — container; `ensureLoaded(projectId)`; stacked layout: Notes editor + (top, scroll) over To-dos list (bottom, scroll), split by a thin divider; flushes on unmount. +- `NotesEditor.tsx` — TipTap `useEditor` (StarterKit + Placeholder); `onUpdate` → + `setDoc`; **BubbleMenu** on selection with "✦ New thread from selection" (+ bold/italic). +- `TodoList.tsx` / `TodoRow.tsx` — add input; each row: checkbox, inline-editable text, + hover "New thread" action, and a right-click context menu ("New thread from this to-do", + "Delete"); drag-to-reorder reusing the repo's reorder helper. + +## Right-panel integration + +- `panelStore.ts`: `RightPanelTab` gains `"notes"`; add `notesPanelOpen` + setter + + `openNotesPanel()`; include in `closeAllPanels`. +- `panelActions.ts`: `openNotesPanel()` (toggle-closes if already active, like usage). +- `UnifiedRightPanel.tsx`: `notesContent` prop, `showNotesTab`, a `NotebookPen` tab button, + and a content layer. +- `ProjectAuxiliaryPanel.tsx`: resolve active project scope → render ``, + wire `onOpenNotes`. Notes tab shown for all scopes (including Home — it's a scratchpad). + +## Styling + +ProseMirror/editor styles (placeholder, list spacing, focus) added to +`src/renderer/styles.css`, scoped to a `.lc-notes-editor` wrapper using existing design +tokens (`--foreground`, `--muted`, `--border`, `--accent`). + +## Testing + +- `vitest` unit tests: notesStore (todo CRUD/reorder, debounced-persist scheduling, flush, + lazy load), `newThreadFromText` (seed + nonce + openNewThread), contract schema parse. +- No node-vitest test for the `db.ts` round-trip: `better-sqlite3` is rebuilt against + Electron's ABI and cannot load under plain-node vitest. The DB path is exercised by the + interactive smoke test instead. +- `pnpm typecheck`, `pnpm lint`, `pnpm fmt:check`, `pnpm test`, `pnpm build`. +- Interactive smoke test of the panel via the interactive-testing skill. + +## Out of scope (YAGNI) + +Markdown import/export, cross-project notes, collaboration, attachments in notes, +slash-command blocks. To-dos use TipTap-free custom rows; no TaskList extension. diff --git a/package.json b/package.json index 9152485f..5f802669 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,10 @@ "@sentry/electron": "^7.13.0", "@sentry/node": "10.50.0", "@tanstack/react-virtual": "^3.13.24", + "@tiptap/extensions": "^3.24.0", + "@tiptap/pm": "^3.24.0", + "@tiptap/react": "^3.24.0", + "@tiptap/starter-kit": "^3.24.0", "@xterm/addon-clipboard": "0.3.0-beta.219", "@xterm/addon-fit": "0.12.0-beta.219", "@xterm/addon-image": "0.10.0-beta.219", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16ed95ce..2cb9af4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,18 @@ importers: '@tanstack/react-virtual': specifier: ^3.13.24 version: 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tiptap/extensions': + specifier: ^3.24.0 + version: 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + '@tiptap/pm': + specifier: ^3.24.0 + version: 3.24.0 + '@tiptap/react': + specifier: ^3.24.0 + version: 3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tiptap/starter-kit': + specifier: ^3.24.0 + version: 3.24.0 '@xterm/addon-clipboard': specifier: 0.3.0-beta.219 version: 0.3.0-beta.219(@xterm/xterm@6.1.0-beta.219) @@ -850,6 +862,15 @@ packages: peerDependencies: '@opentelemetry/api': ^1.9.0 + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@formatjs/ecma402-abstract@2.3.6': resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} @@ -2499,6 +2520,155 @@ packages: '@types/react-dom': optional: true + '@tiptap/core@3.24.0': + resolution: {integrity: sha512-GTAsXAI32p4hEZgPzvUv2RPrObxamy9AFhmhG10fXSvN/cDUs8naEYVIqDV3Sh99jMwQEbTFKW1E1mcspsY6ow==} + peerDependencies: + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-blockquote@3.24.0': + resolution: {integrity: sha512-DgwEEJ1GbDQcT054ynxoaZGmB9apGeUklPrinq9o6xdLHpdg+bO9HCQzggdB8n21VLLglb8jfAEWsVNwh3eASQ==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-bold@3.24.0': + resolution: {integrity: sha512-CujogYaynasklFKHADUseuvj8X2FnWktTCCo3Hl+nlyRvBTmm5TK2aqiamg3v2P4dBh3O6a70mo8BfRJPuiR1g==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-bubble-menu@3.24.0': + resolution: {integrity: sha512-jRXD+JPu9ayvq78g8hsCxx4q/qUFtrdfIYirRSf5YUseuuUbtfrq83AsGabcygpUTefjJkMQoXNITkh6294Ggw==} + peerDependencies: + '@tiptap/core': 3.24.0 + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-bullet-list@3.24.0': + resolution: {integrity: sha512-IOpAm5c4XVVVvkOef+V9XYMVpea+3MgBpCQgn83UQRlwO9eIMwmcyxOznu7gQPQVShTEpkt4T6uK+ZN9o8meIA==} + peerDependencies: + '@tiptap/extension-list': 3.24.0 + + '@tiptap/extension-code-block@3.24.0': + resolution: {integrity: sha512-NZglw4oHoH6oJ5+HvxxQCYk+wODJmsxzUpRQdsOmje08sekQH+Zt9i4UKimBhg4urpd5r+dKXTslab9a5eQ86w==} + peerDependencies: + '@tiptap/core': 3.24.0 + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-code@3.24.0': + resolution: {integrity: sha512-MAQtrPRQ+HRmcGotWbksdIGeH1gqayFAdvi4lNGeFT7taHXP1o1XD7CQp7iYIKmg8IU4/MQ+RdetSfuC1A9edQ==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-document@3.24.0': + resolution: {integrity: sha512-yxgM3+yXy2XZzEwH43y2Kp8D1BkblxEWLXqo0YCoAKtxyKCcEaT8kdlf70kS7D0+VSzYU4D0iN7VdQIYHcL2mA==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-dropcursor@3.24.0': + resolution: {integrity: sha512-Dbv1c5LnvG3PT+yEbCNroyOeeUkHq9wcir2pbC7wri7g7d2sCi0+HvKH0MAxLwY3j5NJJSiSyG2ypMaXOAs4sg==} + peerDependencies: + '@tiptap/extensions': 3.24.0 + + '@tiptap/extension-floating-menu@3.24.0': + resolution: {integrity: sha512-7QEbf3mUzFAkejjQGX9f0L507oMtnOBRwHt2skUTR+9yXgudsN8zaDBSSRHLeMWGk9b7L293ZMA6zCRrZaHrfA==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.24.0 + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-gapcursor@3.24.0': + resolution: {integrity: sha512-CzCP5/jni5RFwW9jCfBO6auh83GbaioMTpSk6tyR3sd+CbwlBcUdsJFGJkbaRdiSS9dgIyi+6hRbhjpYdHcp+w==} + peerDependencies: + '@tiptap/extensions': 3.24.0 + + '@tiptap/extension-hard-break@3.24.0': + resolution: {integrity: sha512-T/ZEBiHQPMyTqDvXG0tiqBToNeuSemIPmNtdoGSgBN/degVl7VJZqQIrLIvOUHfjf3QkRs7TE/mcqTJsIboO/g==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-heading@3.24.0': + resolution: {integrity: sha512-GCSgapIzQPqEGNcVGE0/Pcjg5wITMLYJlrS3GGVw7BPmECJwgexcoOsEwkxtzJnXT/HpFXbvOFW43sM0KeHSjg==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-horizontal-rule@3.24.0': + resolution: {integrity: sha512-DFzWJTrb23x+qssLLs85vEyho8ItUGp3RY9XUsVTIAGZn5IsoUw8wMsvIBlH1ux4Ch7gLchtcD6kpTdMdrL9kw==} + peerDependencies: + '@tiptap/core': 3.24.0 + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-italic@3.24.0': + resolution: {integrity: sha512-mf3cbNlbMPUNj3IyUkIke+o3ZpOUrtVeY5Yqs5IM/VhkUUh/PdIzqw74VuqEAJ0Z4oZ6nNDHeYLrl3Be1j99lQ==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-link@3.24.0': + resolution: {integrity: sha512-MwMoNGG2mL5XGFV1tEGunBRglwsIbW+ZOB2QnKiv+Mcbi2JCWMrorndJZBqpVPR5nM+Bef2KnpchEJmYlQLvKQ==} + peerDependencies: + '@tiptap/core': 3.24.0 + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-list-item@3.24.0': + resolution: {integrity: sha512-zl/U3viJiV9OzkKM37AHIUN1af1TSLrcbHUUoNLkfJ33Nq+NlpaXpCVK0rKRqiLFJf7zk/a5KWG5CrOy9TxjKA==} + peerDependencies: + '@tiptap/extension-list': 3.24.0 + + '@tiptap/extension-list-keymap@3.24.0': + resolution: {integrity: sha512-69fKcrngYGEKWNn4R5oLwl0YuV3FY4kufEValVcjnihUmqJTE1vx+fwctYoTsOGnIuNGpUIQ7f9YDD/0w34qBw==} + peerDependencies: + '@tiptap/extension-list': 3.24.0 + + '@tiptap/extension-list@3.24.0': + resolution: {integrity: sha512-GcxDVMMmDGj7OFTBrV7JpVgr5wxlr2vmjwH7U8QxZX7OJI5vrsMYl/U6KRTvUpG8wP+Zmo5jRlLM+BbL+a/W3g==} + peerDependencies: + '@tiptap/core': 3.24.0 + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-ordered-list@3.24.0': + resolution: {integrity: sha512-buRa6bmBDw0TztH+rAcusIye14DiLDS+yGheo6GiNCTD7kKJnksXagBdxvip3jhW5sx7gyAKvoBmvGSg1BbsGA==} + peerDependencies: + '@tiptap/extension-list': 3.24.0 + + '@tiptap/extension-paragraph@3.24.0': + resolution: {integrity: sha512-wD06aB6hO7LgcrlhGiw7I64k2tus9kNoICX5R+UecBSB1DVJdzKvXoXL2kPNv4DqYvljHdkIeK/OpuOTQd6MJA==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-strike@3.24.0': + resolution: {integrity: sha512-sfN1iQs6Fdlorrfe8wipDkTPwu/Egx3s2fkY7TAWusTGFHwlovuRUGFKqCL9dI4N3u6uqUMpEuWmQNgv+aQGjQ==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-text@3.24.0': + resolution: {integrity: sha512-Im7keLPEihxm3+LyF+drYCoaOY5hlq35lvHAp/el6M8pJ/scts88HrYpdR1Yc4BtpZBIhfHSyWgPaupI4qwdeg==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extension-underline@3.24.0': + resolution: {integrity: sha512-D4W4X3UMq9dLVIOfPB9+UodQ4eAJ8yDcm8qFWAwq0a15YWH6bnwulCuIdV+U5dEG+yaRxN8haB9GrrID9jmrSA==} + peerDependencies: + '@tiptap/core': 3.24.0 + + '@tiptap/extensions@3.24.0': + resolution: {integrity: sha512-z6gRYzy2ucJp07OQ0F2W07NxyhMTxPYH1ia2eGiQkWax1i56oExpjMsDHP8THWlg8Tb7NnbfKpkfh881EsmofA==} + peerDependencies: + '@tiptap/core': 3.24.0 + '@tiptap/pm': 3.24.0 + + '@tiptap/pm@3.24.0': + resolution: {integrity: sha512-QQP/78ryOZDN99gNBV7dgh69/8AYaOYQYFklq/iR+ZRFaaL3+qqHFvPVJapGkzPdymBgNJ34xjFM8n5pJ4QmMg==} + + '@tiptap/react@3.24.0': + resolution: {integrity: sha512-KxnrlQbzOgA02EMsfuGGHtNhfkJQGqVlQttmQctI9DOl/F3gcaRqg+wNTBY1Fof8yDaZ8Z1LL1F0C05W0o3vUw==} + peerDependencies: + '@tiptap/core': 3.24.0 + '@tiptap/pm': 3.24.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.24.0': + resolution: {integrity: sha512-Ef4PCP96vcY2GonXN9J0M8iC6zvxPTmQlL/QZiCwuYqqnH/hNpYIjNSQdTndiDpxRKofa32Sr2HWktgEnL32Bg==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -2699,6 +2869,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/verror@1.10.11': resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} @@ -3867,6 +4040,10 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -4441,6 +4618,9 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + linkifyjs@4.3.3: + resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} + lint-staged@17.0.4: resolution: {integrity: sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA==} engines: {node: '>=22.22.1'} @@ -4896,6 +5076,9 @@ packages: onnxruntime-web@1.26.0-dev.20260416-b7804b056c: resolution: {integrity: sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + oxfmt@0.47.0: resolution: {integrity: sha512-OFbkbzxKCpooQEnRmpTDnuwTX8KHXzZTQ4Df/hz85fpS67Pl+lxPEFvUtin56HIIS0B1k4X8oIzTXRZPufA2CA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5066,6 +5249,45 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.7: + resolution: {integrity: sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.41.8: + resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} + protobufjs@7.6.1: resolution: {integrity: sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==} engines: {node: '>=12.0.0'} @@ -5302,6 +5524,9 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -5947,6 +6172,9 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -6669,6 +6897,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + optional: true + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + optional: true + + '@floating-ui/utils@0.2.11': + optional: true + '@formatjs/ecma402-abstract@2.3.6': dependencies: '@formatjs/fast-memoize': 2.2.7 @@ -8249,6 +8491,178 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@tiptap/core@3.24.0(@tiptap/pm@3.24.0)': + dependencies: + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-blockquote@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-bold@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-bubble-menu@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + '@tiptap/pm': 3.24.0 + optional: true + + '@tiptap/extension-bullet-list@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + + '@tiptap/extension-code-block@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-code@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-document@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-dropcursor@3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + + '@tiptap/extension-floating-menu@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + '@tiptap/pm': 3.24.0 + optional: true + + '@tiptap/extension-gapcursor@3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + + '@tiptap/extension-hard-break@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-heading@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-horizontal-rule@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-italic@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-link@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + '@tiptap/pm': 3.24.0 + linkifyjs: 4.3.3 + + '@tiptap/extension-list-item@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + + '@tiptap/extension-list-keymap@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + + '@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + '@tiptap/pm': 3.24.0 + + '@tiptap/extension-ordered-list@3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + + '@tiptap/extension-paragraph@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-strike@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-text@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extension-underline@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + + '@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + '@tiptap/pm': 3.24.0 + + '@tiptap/pm@3.24.0': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.7 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + '@tiptap/react@3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + '@tiptap/pm': 3.24.0 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.0 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + '@tiptap/extension-floating-menu': 3.24.0(@floating-ui/dom@1.7.6)(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.24.0': + dependencies: + '@tiptap/core': 3.24.0(@tiptap/pm@3.24.0) + '@tiptap/extension-blockquote': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-bold': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-bullet-list': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)) + '@tiptap/extension-code': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-code-block': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + '@tiptap/extension-document': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-dropcursor': 3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)) + '@tiptap/extension-gapcursor': 3.24.0(@tiptap/extensions@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)) + '@tiptap/extension-hard-break': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-heading': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-horizontal-rule': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + '@tiptap/extension-italic': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-link': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + '@tiptap/extension-list': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + '@tiptap/extension-list-item': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)) + '@tiptap/extension-list-keymap': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)) + '@tiptap/extension-ordered-list': 3.24.0(@tiptap/extension-list@3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0)) + '@tiptap/extension-paragraph': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-strike': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-text': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extension-underline': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0)) + '@tiptap/extensions': 3.24.0(@tiptap/core@3.24.0(@tiptap/pm@3.24.0))(@tiptap/pm@3.24.0) + '@tiptap/pm': 3.24.0 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -8498,6 +8912,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/verror@1.10.11': optional: true @@ -9670,6 +10086,8 @@ snapshots: fast-diff@1.3.0: {} + fast-equals@5.4.0: {} + fast-json-stable-stringify@2.1.0: {} fast-uri@3.1.2: {} @@ -10304,6 +10722,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + linkifyjs@4.3.3: {} + lint-staged@17.0.4: dependencies: listr2: 10.2.1 @@ -10971,6 +11391,8 @@ snapshots: platform: 1.3.6 protobufjs: 7.6.1 + orderedmap@2.1.1: {} + oxfmt@0.47.0: dependencies: tinypool: 2.1.0 @@ -11177,6 +11599,80 @@ snapshots: property-information@7.1.0: {} + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.7: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.7 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.7 + + prosemirror-view@1.41.8: + dependencies: + prosemirror-model: 1.25.7 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + protobufjs@7.6.1: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -11555,6 +12051,8 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + rope-sequence@1.3.4: {} + roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -12209,6 +12707,8 @@ snapshots: vscode-uri@3.1.0: {} + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 diff --git a/src/main/db.schema.ts b/src/main/db.schema.ts index 5ebd7797..f7c10b1e 100644 --- a/src/main/db.schema.ts +++ b/src/main/db.schema.ts @@ -57,6 +57,22 @@ export const appState = sqliteTable("app_state", { value: text("value").notNull(), // JSON }); +/** + * Per-project notes panel content. One row per project, keyed by project id. + * `doc` holds the TipTap (ProseMirror) JSON for the free-form notes editor; + * `todos` holds the structured to-do list. Kept in its own table (rather than a + * `projects` column synced via `dbSyncAll`) so editing notes does not rewrite + * the entire projects+threads snapshot on every keystroke. Orphan rows are + * cleaned up explicitly on project deletion (see db.ts), so no FK is declared — + * this avoids an insert/sync ordering race for a brand-new project. + */ +export const projectNotes = sqliteTable("project_notes", { + projectId: text("project_id").primaryKey(), + doc: text("doc"), // JSON (TipTap document), nullable when empty + todos: text("todos").notNull().default("[]"), // JSON array of NotesTodoItem + updatedAt: text("updated_at").notNull(), +}); + /** * Persisted canonical chat items per thread (for renderer-native chat mode). * Mirrors the renderer's `RuntimeChatItem` shape so we can hydrate the chat diff --git a/src/main/db.ts b/src/main/db.ts index 00a3c219..bc881a81 100644 --- a/src/main/db.ts +++ b/src/main/db.ts @@ -1,7 +1,13 @@ import Database from "better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3"; import { asc, eq } from "drizzle-orm"; -import type { ProjectLocation, Project, Thread, ThreadContextUsage } from "@/shared/contracts"; +import type { + ProjectLocation, + Project, + ProjectNotes, + Thread, + ThreadContextUsage, +} from "@/shared/contracts"; import * as schema from "./db.schema"; let _db: ReturnType | undefined; @@ -86,11 +92,17 @@ export function initDatabase(dbPath: string) { thread_id TEXT PRIMARY KEY REFERENCES threads(id) ON DELETE CASCADE, usage TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS project_notes ( + project_id TEXT PRIMARY KEY, + doc TEXT, + todos TEXT NOT NULL DEFAULT '[]', + updated_at TEXT NOT NULL + ); `); // Baseline schema version for future DB migrations. // New upgrade steps should live behind this gate when we need them. - const SCHEMA_VERSION = 15; + const SCHEMA_VERSION = 16; const storedVersion = Number( ( @@ -237,6 +249,17 @@ export function initDatabase(dbPath: string) { `); } + if (storedVersion < 16) { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS project_notes ( + project_id TEXT PRIMARY KEY, + doc TEXT, + todos TEXT NOT NULL DEFAULT '[]', + updated_at TEXT NOT NULL + ); + `); + } + sqlite .prepare( "INSERT INTO app_state (key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", @@ -732,6 +755,39 @@ function safeParse(json: string): unknown { export function dbDeleteProject(projectId: string): void { const db = getDb(); db.delete(schema.projects).where(eq(schema.projects.id, projectId)).run(); + db.delete(schema.projectNotes).where(eq(schema.projectNotes.projectId, projectId)).run(); +} + +// ── Per-project notes ─────────────────────────────────────────────── + +export function dbGetProjectNotes(projectId: string): ProjectNotes | null { + const db = getDb(); + const row = db + .select() + .from(schema.projectNotes) + .where(eq(schema.projectNotes.projectId, projectId)) + .get(); + if (!row) return null; + const parsedTodos = row.todos ? safeParse(row.todos) : []; + return { + projectId: row.projectId, + doc: row.doc ? (safeParse(row.doc) ?? null) : null, + todos: Array.isArray(parsedTodos) ? (parsedTodos as ProjectNotes["todos"]) : [], + updatedAt: row.updatedAt, + }; +} + +export function dbSetProjectNotes(notes: ProjectNotes): void { + const db = getDb(); + const doc = notes.doc == null ? null : JSON.stringify(notes.doc); + const todos = JSON.stringify(notes.todos ?? []); + db.insert(schema.projectNotes) + .values({ projectId: notes.projectId, doc, todos, updatedAt: notes.updatedAt }) + .onConflictDoUpdate({ + target: schema.projectNotes.projectId, + set: { doc, todos, updatedAt: notes.updatedAt }, + }) + .run(); } /** @@ -747,11 +803,13 @@ export function dbSyncAll(projectsData: Project[], threadsData: Thread[], viewJs ); const incomingProjectIds = new Set(projectsData.map((p) => p.id)); const deleteProject = _sqlite!.prepare("DELETE FROM projects WHERE id = ?"); + const deleteProjectNotes = _sqlite!.prepare("DELETE FROM project_notes WHERE project_id = ?"); const upsertProject = prepareProjectSyncStatement(_sqlite!); for (const pid of existingProjectIds) { if (!incomingProjectIds.has(pid)) { deleteProject.run(pid); + deleteProjectNotes.run(pid); } } for (let i = 0; i < projectsData.length; i++) { diff --git a/src/main/ipc/localHandlers.ts b/src/main/ipc/localHandlers.ts index ed3a6b15..2de761d3 100644 --- a/src/main/ipc/localHandlers.ts +++ b/src/main/ipc/localHandlers.ts @@ -5,6 +5,7 @@ import { openMicrophoneSettings } from "../browser/permissions"; import { dbDeleteProject, dbDeleteThread, + dbGetProjectNotes, dbGetProjects, dbGetState, dbGetThreadCompletedTurns, @@ -14,6 +15,7 @@ import { dbReplaceThreadCompletedTurns, dbReplaceThreadRuntimeSnapshot, dbReplaceThreadRuntimeItems, + dbSetProjectNotes, dbSetState, dbSyncAll, dbUpsertProject, @@ -179,6 +181,8 @@ export function createLocalIpcHandlers( dbReplaceThreadRuntimeSnapshot: ({ threadId, items, turns, contextUsage }) => dbReplaceThreadRuntimeSnapshot(threadId, items, turns, contextUsage), dbGetThreadContextUsage: ({ threadId }) => dbGetThreadContextUsage(threadId), + dbGetProjectNotes: ({ projectId }) => dbGetProjectNotes(projectId), + dbSetProjectNotes: (notes) => dbSetProjectNotes(notes), checkForUpdate: () => options.autoUpdater.checkForUpdate(), startUpdateDownload: () => options.autoUpdater.startUpdateDownload(), installUpdate: () => options.autoUpdater.installUpdate(), diff --git a/src/renderer/actions/notesActions.test.ts b/src/renderer/actions/notesActions.test.ts new file mode 100644 index 00000000..106e1526 --- /dev/null +++ b/src/renderer/actions/notesActions.test.ts @@ -0,0 +1,35 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { useAppStore } from "@/renderer/state/appStore"; +import { newThreadFromText } from "./notesActions"; + +const openNewThread = vi.hoisted(() => vi.fn<(projectId?: string) => void>()); +vi.mock("./threadActions", () => ({ openNewThread })); + +beforeEach(() => { + openNewThread.mockReset(); + useAppStore.setState({ pendingComposerSeeds: {} }); + window.lightcode = {} as typeof window.lightcode; +}); + +describe("newThreadFromText", () => { + it("seeds the composer with trimmed text and opens a new thread", () => { + newThreadFromText("p1", " fix the flaky test "); + expect(useAppStore.getState().pendingComposerSeeds["p1"]?.text).toBe("fix the flaky test"); + expect(openNewThread).toHaveBeenCalledWith("p1"); + }); + + it("bumps the nonce on repeated seeds so the consumer re-fires", () => { + newThreadFromText("p1", "first"); + const firstNonce = useAppStore.getState().pendingComposerSeeds["p1"]!.nonce; + newThreadFromText("p1", "second"); + const seed = useAppStore.getState().pendingComposerSeeds["p1"]!; + expect(seed.text).toBe("second"); + expect(seed.nonce).toBeGreaterThan(firstNonce); + }); + + it("ignores blank text", () => { + newThreadFromText("p1", " "); + expect(useAppStore.getState().pendingComposerSeeds["p1"]).toBeUndefined(); + expect(openNewThread).not.toHaveBeenCalled(); + }); +}); diff --git a/src/renderer/actions/notesActions.ts b/src/renderer/actions/notesActions.ts new file mode 100644 index 00000000..19aabe60 --- /dev/null +++ b/src/renderer/actions/notesActions.ts @@ -0,0 +1,14 @@ +import { useAppStore } from "@/renderer/state/appStore"; +import { openNewThread } from "./threadActions"; + +/** + * Start a new thread seeded with `text` (a to-do item or selected note text). + * Opens a draft composer pre-filled with the text so the user can review/edit + * the prompt and pick a model/mode before launching — it does not auto-send. + */ +export function newThreadFromText(projectId: string, text: string): void { + const trimmed = text.trim(); + if (!trimmed) return; + useAppStore.getState().setComposerSeed(projectId, trimmed); + openNewThread(projectId); +} diff --git a/src/renderer/actions/panelActions.ts b/src/renderer/actions/panelActions.ts index f453e0ff..88cb6a60 100644 --- a/src/renderer/actions/panelActions.ts +++ b/src/renderer/actions/panelActions.ts @@ -83,6 +83,16 @@ export function openUsagePanel(): void { panelStore.openUsagePanel(); } +/** Open the docked notes panel, or close all right-side panels if it is already active. */ +export function openNotesPanel(): void { + const panelStore = usePanelStore.getState(); + if (panelStore.notesPanelOpen && panelStore.rightPanelTab === "notes") { + closeAllPanels(); + return; + } + panelStore.openNotesPanel(); +} + export function openProjectSettings(projectId: string): void { usePanelStore.getState().openProjectSettings(projectId); } diff --git a/src/renderer/components/layout/UnifiedRightPanel.tsx b/src/renderer/components/layout/UnifiedRightPanel.tsx index a5a4f0a8..ec9b89cf 100644 --- a/src/renderer/components/layout/UnifiedRightPanel.tsx +++ b/src/renderer/components/layout/UnifiedRightPanel.tsx @@ -5,6 +5,7 @@ import { Gauge, Globe, Maximize2, + NotebookPen, PanelRightClose, TerminalSquare, } from "lucide-react"; @@ -26,12 +27,14 @@ export function UnifiedRightPanel(props: { filesContent: ReactNode; browserContent: ReactNode; usageContent?: ReactNode; + notesContent?: ReactNode; /** Tab-specific action buttons rendered in the header when the usage tab is active. */ usageHeaderActions?: ReactNode; showTerminalTab?: boolean; showFilesTab?: boolean; showGitTab?: boolean; showUsageTab?: boolean; + showNotesTab?: boolean; projectName: string | undefined; onExpandGitToOverlay?: () => void; onExpandFilesToOverlay?: () => void; @@ -41,6 +44,7 @@ export function UnifiedRightPanel(props: { onOpenFiles?: () => void; onOpenBrowser?: () => void; onOpenUsage?: () => void; + onOpenNotes?: () => void; onClose: () => void; }) { const { @@ -51,11 +55,13 @@ export function UnifiedRightPanel(props: { filesContent, browserContent, usageContent, + notesContent, usageHeaderActions, showTerminalTab = true, showFilesTab = true, showGitTab = true, showUsageTab = true, + showNotesTab = true, projectName, onExpandGitToOverlay, onExpandFilesToOverlay, @@ -65,6 +71,7 @@ export function UnifiedRightPanel(props: { onOpenFiles, onOpenBrowser, onOpenUsage, + onOpenNotes, onClose, } = props; @@ -176,6 +183,19 @@ export function UnifiedRightPanel(props: { ) : null} + {showNotesTab ? ( + + ) : null} + +
+ + +
+ ); +} diff --git a/src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/NotesPanel.tsx b/src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/NotesPanel.tsx new file mode 100644 index 00000000..8e8911f2 --- /dev/null +++ b/src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/NotesPanel.tsx @@ -0,0 +1,126 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { ArrowUpDown } from "lucide-react"; +import { overlaySidebarSurfaceClass } from "@/renderer/components/layout/sidebarChrome"; +import { useNotesStore } from "@/renderer/state/notesStore"; +import { + readStoredBoolean, + readStoredNumber, + writeStoredBoolean, + writeStoredNumber, +} from "@/renderer/utils/localStorage"; +import { NotesEditor } from "./NotesEditor"; +import { TodoList } from "./TodoList"; + +const RATIO_KEY = "lc.notes.topRatio"; +const ORDER_KEY = "lc.notes.notesFirst"; +const MIN_RATIO = 0.15; +const MAX_RATIO = 0.85; + +const clampRatio = (n: number) => Math.min(MAX_RATIO, Math.max(MIN_RATIO, n)); + +/** + * Per-project notes panel: a free-form notes editor and a structured to-do list, + * stacked vertically with a draggable divider to resize and a swap control to + * reorder which one sits on top (both persisted to localStorage). Loads the + * project's notes lazily and flushes pending edits when it unmounts. + */ +export function NotesPanel(props: { projectId: string }) { + const { projectId } = props; + const ensureLoaded = useNotesStore((s) => s.ensureLoaded); + const flush = useNotesStore((s) => s.flush); + const status = useNotesStore((s) => s.byProject[projectId]?.status ?? "unloaded"); + + const containerRef = useRef(null); + const [topRatio, setTopRatio] = useState(() => clampRatio(readStoredNumber(RATIO_KEY, 0.6))); + const [notesFirst, setNotesFirst] = useState(() => readStoredBoolean(ORDER_KEY, true)); + + useEffect(() => { + ensureLoaded(projectId); + return () => flush(projectId); + }, [projectId, ensureLoaded, flush]); + + const onResizePointerDown = useCallback( + (e: React.PointerEvent) => { + e.preventDefault(); + const container = containerRef.current; + if (!container) return; + const rect = container.getBoundingClientRect(); + let latest = topRatio; + const onMove = (ev: PointerEvent) => { + latest = clampRatio((ev.clientY - rect.top) / rect.height); + setTopRatio(latest); + }; + const onUp = () => { + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onUp); + // Persist once at the end of the drag rather than on every pointermove. + writeStoredNumber(RATIO_KEY, latest); + }; + window.addEventListener("pointermove", onMove); + window.addEventListener("pointerup", onUp); + }, + [topRatio], + ); + + if (status !== "ready") { + return ( +
+ Loading notes… +
+ ); + } + + // Both sections stay mounted; their vertical order is controlled with CSS + // `order` so swapping never remounts the editor (which would risk clobbering + // its content). The top section gets a fixed flex-basis (resizable); the + // bottom one fills the remaining space. + const sectionStyle = (isTop: boolean) => + isTop + ? ({ order: 0, flexBasis: `${topRatio * 100}%`, flexGrow: 0, flexShrink: 0 } as const) + : ({ order: 2, flexBasis: 0, flexGrow: 1, flexShrink: 1 } as const); + + return ( +
+
+ +
+
+
+ +
+
+ +
+
+ ); +} diff --git a/src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/TodoList.tsx b/src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/TodoList.tsx new file mode 100644 index 00000000..db81261e --- /dev/null +++ b/src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/TodoList.tsx @@ -0,0 +1,99 @@ +import { useState } from "react"; +import { PointerActivationConstraints } from "@dnd-kit/dom"; +import { DragDropProvider, KeyboardSensor, PointerSensor, type DragEndEvent } from "@dnd-kit/react"; +import { isSortable } from "@dnd-kit/react/sortable"; +import { Plus } from "lucide-react"; +import { newThreadFromText } from "@/renderer/actions/notesActions"; +import { useNotesStore } from "@/renderer/state/notesStore"; +import { TodoRow } from "./TodoRow"; + +const todoListSensors = [ + PointerSensor.configure({ + activationConstraints: [new PointerActivationConstraints.Distance({ value: 5 })], + }), + KeyboardSensor.configure({ + keyboardCodes: { + start: ["Space"], + cancel: ["Escape"], + end: ["Space", "Enter", "Tab"], + up: ["ArrowUp"], + down: ["ArrowDown"], + left: ["ArrowLeft"], + right: ["ArrowRight"], + }, + }), +]; + +/** Structured per-project to-do list rendered alongside the notes editor. */ +export function TodoList(props: { projectId: string }) { + const { projectId } = props; + const todos = useNotesStore((s) => s.byProject[projectId]?.todos ?? []); + const addTodo = useNotesStore((s) => s.addTodo); + const toggleTodo = useNotesStore((s) => s.toggleTodo); + const updateTodoText = useNotesStore((s) => s.updateTodoText); + const removeTodo = useNotesStore((s) => s.removeTodo); + const moveTodo = useNotesStore((s) => s.moveTodo); + const [draft, setDraft] = useState(""); + + const remaining = todos.reduce((n, t) => (t.done ? n : n + 1), 0); + + function handleDragEnd(event: DragEndEvent) { + if (event.canceled) return; + const src = event.operation.source; + if (!src || !isSortable(src)) return; + if (src.initialIndex === src.index) return; + moveTodo(projectId, src.initialIndex, src.index); + } + + const submitNew = () => { + const text = draft.trim(); + if (!text) return; + addTodo(projectId, text); + setDraft(""); + }; + + return ( +
+
+ To-dos + {todos.length > 0 ? ( + {remaining} open + ) : null} +
+
+ +
+ {todos.map((todo, index) => ( + toggleTodo(projectId, todo.id)} + onChangeText={(text) => updateTodoText(projectId, todo.id, text)} + onRemove={() => removeTodo(projectId, todo.id)} + onNewThread={() => newThreadFromText(projectId, todo.text)} + /> + ))} +
+
+ {/* The add-to-do field is the final row of the list, styled like a to-do. */} +
+ + setDraft(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + submitNew(); + } + }} + /> +
+
+
+ ); +} diff --git a/src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/TodoRow.tsx b/src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/TodoRow.tsx new file mode 100644 index 00000000..7249afd6 --- /dev/null +++ b/src/renderer/views/MainView/parts/RightPanel/parts/NotesPanel/TodoRow.tsx @@ -0,0 +1,155 @@ +import { useEffect, useRef, useState } from "react"; +import { useSortable } from "@dnd-kit/react/sortable"; +import { Check, MessageSquarePlus, Trash2 } from "lucide-react"; +import type { NotesTodoItem } from "@/shared/contracts"; +import { ContextMenu } from "@/renderer/components/common"; + +const todoActionButtonClass = + "flex size-[18px] shrink-0 items-center justify-center rounded text-muted/55 transition group-hover/todo:opacity-100 focus-visible:opacity-100"; + +export function TodoRow(props: { + todo: NotesTodoItem; + index: number; + projectId: string; + onToggle: () => void; + onChangeText: (text: string) => void; + onRemove: () => void; + onNewThread: () => void; +}) { + const { todo, index, projectId, onToggle, onChangeText, onRemove, onNewThread } = props; + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(todo.text); + const inputRef = useRef(null); + + // Reordered through the local list DragDropProvider. Dragging is disabled + // while editing so the inline input stays interactive. + const { ref, handleRef, isDragging } = useSortable({ + id: `todo:${todo.id}`, + index, + type: "todo", + accept: "todo", + group: `todos:${projectId}`, + disabled: editing, + data: { type: "todo", todoId: todo.id, projectId, text: todo.text }, + }); + + useEffect(() => { + if (editing) { + inputRef.current?.focus(); + inputRef.current?.select(); + } + }, [editing]); + + const commitEdit = () => { + setEditing(false); + const next = draft.trim(); + if (next && next !== todo.text) { + onChangeText(next); + } else { + setDraft(todo.text); + } + }; + + const actionVisibilityClass = isDragging ? "opacity-100" : "opacity-0"; + + return ( + , + }, + { id: "delete", label: "Delete", icon: , variant: "danger" }, + ]} + onAction={(key) => { + if (key === "new-thread") onNewThread(); + else if (key === "delete") onRemove(); + }} + > +
+ + {editing ? ( + setDraft(e.target.value)} + onBlur={commitEdit} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + commitEdit(); + } else if (e.key === "Escape") { + e.preventDefault(); + setDraft(todo.text); + setEditing(false); + } + }} + /> + ) : ( + + )} +
+ + +
+
+
+ ); +} diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts index 4e132e9a..2e4ac8e6 100644 --- a/src/shared/contracts.ts +++ b/src/shared/contracts.ts @@ -12,3 +12,4 @@ export * from "./contracts/runtimeEvent"; export * from "./contracts/agentInstance"; export * from "./contracts/workflowTranscript"; export * from "./contracts/usage"; +export * from "./contracts/notes"; diff --git a/src/shared/contracts/notes.test.ts b/src/shared/contracts/notes.test.ts new file mode 100644 index 00000000..5427a73e --- /dev/null +++ b/src/shared/contracts/notes.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { ZodError } from "zod"; +import { notesTodoItemSchema, projectNotesSchema } from "./notes"; + +describe("projectNotesSchema", () => { + it("parses a full payload", () => { + const parsed = projectNotesSchema.parse({ + projectId: "p1", + doc: { type: "doc", content: [] }, + todos: [{ id: "t1", text: "ship it", done: false, createdAt: "2026-01-01T00:00:00.000Z" }], + updatedAt: "2026-01-01T00:00:00.000Z", + }); + expect(parsed.todos).toHaveLength(1); + expect(parsed.todos[0]?.text).toBe("ship it"); + }); + + it("allows a null doc and empty todos", () => { + const parsed = projectNotesSchema.parse({ + projectId: "p1", + doc: null, + todos: [], + updatedAt: "2026-01-01T00:00:00.000Z", + }); + expect(parsed.doc).toBeNull(); + expect(parsed.todos).toEqual([]); + }); + + it("rejects an empty projectId", () => { + expect(() => + projectNotesSchema.parse({ projectId: "", doc: null, todos: [], updatedAt: "t" }), + ).toThrow(ZodError); + }); +}); + +describe("notesTodoItemSchema", () => { + it("rejects a todo missing its id", () => { + expect(() => notesTodoItemSchema.parse({ text: "x", done: false, createdAt: "t" })).toThrow( + ZodError, + ); + }); +}); diff --git a/src/shared/contracts/notes.ts b/src/shared/contracts/notes.ts new file mode 100644 index 00000000..7c8db16f --- /dev/null +++ b/src/shared/contracts/notes.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +/** A single structured to-do item in a project's notes panel. */ +export const notesTodoItemSchema = z.object({ + id: z.string().min(1), + text: z.string(), + done: z.boolean(), + createdAt: z.string().min(1), +}); +export type NotesTodoItem = z.infer; + +/** + * Per-project notes payload. `doc` is the TipTap (ProseMirror) JSON document for + * the free-form notes editor, stored opaquely; `null` when the editor is empty. + * `todos` is the structured to-do list rendered beneath the editor. + */ +export const projectNotesSchema = z.object({ + projectId: z.string().min(1), + doc: z.unknown().nullable(), + todos: z.array(notesTodoItemSchema), + updatedAt: z.string().min(1), +}); +export type ProjectNotes = z.infer; diff --git a/src/shared/ipc/procedureMap.ts b/src/shared/ipc/procedureMap.ts index 9b3cf130..fc1b44b0 100644 --- a/src/shared/ipc/procedureMap.ts +++ b/src/shared/ipc/procedureMap.ts @@ -79,6 +79,8 @@ export const MAIN_LOCAL_PROCEDURE_NAMES = [ "dbReplaceThreadCompletedTurns", "dbReplaceThreadRuntimeSnapshot", "dbGetThreadContextUsage", + "dbGetProjectNotes", + "dbSetProjectNotes", "checkForUpdate", "startUpdateDownload", "installUpdate", diff --git a/src/shared/ipc/procedures/db.ts b/src/shared/ipc/procedures/db.ts index 89a7bd68..e3b598aa 100644 --- a/src/shared/ipc/procedures/db.ts +++ b/src/shared/ipc/procedures/db.ts @@ -1,16 +1,18 @@ import { z } from "zod"; import { projectSchema, threadSchema } from "../../contracts"; -import type { Project, Thread, ThreadContextUsage } from "../../contracts"; +import type { Project, ProjectNotes, Thread, ThreadContextUsage } from "../../contracts"; import { defineIpcProcedure, defineNoArgProcedure, definePayloadProcedure } from "../core"; import { dbDeleteProjectPayloadSchema, dbDeleteThreadPayloadSchema, dbGetCompletedTurnsPayloadSchema, + dbGetProjectNotesPayloadSchema, dbGetRuntimeItemsPayloadSchema, dbGetThreadContextUsagePayloadSchema, dbReplaceCompletedTurnsPayloadSchema, dbReplaceRuntimeItemsPayloadSchema, dbReplaceRuntimeSnapshotPayloadSchema, + dbSetProjectNotesPayloadSchema, dbStateKeySchema, dbStatePayloadSchema, dbSyncAllPayloadSchema, @@ -108,4 +110,17 @@ export const dbProcedures = { >("dbGetThreadContextUsage", "main-local", dbGetThreadContextUsagePayloadSchema, (threadId) => dbGetThreadContextUsagePayloadSchema.parse({ threadId }), ), + dbGetProjectNotes: defineIpcProcedure< + [string], + z.infer, + ProjectNotes | null, + "main-local" + >("dbGetProjectNotes", "main-local", dbGetProjectNotesPayloadSchema, (projectId) => + dbGetProjectNotesPayloadSchema.parse({ projectId }), + ), + dbSetProjectNotes: definePayloadProcedure( + "dbSetProjectNotes", + "main-local", + dbSetProjectNotesPayloadSchema, + ), } as const; diff --git a/src/shared/ipc/schemas.ts b/src/shared/ipc/schemas.ts index b266505c..3582ba4e 100644 --- a/src/shared/ipc/schemas.ts +++ b/src/shared/ipc/schemas.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import type { RuntimeEvent, WorkflowRun } from "../contracts"; import { projectLocationSchema, + projectNotesSchema, projectSchema, threadContextUsageSchema, threadSchema, @@ -119,6 +120,11 @@ export const dbGetThreadContextUsagePayloadSchema = z.object({ threadId: z.string().min(1), }); +export const dbGetProjectNotesPayloadSchema = z.object({ + projectId: z.string().min(1), +}); +export const dbSetProjectNotesPayloadSchema = projectNotesSchema; + export const openExternalPayloadSchema = z.string().min(1); export const windowChromePayloadSchema = z.object({