From 62654c97a7d02a148da8ab630d760182a24fa513 Mon Sep 17 00:00:00 2001 From: Alex Ray Date: Wed, 20 May 2026 13:34:05 -0700 Subject: [PATCH 1/3] Add file watching, table support, and project docs - File watcher: use notify crate to watch repo for .md changes, emit Tauri events, auto-refresh editor and file tree - Table support: add TipTap table extensions so markdown tables render in the WYSIWYG editor - Initialize brain-implant for knowledge management - Add design system prompt for future redesign - Update docs: fix history duplicates, add table example to welcome doc, minor getting-started correction Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 20 +++ brain.yaml | 65 +++++++++ docs/.brain-state.json | 14 ++ docs/backlog/items.md | 29 ++++ docs/backlog/questions.md | 27 ++++ docs/decisions/_template.md | 20 +++ docs/design-system-prompt.md | 148 ++++++++++++++++++++ docs/getting-started.md | 2 +- docs/history.md | 41 ++++++ docs/reports/daily/.gitkeep | 0 docs/reports/weekly/.gitkeep | 0 docs/sessions/.gitkeep | 0 docs/sprints/.current-sprint | 0 docs/sprints/_sprint-template/TEMPLATE.md | 42 ++++++ docs/welcome.md | 10 ++ package-lock.json | 87 ++++++++++-- package.json | 4 + src-tauri/Cargo.lock | 159 +++++++++++++++++++++- src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 4 + src-tauri/src/watcher.rs | 102 ++++++++++++++ src/App.jsx | 64 ++++++++- src/components/Editor/Editor.jsx | 8 ++ src/index.css | 19 +++ src/tauri-api.js | 4 + 25 files changed, 854 insertions(+), 16 deletions(-) create mode 100644 CLAUDE.md create mode 100644 brain.yaml create mode 100644 docs/.brain-state.json create mode 100644 docs/backlog/items.md create mode 100644 docs/backlog/questions.md create mode 100644 docs/decisions/_template.md create mode 100644 docs/design-system-prompt.md create mode 100644 docs/history.md create mode 100644 docs/reports/daily/.gitkeep create mode 100644 docs/reports/weekly/.gitkeep create mode 100644 docs/sessions/.gitkeep create mode 100644 docs/sprints/.current-sprint create mode 100644 docs/sprints/_sprint-template/TEMPLATE.md create mode 100644 src-tauri/src/watcher.rs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..70aa504 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,20 @@ + +## Knowledge Base (Brain Implant) + +This project uses [brain-implant](https://github.com/jalexray/Brain-Implant) for knowledge management. + +### Brain Structure +- Brain root: `docs/` +- Session logs: `docs/sessions/` +- Decision records: `docs/decisions/` +- Development history: `docs/history.md` +- Config: `brain.yaml` + +### Session Workflow +- `/checkin` — Load context from previous session +- `/checkout` — Document session + capture decisions + doc staleness check + +### Doc Maintenance +- `/update-brain` — Scan changes, update stale docs, sync memories, append to history +- `/audit-docs` — Full documentation health audit with freshness grading + diff --git a/brain.yaml b/brain.yaml new file mode 100644 index 0000000..ebe1512 --- /dev/null +++ b/brain.yaml @@ -0,0 +1,65 @@ +project: + name: "docugit" + description: "" + repo: "jalexray/docugit" + +paths: + brain_root: "docs" + session_logs: "docs/sessions" + decisions: "docs/decisions" + sprints: "docs/sprints" # null if no sprints + reports: + daily: "docs/reports/daily" # null to disable + weekly: "docs/reports/weekly" # null to disable + +team: + members: [] + audience: "" # Who reads reports (e.g., "engineering team") + +sprints: + naming: "v{N}-{slug}" # v1-POC, sprint-1-launch, etc. + +reports: + daily: + enabled: false + data_sources: [] + # Example pluggable data source: + # - name: "API Health" + # type: "shell" + # command: "curl -s https://myapp.com/api/health" + weekly: + enabled: false + tone: "casual-collegial" # casual-collegial | professional | technical + signoff: "" + +staleness: + warn_days: 14 + critical_days: 30 + ignore_patterns: + - "**/reports/daily/*" + - "**/reports/weekly/*" + - "**/sessions/*" + - "**/.gitkeep" + +memory: + sync_enabled: true + promote_types: + - project + - reference + - feedback + +schedule: + audit: + enabled: false + cron: "0 18 * * 0" # Weekly Sunday 6pm + action: "/audit-docs" + on_stale: "log-only" # github-issue | slack | email | log-only + reflection: + enabled: false + cron: "0 9 * * 1" # Monday 9am + action: "/update-brain" + auto_apply: false + +# Reserved for future typed integrations (PostHog, Stripe, Linear, etc.) +# See README for data_sources recipes that work today. +integrations: {} diff --git a/docs/.brain-state.json b/docs/.brain-state.json new file mode 100644 index 0000000..965fd46 --- /dev/null +++ b/docs/.brain-state.json @@ -0,0 +1,14 @@ +{ + "schema_version": 1, + "last_update_brain_run": "2026-05-20T00:00:00-07:00", + "last_update_brain_commit": "d1d1cf0e76e3be7a8b0d00c0f80195a5bb7d98f9", + "last_audit_run": "2026-05-20T00:00:00-07:00", + "audit_results": { + "grade": "B", + "total_audited": 5, + "fresh": 3, + "aging": 0, + "stale": 2, + "critical": 0 + } +} diff --git a/docs/backlog/items.md b/docs/backlog/items.md new file mode 100644 index 0000000..7428268 --- /dev/null +++ b/docs/backlog/items.md @@ -0,0 +1,29 @@ +# Backlog + +## Open + + + +## Completed + + diff --git a/docs/backlog/questions.md b/docs/backlog/questions.md new file mode 100644 index 0000000..36a2536 --- /dev/null +++ b/docs/backlog/questions.md @@ -0,0 +1,27 @@ +# Open Questions + +Questions that need answers before work can proceed. Move answered questions to the bottom. + +## Open + + + +## Answered + + diff --git a/docs/decisions/_template.md b/docs/decisions/_template.md new file mode 100644 index 0000000..d5adae4 --- /dev/null +++ b/docs/decisions/_template.md @@ -0,0 +1,20 @@ +# [Decision Title] + +**Date**: YYYY-MM-DD +**Status**: proposed | accepted | superseded by [link] + +## Context + +What prompted this decision? What problem were we solving? + +## Decision + +What did we decide? + +## Reasoning + +Why this over alternatives? What trade-offs did we consider? + +## Consequences + +What follows from this decision? What doors does it open or close? diff --git a/docs/design-system-prompt.md b/docs/design-system-prompt.md new file mode 100644 index 0000000..474b1af --- /dev/null +++ b/docs/design-system-prompt.md @@ -0,0 +1,148 @@ +# DocuGit Design System Brief + +## What is DocuGit? + +DocuGit is a macOS desktop application for editing markdown files with built-in git integration. Think of it as a lightweight, Word-like writing environment that lives on top of a git repository. + +### The problem it solves + +Developers and technical writers increasingly use LLMs (Claude, ChatGPT, etc.) to generate markdown documentation — project specs, READMEs, design docs, technical guides. These documents end up in git repos, but editing them is awkward: + +- **IDEs** (VS Code, JetBrains) treat markdown as code. You're staring at raw syntax, managing split previews, and fighting an interface built for programming. +- **Dedicated markdown editors** (Obsidian, Typora) don't understand git. You lose staging, diffing, branching, and commit workflows. +- **Google Docs / Notion** aren't local-first, don't output clean markdown, and don't integrate with git at all. + +DocuGit bridges this gap: a clean WYSIWYG editor where markdown looks like a finished document, with git operations always one click away. + +### Target audience + +- **Developers** who write and maintain documentation alongside code +- **Technical writers** working in git-based repos +- **LLM power users** who generate markdown docs as part of AI-assisted workflows and need a comfortable place to review, edit, and version them +- **Solo creators and small teams** who want a simple, local-first writing tool without SaaS overhead + +The common thread: people who work with markdown in git repos and want a writing-first experience, not a coding-first one. + +## Current state of the app + +DocuGit is functional but visually undesigned. The UI was built pragmatically with Tailwind utility classes — it works, but there's no intentional design system, no visual identity, and several inconsistencies. It's ready for a real design pass. + +### Tech stack + +- **Desktop shell:** Tauri 2 (Rust backend, WebView frontend) — macOS only for now +- **Frontend:** React 19, Tailwind CSS 4, TipTap v3 (WYSIWYG editor) +- **Embedded terminal:** xterm.js with PTY support +- **File watching:** Rust `notify` crate — auto-refreshes editor when files change on disk + +### Application structure + +The app is a single-window, multi-panel layout: + +``` ++-------+----------------------------+---------+ +| Header (logo, panel toggles, repo path) | ++-------+----------------------------+---------+ +| | | | +| Side | Editor | Git | +| bar | (TipTap WYSIWYG) | Panel | +| 256px | | 320px | +| | | | +| | | | ++-------+----------------------------+---------+ +| Terminal (xterm.js, collapsible) | ++----------------------------------------------+ +``` + +**Header bar (~40px):** "DocuGit" text logo, three toggle buttons (Files / Git / Terminal), and the current repo path right-aligned. + +**Sidebar (256px, collapsible):** +- Repo picker with four modes (open doc, choose folder, scan for repos, detect from path) +- Hierarchical file tree showing only `.md` files +- "New File" creation form +- Active file highlighted in blue + +**Editor (flexible center):** +- Formatting toolbar: headings (H1-H3), bold/italic/strike/code, lists, blockquote, code block, horizontal rule, undo/redo, save button +- TipTap WYSIWYG area with prose typography (styled via @tailwindcss/typography) +- File path display with dirty indicator (orange asterisk) +- Empty state when no file is open + +**Git panel (320px, collapsible):** +- Branch selector with create-new-branch form +- Changed/untracked files list with checkboxes for selective staging +- Staged files list with unstage option +- Commit form with message textarea +- Push button with unpushed commit list + +**Terminal panel (~288px, collapsible):** +- Dark-themed xterm.js terminal with full PTY support +- Header bar with close button + +### Current visual style + +The app uses an ad-hoc gray palette with minimal color accents: + +- **Backgrounds:** White (editor), Gray-50 (sidebar, git panel, header), #1e1e1e (terminal) +- **Interactive elements:** Gray-700 for primary buttons, Blue-600 for save/links, Green-600 for commit +- **Text:** Gray-400 through Gray-700 for varying emphasis +- **Borders:** Gray-200 (light dividers), Gray-300 (inputs) +- **Typography:** System font, mostly xs (0.75rem) for UI chrome, prose sizing in editor +- **Active states:** Inconsistent — sometimes blue highlight (file tree), sometimes gray-700 with white text (toolbar, toggles) + +### Known design issues + +- No visual identity — "DocuGit" is just text, no logo or brand treatment +- No dark mode (terminal is dark but everything else is light-only) +- Active/selected states use two different color systems (blue vs. gray) with no clear logic +- UI text is uniformly tiny (xs/0.75rem) — functional but cramped +- No spacing rhythm or consistent sizing scale +- Toolbar buttons are text-only with no icons +- No visual hierarchy between panels — everything is the same gray +- No transitions or micro-animations +- File tree has no file type icons +- Git status labels are plain text with no color coding +- No empty states with personality (just gray text) +- Accessibility gaps: missing ARIA labels, no focus management for dropdowns + +## What I need + +Design a **design system** for DocuGit — a cohesive set of foundations and components that can be applied across the entire app. This should feel like a polished, native-quality macOS writing tool, not a web app crammed into a window. + +### Design system deliverables + +**1. Foundations** + +- **Color palette:** Light mode and dark mode tokens. The app should feel calm and paper-like in the editor area but capable and dense in the chrome (sidebar, git panel). Consider how the terminal's dark theme coexists with both modes. +- **Typography scale:** A clear type ramp for UI chrome (labels, buttons, file names) vs. editor content (prose). Include font recommendations — system fonts are fine, but specify the stack and any weight/size pairings. +- **Spacing and sizing:** A consistent spacing scale and component sizing system (button heights, input heights, panel padding, icon sizes). +- **Elevation and layering:** How panels, dropdowns, modals, and tooltips relate spatially. Shadow and border treatments. +- **Iconography direction:** Style recommendation for icons (outlined, solid, stroke weight). Don't need individual icons designed, just the system. +- **Motion principles:** Guidelines for transitions (panel show/hide, dropdown open, state changes). Keep it subtle and fast. + +**2. Core components** + +Design these key components within the system: + +- **Buttons:** Primary, secondary, ghost, destructive. Active/disabled/loading states. +- **Toggle buttons:** For panel visibility (Files/Git/Terminal). On/off states that are immediately readable. +- **Text inputs and textareas:** For file names, commit messages, branch names. Focus, error, disabled states. +- **File tree item:** File and folder variants. Selected, hover, and depth-indentation treatment. Consider file status indicators (modified, staged, new) that align with git panel. +- **Toolbar button:** For the editor formatting bar. Active (format applied), hover, disabled. Consider icon+text vs. icon-only. +- **Dropdown / select:** For branch picker and any future selects. Open/closed states, selected item treatment. +- **Status badges:** For git file statuses (modified, added, deleted, untracked, staged). Small, color-coded, readable at a glance. +- **Panel chrome:** Header/title treatment for sidebar, git panel, terminal. Collapse/expand affordance. + +**3. Layout system** + +- Panel structure and resizing behavior +- How panels collapse and expand (animation, toggle affordance) +- Responsive behavior as the window resizes (minimum widths, what collapses first) +- Editor area treatment — should it feel like a page (centered, max-width) or fill available space? + +### Design direction + +- **Native feel:** Should feel at home on macOS — respect platform conventions for window chrome, control sizing, and interaction patterns +- **Writing-first:** The editor is the hero. Everything else should support the writing experience without competing for attention +- **Professional but approachable:** This isn't a toy or a hacker tool. It should feel trustworthy for real work while still being pleasant to use +- **Information density:** The sidebar and git panel should be compact and scannable. Don't waste space, but don't cram things either +- **Calm:** Muted palette with purposeful color. Reserve bright colors for meaningful signals (unsaved changes, git status, errors) diff --git a/docs/getting-started.md b/docs/getting-started.md index 7104442..b919c93 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -57,4 +57,4 @@ New branches are created from your current HEAD and checked out automatically. - **Cmd+B** bolds selected text - **Cmd+I** italicizes selected text - Click **Browse for Document** in the sidebar to open any markdown file on your machine -- The Git panel toggles with the **Git** button in the top-right corner +- The Git panel toggles with the **Git** button in the header bar diff --git a/docs/history.md b/docs/history.md new file mode 100644 index 0000000..e020d97 --- /dev/null +++ b/docs/history.md @@ -0,0 +1,41 @@ +# Project History + +Auto-generated by brain-implant from git history on 2026-05-20. +Edit freely — this is your document now. Run `/update-brain` to append recent activity. + +--- + +## Timeline + +### 2026-05 — May (current) +_10+ commits, 2 contributors_ + +- Add one-line start script, clean up README and config +- Adding toggle +- Support non-git dirs, add collapsible sidebar and git panel +- Migrate from Flask web app to Tauri macOS desktop app +- Add embedded terminal with PTY support +- Add file watching via `notify` crate (auto-refresh editor on external changes) +- Add TipTap table extension support +- Brain-implant initialized for knowledge management + +--- + +## Architecture Evolution + +Key structural changes detected from git history: + +- **2026-05-01**: Project initialized (React + Flask + TipTap) +- **2026-05-15**: Migrate from Flask web app to Tauri macOS desktop app +- **2026-05-15**: Add embedded terminal with PTY support (portable-pty + xterm.js) +- **2026-05-20**: Add filesystem watcher (Rust `notify` crate) for live document refresh + +--- + +## Key Decisions + +Decision signals extracted from commit messages: + +- **2026-05-15**: Migrate from Flask web app to Tauri macOS desktop app — single binary, native feel, PTY support +- **2026-05-20**: Use `notify` crate for file watching over polling — event-driven, debounced, filtered to .md files + diff --git a/docs/reports/daily/.gitkeep b/docs/reports/daily/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/reports/weekly/.gitkeep b/docs/reports/weekly/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/sessions/.gitkeep b/docs/sessions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/sprints/.current-sprint b/docs/sprints/.current-sprint new file mode 100644 index 0000000..e69de29 diff --git a/docs/sprints/_sprint-template/TEMPLATE.md b/docs/sprints/_sprint-template/TEMPLATE.md new file mode 100644 index 0000000..9928bff --- /dev/null +++ b/docs/sprints/_sprint-template/TEMPLATE.md @@ -0,0 +1,42 @@ +# Sprint: [Name] + +## Executive Summary + +What this sprint delivers and why. 2-3 sentences. + +## Context + +What preceded this sprint. What problems or opportunities prompted it. + +### What the Previous Sprint Built + +- Item 1 +- Item 2 + +### What the Previous Sprint Revealed + +- Problem 1 +- Problem 2 + +## Goals + +### Primary + +- Goal 1 +- Goal 2 + +### Success Metrics + +| Metric | Target | Rationale | +|--------|--------|-----------| +| ... | ... | ... | + +## Design Principles + +- Principle 1 +- Principle 2 + +## Out of Scope + +- Item 1 +- Item 2 diff --git a/docs/welcome.md b/docs/welcome.md index 0ba6f29..349b9a9 100644 --- a/docs/welcome.md +++ b/docs/welcome.md @@ -32,6 +32,16 @@ def hello(): > DocuGit gives you a Word-like editing experience for markdown files that live in git repos. +## Tables Work Too + +| Feature | Shortcut | +|---------|----------| +| Save | Cmd+S | +| Bold | Cmd+B | +| Italic | Cmd+I | + +Tables render as formatted grids right in the editor. Try adding a row. + ## What's Next? Once you've made some edits and saved, head over to the **Git panel** on the right side of the screen to stage, commit, and push your changes. See `getting-started.md` for a walkthrough. diff --git a/package-lock.json b/package-lock.json index 5af7b94..f284e65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,10 @@ "@tailwindcss/vite": "^4.1.18", "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tiptap/extension-table": "^3.22.5", + "@tiptap/extension-table-cell": "^3.22.5", + "@tiptap/extension-table-header": "^3.22.5", + "@tiptap/extension-table-row": "^3.22.5", "@tiptap/markdown": "^3.22.5", "@tiptap/pm": "^3.22.5", "@tiptap/react": "^3.22.5", @@ -66,6 +70,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -900,17 +905,6 @@ "@floating-ui/utils": "^0.2.11" } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, "node_modules/@floating-ui/utils": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", @@ -1857,6 +1851,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.5.tgz", "integrity": "sha512-L1lhWz6ujGny8LduTJ7MBWYhzigwOvfUJUrJ7IzOJSuy3+OAzisdGDD1GV7LEO/hU0Hr2Mkm1wajRIHExvS9HQ==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2079,6 +2074,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.5.tgz", "integrity": "sha512-cVO3ZHCgxAWZ4zrFSs81FO2nyCk1wb2EHkpLpW98FzbJLkN9rDkazhW99P3HRWy/CvUldOT+8ecI1YrQtBojMg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2153,6 +2149,60 @@ "@tiptap/core": "3.22.5" } }, + "node_modules/@tiptap/extension-table": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.22.5.tgz", + "integrity": "sha512-GMBM07bCwzHx1NK08zXRr2mNTDnP78Hd0VxFsRBIDFddDMZ2qG5jhwKHXN5cHMTrdWokWFUjvnEeJeV3guHoGg==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5", + "@tiptap/pm": "3.22.5" + } + }, + "node_modules/@tiptap/extension-table-cell": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.22.5.tgz", + "integrity": "sha512-Wn4asCgNLfOPH5EOpiMjzOJXTZvv+TTqUT+gzm2fV69ZkleCGNO0BZwuR/TCIDLGIArbvHzyYy2/lJAfG4UCtg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "3.22.5" + } + }, + "node_modules/@tiptap/extension-table-header": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.22.5.tgz", + "integrity": "sha512-aJmbgbO6QbSj0Rw3X4ogGPyd+8FwP6RgG71Dpa3NovzVkqJc3ZUq0wC3XH48U9Hd89F8f4AggFgHjU6/kQAgQQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "3.22.5" + } + }, + "node_modules/@tiptap/extension-table-row": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.22.5.tgz", + "integrity": "sha512-9A2BdX+R+P71f192Fo74OttMHj1WoFVO0ezaCzFbT8uNVG3nCJ7B5/1UkTlzqDdGOuWh1VpR63pFZP9LFsUv6A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "3.22.5" + } + }, "node_modules/@tiptap/extension-text": { "version": "3.22.5", "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.5.tgz", @@ -2184,6 +2234,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.5.tgz", "integrity": "sha512-Ifg4MzKCj3uRqe3ieTwYnomu2y4p7EXr2avVSKZYfh12i2dyWe2Gkn1KuZDREANVE+gHqFlQjJRYzhJFwzSCrg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2215,6 +2266,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.5.tgz", "integrity": "sha512-Cr9Mv4igxvI2tKMiahw48sZxva3PfDzypErH8IB82N+9qa9n9ygVMt0BOaDg53hLKxEEVeYr2S/wCcJIVFgBTw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", @@ -2360,6 +2412,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2369,6 +2422,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2421,6 +2475,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2526,6 +2581,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2779,6 +2835,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3766,6 +3823,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3968,6 +4026,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3977,6 +4036,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4132,7 +4192,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -4237,6 +4298,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4364,6 +4426,7 @@ "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 335544c..6b57564 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,10 @@ "@tailwindcss/vite": "^4.1.18", "@tauri-apps/api": "^2.11.0", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tiptap/extension-table": "^3.22.5", + "@tiptap/extension-table-cell": "^3.22.5", + "@tiptap/extension-table-header": "^3.22.5", + "@tiptap/extension-table-row": "^3.22.5", "@tiptap/markdown": "^3.22.5", "@tiptap/pm": "^3.22.5", "@tiptap/react": "^3.22.5", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c5203b1..dc50c72 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -732,6 +732,7 @@ version = "0.1.0" dependencies = [ "dirs", "log", + "notify", "portable-pty", "serde", "serde_json", @@ -905,6 +906,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -975,6 +986,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "funty" version = "2.0.0" @@ -1660,6 +1680,26 @@ dependencies = [ "cfb", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "ioctl-rs" version = "0.1.6" @@ -1793,6 +1833,26 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.11.1", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1938,6 +1998,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.2.0" @@ -2014,6 +2086,25 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.11.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "num-conv" version = "0.2.1" @@ -3855,7 +3946,7 @@ checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", - "mio", + "mio 1.2.0", "pin-project-lite", "socket2", "windows-sys 0.61.2", @@ -4691,6 +4782,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4733,6 +4833,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -4790,6 +4905,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4808,6 +4929,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4826,6 +4953,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4856,6 +4989,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4874,6 +5013,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4892,6 +5037,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4910,6 +5061,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6310f7e..77cdcf1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,3 +26,4 @@ tauri-plugin-log = "2" dirs = "6" tauri-plugin-dialog = "2" portable-pty = "0.8" +notify = "6.1" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4e7d1d9..5473854 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,6 +2,7 @@ mod config; mod files; mod git; mod terminal; +mod watcher; #[tauri::command] fn get_config() -> Result { @@ -56,6 +57,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .manage(terminal::PtyState::new()) + .manage(watcher::WatcherState::new()) .setup(|app| { if cfg!(debug_assertions) { app.handle().plugin( @@ -92,6 +94,8 @@ pub fn run() { terminal::pty_write, terminal::pty_resize, terminal::pty_kill, + watcher::watch_repo, + watcher::unwatch_repo, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/watcher.rs b/src-tauri/src/watcher.rs new file mode 100644 index 0000000..a5a9215 --- /dev/null +++ b/src-tauri/src/watcher.rs @@ -0,0 +1,102 @@ +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::Instant; +use tauri::{AppHandle, Emitter, Manager}; + +pub struct WatcherState { + watcher: Mutex>, +} + +impl WatcherState { + pub fn new() -> Self { + Self { + watcher: Mutex::new(None), + } + } +} + +#[tauri::command] +pub fn watch_repo(repo_path: String, app: AppHandle) -> Result<(), String> { + let state = app.state::(); + let mut guard = state.watcher.lock().map_err(|e| e.to_string())?; + + // Drop existing watcher + *guard = None; + + let repo = PathBuf::from(&repo_path); + let debounce: Mutex> = Mutex::new(HashMap::new()); + let app_handle = app.clone(); + + let mut watcher = RecommendedWatcher::new( + move |res: Result| { + let Ok(event) = res else { return }; + + let is_structural = matches!( + event.kind, + notify::EventKind::Create(_) | notify::EventKind::Remove(_) + ); + let is_modify = matches!(event.kind, notify::EventKind::Modify(_)); + + if !is_structural && !is_modify { + return; + } + + let now = Instant::now(); + let mut seen = debounce.lock().unwrap(); + let mut tree_dirty = false; + + for path in &event.paths { + let is_md = path + .extension() + .map(|e| e.eq_ignore_ascii_case("md")) + .unwrap_or(false); + if !is_md { + continue; + } + + // Debounce: skip if seen < 1s ago + if let Some(last) = seen.get(path) { + if now.duration_since(*last).as_millis() < 1000 { + continue; + } + } + seen.insert(path.clone(), now); + + if let Ok(rel) = path.strip_prefix(&repo) { + let rel_str = rel.to_string_lossy().to_string(); + let _ = app_handle.emit("fs:file-changed", &rel_str); + } + + if is_structural { + tree_dirty = true; + } + } + + if tree_dirty { + let _ = app_handle.emit("fs:tree-changed", ()); + } + + // Cleanup old debounce entries + seen.retain(|_, t| now.duration_since(*t).as_secs() < 10); + }, + Config::default(), + ) + .map_err(|e| e.to_string())?; + + watcher + .watch(Path::new(&repo_path), RecursiveMode::Recursive) + .map_err(|e| e.to_string())?; + + *guard = Some(watcher); + Ok(()) +} + +#[tauri::command] +pub fn unwatch_repo(app: AppHandle) -> Result<(), String> { + let state = app.state::(); + let mut guard = state.watcher.lock().map_err(|e| e.to_string())?; + *guard = None; + Ok(()) +} diff --git a/src/App.jsx b/src/App.jsx index 3134c5f..e1d10be 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import Layout from './components/Layout' import Sidebar from './components/Sidebar/Sidebar' import Editor from './components/Editor/Editor' @@ -36,6 +36,12 @@ function App() { const [terminalMounted, setTerminalMounted] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [contentRevision, setContentRevision] = useState(0) + + // Refs for accessing current state inside event listeners + const activeFileRef = useRef(null) + const isDirtyRef = useRef(false) + const fileContentRef = useRef('') useEffect(() => { localStorage.setItem('docugit:showSidebar', showSidebar) @@ -92,6 +98,60 @@ function App() { }).catch(() => {}) }, [isDirty]) + // Keep refs in sync with state for use inside event listeners + useEffect(() => { activeFileRef.current = activeFilePath }, [activeFilePath]) + useEffect(() => { isDirtyRef.current = isDirty }, [isDirty]) + useEffect(() => { fileContentRef.current = fileContent }, [fileContent]) + + // Start file watcher when repo changes + useEffect(() => { + if (!repoPath) return + api.watchRepo(repoPath).catch(() => {}) + return () => { api.unwatchRepo().catch(() => {}) } + }, [repoPath]) + + // Listen for file system events from the watcher + useEffect(() => { + if (!repoPath) return + + let unlisten1, unlisten2 + let treeTimer = null + + const setup = async () => { + const { listen } = await import('@tauri-apps/api/event') + + unlisten1 = await listen('fs:file-changed', async (event) => { + const changedPath = event.payload + if (changedPath === activeFileRef.current && !isDirtyRef.current) { + try { + const data = await api.getFile(changedPath) + if (data.content !== fileContentRef.current) { + setFileContent(data.content) + setContentRevision(prev => prev + 1) + } + } catch { + // File may have been deleted + } + } + }) + + unlisten2 = await listen('fs:tree-changed', () => { + // Debounce tree refreshes + if (treeTimer) clearTimeout(treeTimer) + treeTimer = setTimeout(() => refreshFiles(), 500) + }) + } + + setup() + + return () => { + unlisten1?.() + unlisten2?.() + if (treeTimer) clearTimeout(treeTimer) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [repoPath]) + const handleSetRepo = async (path) => { try { setError(null) @@ -219,7 +279,7 @@ function App() { editor={ activeFilePath ? ( invoke('git_unpushed') export const gitPush = () => invoke('git_push') export const gitLog = (limit = 20) => invoke('git_log', { limit }) +// File watcher +export const watchRepo = (repoPath) => invoke('watch_repo', { repoPath }) +export const unwatchRepo = () => invoke('unwatch_repo') + // Terminal export const ptySpawn = () => invoke('pty_spawn') export const ptyWrite = (data) => invoke('pty_write', { data }) From bb9a562952916109628162a2310b85a52964a2b0 Mon Sep 17 00:00:00 2001 From: Alex Ray Date: Thu, 21 May 2026 21:59:48 -0700 Subject: [PATCH 2/3] Add conflict detection banner and in-document search When a file changes on disk while the editor has unsaved changes, an amber banner now alerts the user with options to load the external version or keep their edits. Also adds Cmd+F search with match highlighting, navigation, and replace support using a custom TipTap/ProseMirror extension. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/.brain-state.json | 10 +- docs/sessions/session-01.md | 146 ++++++++++++++++++++ src/App.jsx | 43 ++++-- src/components/Editor/ConflictBanner.jsx | 23 ++++ src/components/Editor/Editor.jsx | 29 +++- src/components/Editor/SearchBar.jsx | 115 ++++++++++++++++ src/components/Editor/SearchExtension.js | 166 +++++++++++++++++++++++ src/index.css | 11 ++ 8 files changed, 527 insertions(+), 16 deletions(-) create mode 100644 docs/sessions/session-01.md create mode 100644 src/components/Editor/ConflictBanner.jsx create mode 100644 src/components/Editor/SearchBar.jsx create mode 100644 src/components/Editor/SearchExtension.js diff --git a/docs/.brain-state.json b/docs/.brain-state.json index 965fd46..7b7c00d 100644 --- a/docs/.brain-state.json +++ b/docs/.brain-state.json @@ -2,13 +2,13 @@ "schema_version": 1, "last_update_brain_run": "2026-05-20T00:00:00-07:00", "last_update_brain_commit": "d1d1cf0e76e3be7a8b0d00c0f80195a5bb7d98f9", - "last_audit_run": "2026-05-20T00:00:00-07:00", + "last_audit_run": "2026-05-21T00:00:00-07:00", "audit_results": { - "grade": "B", - "total_audited": 5, - "fresh": 3, + "grade": "A", + "total_audited": 6, + "fresh": 6, "aging": 0, - "stale": 2, + "stale": 0, "critical": 0 } } diff --git a/docs/sessions/session-01.md b/docs/sessions/session-01.md new file mode 100644 index 0000000..8e63190 --- /dev/null +++ b/docs/sessions/session-01.md @@ -0,0 +1,146 @@ +# Development Log - Session 01 + +**Date**: 2026-05-20 +**Duration**: ~1.5 hours + +--- + +## Current State & How to Resume + +### System State (as of session end) +- Branch: `feature/terminal` — pushed to origin +- PR #2 open against `main`: "Migrate to Tauri desktop app with terminal, file watcher, and table support" +- App builds successfully (`cargo tauri dev`) +- All features functional: file watcher, table support, terminal, git panel + +### To Verify State +```bash +cd /Users/xray/software/docugit +git status +cargo check --manifest-path src-tauri/Cargo.toml +npx vite build +``` + +### To Resume Development +1. Run `cargo tauri dev` to launch the app +2. Check PR #2 for any review feedback +3. Next focus area: design system implementation (prompt is ready at `docs/design-system-prompt.md`) + +### Checklist Status +| Feature | Status | Notes | +|---------|--------|-------| +| File watcher (notify crate) | Done | Watches .md files, debounced, emits Tauri events | +| Auto-refresh editor | Done | Re-reads file on external change if not dirty | +| Auto-refresh file tree | Done | Tree updates on file create/delete | +| Table support (TipTap) | Done | Named exports fix applied after initial blank screen | +| Design system prompt | Done | Comprehensive brief at docs/design-system-prompt.md | +| Brain-implant init | Done | Skills installed, first audit + update-brain run complete | +| PR created | Done | PR #2 on GitHub | + +--- + +## Executive Summary + +This session added three features to DocuGit: **file watching** (Rust `notify` crate watches the repo and auto-refreshes the editor/tree via Tauri events), **markdown table support** (TipTap table extensions), and a **design system prompt** for a future UI redesign. + +We also initialized brain-implant for knowledge management and ran the first documentation audit (grade B — 3 fresh, 2 stale, 0 critical). The stale docs turned out to be functionally accurate despite the Flask-to-Tauri migration; only minor corrections were needed. + +A bug was introduced and fixed mid-session: the TipTap table extensions use named exports (`{ Table }`) not default exports (`Table`), which caused a blank white screen. Fixed by switching import syntax. + +--- + +## Detailed Progress + +### File Watching +- Created `src-tauri/src/watcher.rs` — Rust module using `notify 6.1` crate +- `WatcherState` holds an optional `RecommendedWatcher` in a `Mutex` +- `watch_repo` command starts recursive watching with debouncing (1s per path) +- Filters to `.md` files only, emits `fs:file-changed` (relative path) and `fs:tree-changed` (create/delete only) +- Frontend uses refs to access current state inside event listeners (React closure stale-state pattern) +- `contentRevision` counter bumped on external changes to force TipTap editor remount via key prop +- Self-saves naturally ignored: content comparison prevents redundant updates + +#### Technical Decisions +**Notify crate over Tauri fs-watch plugin** +- Context: Needed file watching for auto-refresh +- Options: `tauri-plugin-fs-watch`, `notify` crate directly, polling with `setInterval` +- Decision: `notify` crate directly +- Rationale: Lighter dependency, full control over debouncing and event filtering, no plugin compatibility concerns + +**Editor remount via key prop vs setContent** +- Context: TipTap ignores content prop changes after initialization +- Options: `editor.commands.setContent()` (preserves instance), key change (remounts) +- Decision: Key change with `contentRevision` counter +- Rationale: Simpler, and cursor position loss is acceptable for external changes + +### Table Support +- Installed `@tiptap/extension-table`, `table-row`, `table-header`, `table-cell` at version 3.22.5 (matching existing TipTap) +- Added table/cell CSS styles to `src/index.css` +- Bug: default imports caused blank screen — TipTap table extensions export named, not default + +### Design System Prompt +- Created comprehensive brief at `docs/design-system-prompt.md` +- Covers: problem statement, audience, full UI inventory, current visual style, known issues, specific deliverables (foundations + 14 components + layout system), design direction + +### Documentation +- Ran `/audit-docs` — grade B, identified getting-started.md and welcome.md as stale +- Ran `/update-brain` — fixed history.md duplicates, added table example to welcome.md, minor getting-started.md correction +- No Claude memories promoted (project overview already in docs, user prefs stay in memory) + +--- + +## Files Created/Modified + +### New Files +- `src-tauri/src/watcher.rs` — File watcher module (notify crate, Tauri event emission) +- `docs/design-system-prompt.md` — Design system brief for LLM design tool +- `docs/history.md` — Project history (auto-generated, then cleaned up) +- `docs/.brain-state.json` — Brain-implant state tracking +- `CLAUDE.md` — Brain-implant configuration +- `brain.yaml` — Brain-implant config +- `docs/backlog/`, `docs/decisions/`, `docs/reports/`, `docs/sessions/`, `docs/sprints/` — Brain-implant scaffolding + +### Modified Files +- `src-tauri/Cargo.toml` — Added `notify = "6.1"` +- `src-tauri/src/lib.rs` — Registered watcher module, state, and commands +- `src/App.jsx` — Added watcher lifecycle, event listeners, contentRevision state, refs +- `src/components/Editor/Editor.jsx` — Added table extensions (named imports) +- `src/index.css` — Added table styles +- `src/tauri-api.js` — Added `watchRepo`/`unwatchRepo` functions +- `package.json` — Added TipTap table extension dependencies +- `docs/getting-started.md` — Fixed Git toggle button location +- `docs/welcome.md` — Added table example section + +--- + +## Known Issues & Technical Debt + +### Blank screen on import errors +- **Status**: Resolved +- **Description**: Default imports of TipTap extensions that only export named caused the entire app to white-screen with no visible error +- **Lesson**: Always check export style when adding new TipTap extensions + +### No dark mode +- **Status**: Deferred +- **Description**: Terminal is dark but all other panels are light-only +- **Impact**: Design system work will address this + +### File watcher debounce edge cases +- **Status**: Acceptable +- **Description**: If two different users edit the same file within the 1s debounce window, the second change may be missed +- **Impact**: Negligible for single-user desktop app + +--- + +## Next Steps + +### Immediate (Next Session) +1. Review PR #2 feedback and merge to main +2. Use `docs/design-system-prompt.md` with a design tool to generate the design system +3. Begin implementing the design system (colors, typography, component styles) + +### Upcoming +- Dark mode support +- Editor toolbar icons (currently text-only) +- Accessibility improvements (ARIA labels, focus management) +- Consider resizable panels (drag-to-resize sidebar/git panel) diff --git a/src/App.jsx b/src/App.jsx index e1d10be..7fbdcf0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -37,6 +37,7 @@ function App() { const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [contentRevision, setContentRevision] = useState(0) + const [externalContent, setExternalContent] = useState(null) // Refs for accessing current state inside event listeners const activeFileRef = useRef(null) @@ -122,16 +123,20 @@ function App() { unlisten1 = await listen('fs:file-changed', async (event) => { const changedPath = event.payload - if (changedPath === activeFileRef.current && !isDirtyRef.current) { - try { - const data = await api.getFile(changedPath) - if (data.content !== fileContentRef.current) { - setFileContent(data.content) - setContentRevision(prev => prev + 1) - } - } catch { - // File may have been deleted + if (changedPath !== activeFileRef.current) return + try { + const data = await api.getFile(changedPath) + if (data.content === fileContentRef.current) return // no real change + if (!isDirtyRef.current) { + // Editor clean — auto-reload + setFileContent(data.content) + setContentRevision(prev => prev + 1) + } else { + // Editor dirty — conflict! Store the external version + setExternalContent(data.content) } + } catch { + // File may have been deleted } }) @@ -162,6 +167,7 @@ function App() { setActiveFilePath(null) setFileContent('') setIsDirty(false) + setExternalContent(null) } else { setError(data.error || 'Invalid repo path') } @@ -177,6 +183,7 @@ function App() { setActiveFilePath(null) setFileContent('') setIsDirty(false) + setExternalContent(null) setGitStatus(null) } @@ -191,6 +198,7 @@ function App() { setActiveFilePath(filepath) setFileContent(data.content) setIsDirty(false) + setExternalContent(null) localStorage.setItem('docugit:lastFile', filepath) } catch (err) { setError(err.message) @@ -212,6 +220,19 @@ function App() { } }, [activeFilePath, refreshGitStatus]) + const handleConflictKeepMine = () => { + setExternalContent(null) + } + + const handleConflictLoadTheirs = () => { + if (externalContent !== null) { + setFileContent(externalContent) + setContentRevision(prev => prev + 1) + setIsDirty(false) + setExternalContent(null) + } + } + const handleOpenDoc = async (filePath) => { if (isDirty && !window.confirm('You have unsaved changes. Discard them?')) { return @@ -230,6 +251,7 @@ function App() { setActiveFilePath(data.relative_path) setFileContent(data.content) setIsDirty(false) + setExternalContent(null) localStorage.setItem('docugit:lastFile', data.relative_path) } catch (err) { setError(err.message) @@ -285,6 +307,9 @@ function App() { onDirtyChange={setIsDirty} onSave={handleSave} filePath={activeFilePath} + hasConflict={externalContent !== null} + onConflictKeepMine={handleConflictKeepMine} + onConflictLoadTheirs={handleConflictLoadTheirs} /> ) : ( diff --git a/src/components/Editor/ConflictBanner.jsx b/src/components/Editor/ConflictBanner.jsx new file mode 100644 index 0000000..f574dbb --- /dev/null +++ b/src/components/Editor/ConflictBanner.jsx @@ -0,0 +1,23 @@ +export default function ConflictBanner({ onKeepMine, onLoadTheirs }) { + return ( +
+ + This file was modified externally while you have unsaved changes. + +
+ + +
+
+ ) +} diff --git a/src/components/Editor/Editor.jsx b/src/components/Editor/Editor.jsx index cd13d93..4d07150 100644 --- a/src/components/Editor/Editor.jsx +++ b/src/components/Editor/Editor.jsx @@ -1,4 +1,4 @@ -import { useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback } from 'react' import { useEditor, EditorContent } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import { Markdown } from '@tiptap/markdown' @@ -7,8 +7,13 @@ import { TableRow } from '@tiptap/extension-table-row' import { TableHeader } from '@tiptap/extension-table-header' import { TableCell } from '@tiptap/extension-table-cell' import Toolbar from './Toolbar' +import ConflictBanner from './ConflictBanner' +import { SearchExtension } from './SearchExtension' +import SearchBar from './SearchBar' + +export default function Editor({ content, isDirty, onDirtyChange, onSave, filePath, hasConflict, onConflictKeepMine, onConflictLoadTheirs }) { + const [showSearch, setShowSearch] = useState(false) -export default function Editor({ content, isDirty, onDirtyChange, onSave, filePath }) { const editor = useEditor({ extensions: [ StarterKit, @@ -17,6 +22,7 @@ export default function Editor({ content, isDirty, onDirtyChange, onSave, filePa TableHeader, TableCell, Markdown, + SearchExtension, ], content: content, contentType: 'markdown', @@ -38,6 +44,10 @@ export default function Editor({ content, isDirty, onDirtyChange, onSave, filePa e.preventDefault() handleSave() } + if ((e.metaKey || e.ctrlKey) && e.key === 'f') { + e.preventDefault() + setShowSearch(true) + } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) @@ -47,7 +57,22 @@ export default function Editor({ content, isDirty, onDirtyChange, onSave, filePa return (
+ {hasConflict && ( + + )} + {showSearch && ( + { + setShowSearch(false) + editor.commands.focus() + }} + /> + )}
diff --git a/src/components/Editor/SearchBar.jsx b/src/components/Editor/SearchBar.jsx new file mode 100644 index 0000000..a34cc80 --- /dev/null +++ b/src/components/Editor/SearchBar.jsx @@ -0,0 +1,115 @@ +import { useState, useEffect, useRef } from 'react' + +export default function SearchBar({ editor, onClose }) { + const [searchTerm, setSearchTerm] = useState('') + const [replaceTerm, setReplaceTerm] = useState('') + const [showReplace, setShowReplace] = useState(false) + const inputRef = useRef(null) + + useEffect(() => { + inputRef.current?.focus() + // Select any existing text in search input + inputRef.current?.select() + }, []) + + useEffect(() => { + editor.commands.setSearchTerm(searchTerm) + }, [searchTerm, editor]) + + const handleClose = () => { + editor.commands.clearSearch() + onClose() + } + + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + e.preventDefault() + handleClose() + } else if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + editor.commands.goToNextMatch() + } else if (e.key === 'Enter' && e.shiftKey) { + e.preventDefault() + editor.commands.goToPrevMatch() + } + } + + const { results, activeIndex } = editor.storage.search + const matchCount = results.length + const currentMatch = matchCount > 0 ? activeIndex + 1 : 0 + + return ( +
+
+ + setSearchTerm(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search..." + className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400 bg-white" + /> + + {searchTerm ? `${currentMatch} of ${matchCount}` : ''} + + + + +
+ {showReplace && ( +
+ setReplaceTerm(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Escape') handleClose() + }} + placeholder="Replace..." + className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400 bg-white" + /> + + +
+ )} +
+ ) +} diff --git a/src/components/Editor/SearchExtension.js b/src/components/Editor/SearchExtension.js new file mode 100644 index 0000000..3ea5c00 --- /dev/null +++ b/src/components/Editor/SearchExtension.js @@ -0,0 +1,166 @@ +import { Extension } from '@tiptap/react' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' + +const searchPluginKey = new PluginKey('search') + +function findMatches(doc, searchTerm) { + if (!searchTerm) return [] + const results = [] + const term = searchTerm.toLowerCase() + doc.descendants((node, pos) => { + if (!node.isText) return + const text = node.text.toLowerCase() + let index = 0 + while ((index = text.indexOf(term, index)) !== -1) { + results.push({ from: pos + index, to: pos + index + searchTerm.length }) + index += 1 + } + }) + return results +} + +export { searchPluginKey } + +export const SearchExtension = Extension.create({ + name: 'search', + + addStorage() { + return { + searchTerm: '', + activeIndex: 0, + results: [], + } + }, + + addCommands() { + return { + setSearchTerm: (term) => ({ editor }) => { + editor.storage.search.searchTerm = term + const results = findMatches(editor.state.doc, term) + editor.storage.search.results = results + // Reset active index + editor.storage.search.activeIndex = results.length > 0 ? 0 : -1 + // Force plugin to re-decorate + const { tr } = editor.state + tr.setMeta(searchPluginKey, { searchTerm: term, activeIndex: editor.storage.search.activeIndex }) + editor.view.dispatch(tr) + return true + }, + goToNextMatch: () => ({ editor }) => { + const { results, activeIndex } = editor.storage.search + if (results.length === 0) return false + const next = (activeIndex + 1) % results.length + editor.storage.search.activeIndex = next + const { tr } = editor.state + tr.setMeta(searchPluginKey, { activeIndex: next }) + editor.view.dispatch(tr) + // Scroll to match + const match = results[next] + if (match) { + editor.commands.setTextSelection(match.from) + editor.commands.scrollIntoView() + } + return true + }, + goToPrevMatch: () => ({ editor }) => { + const { results, activeIndex } = editor.storage.search + if (results.length === 0) return false + const prev = (activeIndex - 1 + results.length) % results.length + editor.storage.search.activeIndex = prev + const { tr } = editor.state + tr.setMeta(searchPluginKey, { activeIndex: prev }) + editor.view.dispatch(tr) + const match = results[prev] + if (match) { + editor.commands.setTextSelection(match.from) + editor.commands.scrollIntoView() + } + return true + }, + replaceCurrentMatch: (replacement) => ({ editor }) => { + const { results, activeIndex } = editor.storage.search + if (results.length === 0 || activeIndex < 0) return false + const match = results[activeIndex] + editor.chain() + .setTextSelection({ from: match.from, to: match.to }) + .insertContent(replacement) + .run() + // Re-run search after replace + editor.commands.setSearchTerm(editor.storage.search.searchTerm) + return true + }, + replaceAll: (replacement) => ({ editor }) => { + const { results, searchTerm } = editor.storage.search + if (results.length === 0) return false + // Replace from end to start to preserve positions + const chain = editor.chain() + for (let i = results.length - 1; i >= 0; i--) { + chain.setTextSelection({ from: results[i].from, to: results[i].to }) + chain.insertContent(replacement) + } + chain.run() + editor.commands.setSearchTerm(searchTerm) + return true + }, + clearSearch: () => ({ editor }) => { + editor.storage.search.searchTerm = '' + editor.storage.search.results = [] + editor.storage.search.activeIndex = -1 + const { tr } = editor.state + tr.setMeta(searchPluginKey, { searchTerm: '', activeIndex: -1 }) + editor.view.dispatch(tr) + return true + }, + } + }, + + addProseMirrorPlugins() { + const extension = this + + return [ + new Plugin({ + key: searchPluginKey, + state: { + init() { + return DecorationSet.empty + }, + apply(tr, oldDecos) { + const meta = tr.getMeta(searchPluginKey) + if (meta !== undefined) { + const { searchTerm } = extension.storage + if (!searchTerm) return DecorationSet.empty + const results = findMatches(tr.doc, searchTerm) + extension.storage.results = results + const activeIndex = extension.storage.activeIndex + const decorations = results.map((match, i) => { + const className = i === activeIndex ? 'search-match search-match-active' : 'search-match' + return Decoration.inline(match.from, match.to, { class: className }) + }) + return DecorationSet.create(tr.doc, decorations) + } + // If the document changed, rebuild decorations + if (tr.docChanged && extension.storage.searchTerm) { + const results = findMatches(tr.doc, extension.storage.searchTerm) + extension.storage.results = results + if (extension.storage.activeIndex >= results.length) { + extension.storage.activeIndex = results.length > 0 ? 0 : -1 + } + const decorations = results.map((match, i) => { + const className = i === extension.storage.activeIndex ? 'search-match search-match-active' : 'search-match' + return Decoration.inline(match.from, match.to, { class: className }) + }) + return DecorationSet.create(tr.doc, decorations) + } + return oldDecos.map(tr.mapping, tr.doc) + }, + }, + props: { + decorations(state) { + return this.getState(state) + }, + }, + }), + ] + }, +}) diff --git a/src/index.css b/src/index.css index 9db0a3f..8960f77 100644 --- a/src/index.css +++ b/src/index.css @@ -79,3 +79,14 @@ html, body, #root { .tiptap .selectedCell { background: #dbeafe; } + +/* Search match highlighting */ +.search-match { + background: #fef08a; + border-radius: 2px; +} +.search-match-active { + background: #f97316; + color: white; + border-radius: 2px; +} From 36406f9ea6b434fb444e63f4022c591e25d13c3a Mon Sep 17 00:00:00 2001 From: Alex Ray Date: Thu, 21 May 2026 22:02:56 -0700 Subject: [PATCH 3/3] Add decision records and documentation audit report - Record Tauri migration decision (2026-05-15) with context, reasoning, and consequences - Record file watching approach decision (2026-05-20) with alternatives considered - Save audit report (grade A, 6/6 fresh, 0 stale) - Update brain state Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/decisions/2026-05-15-migrate-to-tauri.md | 32 +++++++++ .../2026-05-20-file-watching-with-notify.md | 31 ++++++++ docs/reports/audit-2026-05-21.md | 71 +++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 docs/decisions/2026-05-15-migrate-to-tauri.md create mode 100644 docs/decisions/2026-05-20-file-watching-with-notify.md create mode 100644 docs/reports/audit-2026-05-21.md diff --git a/docs/decisions/2026-05-15-migrate-to-tauri.md b/docs/decisions/2026-05-15-migrate-to-tauri.md new file mode 100644 index 0000000..3f49fb1 --- /dev/null +++ b/docs/decisions/2026-05-15-migrate-to-tauri.md @@ -0,0 +1,32 @@ +# Migrate from Flask to Tauri Desktop App + +**Date**: 2026-05-15 +**Status**: accepted + +## Context + +DocuGit started as a Flask+Vite two-process web app. The user runs a Python backend and a Vite dev server, then opens a browser tab. This worked for prototyping but had friction: two processes to manage, no native OS integration, no access to PTY for an embedded terminal, and the app felt like a web page rather than a tool. + +## Decision + +Replace Flask+Vite with a single Tauri 2 macOS desktop app. The React frontend runs in a native WebView, and a Rust backend handles file I/O, git operations, and config via IPC commands. + +## Reasoning + +- **Single binary**: One process to launch, no port management, no browser tab +- **Native feel**: macOS window chrome, document-edited indicator, native file dialogs +- **PTY support**: Rust's `portable-pty` enables an embedded terminal — not feasible from Flask +- **Performance**: Rust file I/O and git operations (via subprocess) are fast with no HTTP overhead +- **Distribution**: Tauri builds `.dmg` and `.app` bundles for macOS +- **Alternatives considered**: + - **Electron**: Heavier runtime (~100MB+ vs ~5MB for Tauri), less native feel + - **Keep Flask**: No path to PTY, terminal, or native integration + - **Wails (Go)**: Smaller ecosystem, less mature than Tauri 2 + +## Consequences + +- **macOS only for now** — Tauri supports Windows/Linux but we haven't tested or configured those targets +- **Rust required** — contributors need the Rust toolchain, which is a higher bar than Python +- **No web access** — the app is local-only (was also true with Flask, but now it's structural) +- **GitPython replaced** — git operations are now via CLI subprocess, which is actually more transparent and debuggable +- **Frontend unchanged** — React + TipTap + Tailwind carried over with only import path changes (`fetch` → `invoke`) diff --git a/docs/decisions/2026-05-20-file-watching-with-notify.md b/docs/decisions/2026-05-20-file-watching-with-notify.md new file mode 100644 index 0000000..bb98dd1 --- /dev/null +++ b/docs/decisions/2026-05-20-file-watching-with-notify.md @@ -0,0 +1,31 @@ +# File Watching with Rust notify Crate + +**Date**: 2026-05-20 +**Status**: accepted + +## Context + +DocuGit had no mechanism to detect external file changes. If a user edited a `.md` file outside the app (e.g., via an LLM tool, terminal, or another editor), the UI wouldn't reflect the change until the user manually re-opened the file. For a tool designed around LLM-assisted doc workflows, this was a significant gap. + +## Decision + +Use the Rust `notify` crate (v6.1) to watch the repo directory recursively for `.md` file changes. The watcher emits Tauri events (`fs:file-changed`, `fs:tree-changed`) that the React frontend listens for and reacts to. + +## Reasoning + +- **Event-driven**: macOS FSEvents via `notify` is efficient — no polling, no wasted CPU +- **Filtered and debounced**: Only `.md` files trigger events, with 1-second per-path debouncing to avoid rapid-fire updates during saves +- **Two event types**: `fs:file-changed` (content modification) refreshes the active editor; `fs:tree-changed` (create/delete) refreshes the sidebar file tree +- **Self-save handling**: When the user saves via the app, the watcher fires but the frontend compares content and skips redundant updates — no flicker or cursor loss +- **Alternatives considered**: + - **`tauri-plugin-fs-watch`**: Extra plugin dependency, less control over filtering and debouncing + - **`setInterval` polling**: Simple but wasteful, introduces latency, harder to get right + - **No watching**: Viable but poor UX for the LLM-assisted workflow where files change externally + +## Consequences + +- **Immediate refresh**: External edits appear in the editor within ~1 second +- **Dirty files protected**: If the user has unsaved changes, external updates are ignored (no data loss) +- **Editor remounts on external change**: Uses a `contentRevision` key bump to force TipTap to re-initialize — cursor position is lost, but this is acceptable for external changes +- **Watcher lifecycle tied to repo**: Starts when a repo is selected, stops on cleanup or repo switch +- **Resource cost**: One FSEvents stream per repo — negligible on macOS diff --git a/docs/reports/audit-2026-05-21.md b/docs/reports/audit-2026-05-21.md new file mode 100644 index 0000000..a2e5a46 --- /dev/null +++ b/docs/reports/audit-2026-05-21.md @@ -0,0 +1,71 @@ +# Documentation Health Audit - 2026-05-21 + +## Summary +- **Total docs audited**: 6 +- **Fresh** (< 0.5): 6 +- **Aging** (0.5-0.99): 0 +- **Stale** (1.0-1.99): 0 +- **Critical** (2.0+): 0 + +## Overall Grade: A + +> 100% fresh, 0 critical + +--- + +## Fresh Docs (score < 0.5) + +| Doc | Last Updated | Score | Notes | +|-----|-------------|-------|-------| +| `docs/design-system-prompt.md` | May 20 | 0.0 | References code; updated same day | +| `docs/history.md` | May 20 | 0.0 | References code; updated same day | +| `docs/getting-started.md` | May 20 | 0.01 | Standalone; 1 day old | +| `docs/welcome.md` | May 20 | 0.01 | Standalone; 1 day old | +| `docs/backlog/items.md` | May 20 | 0.01 | Empty scaffold | +| `CLAUDE.md` | May 20 | 0.0 | Config; current | + +## Skipped +- `docs/sessions/session-01.md` — session log +- `docs/backlog/questions.md` — empty template +- `docs/decisions/_template.md` — template +- `docs/sprints/_sprint-template/TEMPLATE.md` — template + +--- + +## Coverage Gaps + +| Directory | Files | Docs Referencing | +|-----------|-------|-----------------| +| `src/components/` | 16 | 1 (`design-system-prompt.md`) | +| `src-tauri/src/` | 7 | 1 (`design-system-prompt.md`) | + +References are high-level (architecture overview for designers), not developer-facing API docs. + +## Decision Health +- **Total decision records**: 0 at time of audit (2 created after audit) +- **Recommendation**: Record Tauri migration and file watcher decisions + +## History Coverage +- `history.md` covers through: **2026-05** (current month) +- Status: Current + +--- + +## Recommended Actions + +1. Record architectural decisions in `docs/decisions/` — done post-audit +2. Consider developer-facing architecture doc for `src-tauri/src/` command API +3. No stale or critical docs — documentation is healthy + +--- + +## Comparison to Previous Audit + +| Metric | 2026-05-20 | 2026-05-21 | Change | +|--------|-----------|-----------|--------| +| Grade | B | A | +1 | +| Fresh | 3 | 6 | +3 | +| Stale | 2 | 0 | -2 | +| Critical | 0 | 0 | — | + +Improvement driven by `/update-brain` run on 2026-05-20 which fixed stale docs.