+
+ {/each}
+
+ {/each}
+
+
diff --git a/packages/ui/src/command-palette/index.ts b/packages/ui/src/command-palette/index.ts
new file mode 100644
index 0000000000..b9b7b4e2fd
--- /dev/null
+++ b/packages/ui/src/command-palette/index.ts
@@ -0,0 +1,79 @@
+import type { Component } from 'svelte';
+
+export { default as CommandPalette } from './command-palette.svelte';
+
+/**
+ * A single item that can appear in the command palette.
+ *
+ * This is the shared contract between command sources (workspace actions,
+ * static registries, dynamic search results) and the `CommandPalette` component.
+ * Anything that produces this shape can feed the palette—no adapters, no wrappers.
+ *
+ * Only `id`, `label`, and `onSelect` are required. Every other field is a
+ * progressive enhancement—omit what you don't need and the component adapts:
+ * no icon → text-only row, no description → single-line label, no keywords →
+ * search matches against the label alone.
+ *
+ * @example
+ * ```typescript
+ * const items: CommandPaletteItem[] = [
+ * {
+ * id: 'dedup',
+ * label: 'Remove Duplicates',
+ * description: 'Close duplicate tabs with the same URL',
+ * icon: CopyMinusIcon,
+ * keywords: ['dedup', 'duplicate', 'clean'],
+ * group: 'Quick Actions',
+ * destructive: true,
+ * onSelect: () => removeDuplicates(),
+ * },
+ * ];
+ * ```
+ */
+export type CommandPaletteItem = {
+ /** Stable identifier used as the Svelte `{#each}` key. Must be unique within the items array. */
+ id: string;
+ /** Primary display text shown for this command. Also used as the base search target. */
+ label: string;
+ /**
+ * Secondary text rendered below the label in a smaller, muted style.
+ *
+ * When the item is `destructive`, this doubles as the confirmation dialog
+ * description (falls back to "Are you sure?" when omitted).
+ */
+ description?: string;
+ /**
+ * Svelte component rendered as a 16×16 icon to the left of the label.
+ *
+ * Typically a Lucide icon import. Omit when icons aren't available
+ * (e.g. programmatically generated items).
+ */
+ icon?: Component;
+ /**
+ * Extra search tokens beyond the label. The palette builds its search
+ * value as `[label, ...keywords].join(' ')`, so these let users find
+ * the command via synonyms or abbreviations without cluttering the label.
+ *
+ * @example `['dedup', 'duplicate', 'clean']`
+ */
+ keywords?: string[];
+ /**
+ * Heading under which this item is grouped in the palette.
+ *
+ * Items sharing the same `group` string are rendered together under a
+ * `Command.Group` heading. Items without a group render ungrouped at
+ * the top.
+ */
+ group?: string;
+ /**
+ * When `true`, selecting this item opens a confirmation dialog before
+ * executing `onSelect`. The dialog uses `label` as its title and
+ * `description` as its body text.
+ *
+ * Use for irreversible or high-impact operations (bulk close, delete, etc.).
+ * Defaults to `false` when omitted.
+ */
+ destructive?: boolean;
+ /** Callback invoked when the user selects this item (after confirmation if `destructive`). */
+ onSelect: () => void | Promise;
+};
diff --git a/specs/20260314T104746-inline-file-creation.md b/specs/20260314T104746-inline-file-creation.md
new file mode 100644
index 0000000000..254ebe2f89
--- /dev/null
+++ b/specs/20260314T104746-inline-file-creation.md
@@ -0,0 +1,103 @@
+# Inline File Creation & Rename UX
+
+Replace modal dialogs with inline tree inputs for file/folder creation and rename—matching VS Code, Obsidian, and JetBrains patterns.
+
+## Current State
+
+- **CreateDialog.svelte** — Modal dialog pops up center-screen to name new files/folders
+- **RenameDialog.svelte** — Same modal approach for rename
+- **FileTreeItem.svelte** — Already has context menu (right-click) with New File/Folder/Rename/Delete
+- **Toolbar.svelte** — Buttons trigger the modal dialogs
+- **FileTree.svelte** — Keyboard nav (arrows, Home/End, Enter/Space) works well
+
+## Problem
+
+Modal dialogs for file creation are not idiomatic. Every major file explorer (VS Code, Obsidian, JetBrains, Sublime) uses inline inputs that appear directly in the tree at the insertion point.
+
+## Design (VS Code Pattern)
+
+### Inline Creation
+1. User clicks "New File" button (toolbar), or right-clicks folder → "New File" (context menu), or presses keyboard shortcut
+2. If a folder is selected/focused, auto-expand it and insert an inline input as the **first child** of that folder
+3. If a file is selected, insert the inline input as a **sibling** (in the same parent folder)
+4. If nothing is selected, insert at root level
+5. **Enter** confirms → creates file/folder with that name
+6. **Escape** cancels → removes the inline input
+7. Clicking outside (blur) also confirms (VS Code behavior)
+
+### Inline Rename
+1. User right-clicks → "Rename", or presses F2 (keyboard shortcut)
+2. The name text is replaced with an inline input pre-filled with the current name
+3. Text is selected (so typing replaces it)
+4. Same Enter/Escape/blur behavior as creation
+
+### Keyboard Shortcuts (scoped to tree panel)
+- **N** — New file (in focused folder or at root)
+- **Shift+N** — New folder
+- **F2** — Rename focused item
+- **Delete** / **Backspace** — Delete focused item (with confirmation dialog—keep DeleteConfirmation.svelte)
+
+### Context Menu
+Already exists. Update actions to trigger inline inputs instead of dialogs.
+
+## Changes
+
+### 1. `fs-state.svelte.ts` — Add inline editing state
+- `inlineCreate: { parentId: FileId | null; type: 'file' | 'folder' } | null`
+- `renamingId: FileId | null`
+- Actions: `startCreate(parentId, type)`, `confirmCreate(name)`, `cancelCreate()`, `startRename(id)`, `confirmRename(name)`, `cancelRename()`
+
+### 2. New: `InlineNameInput.svelte`
+- Tiny input that fits inline in the tree item row
+- Handles Enter/Escape/blur
+- Calls confirm/cancel actions
+- Auto-focuses on mount
+
+### 3. `FileTree.svelte` — Render inline create input & keyboard shortcuts
+- After the children of a folder (or at root), render InlineNameInput when inlineCreate matches that location
+- Add N, Shift+N, F2, Delete keyboard handlers
+
+### 4. `FileTreeItem.svelte` — Inline rename support
+- When `renamingId === id`, replace name span with InlineNameInput (pre-filled)
+- Context menu actions call new state actions instead of opening dialogs
+
+### 5. `Toolbar.svelte` — Remove dialog state, call inline actions
+- "New File" → `fsState.actions.startCreate(parentId, 'file')`
+- "New Folder" → `fsState.actions.startCreate(parentId, 'folder')`
+- Remove CreateDialog and RenameDialog imports/instances
+
+### 6. Delete `CreateDialog.svelte` and `RenameDialog.svelte`
+
+## Todo
+
+- [x] Add inline editing state and actions to `fs-state.svelte.ts`
+- [x] Create `InlineNameInput.svelte` component
+- [x] Update `FileTree.svelte` with inline create rendering + keyboard shortcuts
+- [x] Update `FileTreeItem.svelte` with inline rename + update context menu actions
+- [x] Update `Toolbar.svelte` to use inline actions instead of dialogs
+- [x] Delete `CreateDialog.svelte` and `RenameDialog.svelte`
+- [x] Verify everything works end-to-end (all diagnostics clean)
+
+## Review
+
+### What changed
+
+**Deleted** (2 files):
+- `CreateDialog.svelte` — Modal dialog for file/folder creation
+- `RenameDialog.svelte` — Modal dialog for renaming
+
+**Created** (1 file):
+- `InlineNameInput.svelte` — ~68 lines. Small input component with Enter/Escape/blur handling, auto-focus, filename stem selection.
+
+**Modified** (4 files):
+- `fs-state.svelte.ts` — Added `inlineCreate` and `renamingId` state + 6 new actions (`startCreate`, `confirmCreate`, `cancelCreate`, `startRename`, `confirmRename`, `cancelRename`). All centralized, no duplication.
+- `FileTree.svelte` — Renders inline create input at root level. Added keyboard shortcuts: N (new file), Shift+N (new folder), F2 (rename), Delete/Backspace (delete). Suppresses tree nav during inline editing.
+- `FileTreeItem.svelte` — Renders inline rename (replacing name text) and inline create (inside folder children). Context menu now triggers inline actions with keyboard shortcut hints. Removed CreateDialog/RenameDialog imports.
+- `Toolbar.svelte` — Buttons now call `fsState.actions.startCreate/startRename` directly. Removed all dialog state and imports.
+
+### Net result
+- **2 components deleted**, 1 created → net -1 component
+- **Duplicated dialog state eliminated** (was in both Toolbar and every FileTreeItem)
+- **N×3 hidden dialog instances removed** (every tree item was mounting CreateDialog + RenameDialog + DeleteConfirmation; now only DeleteConfirmation remains per-item)
+- **Centralized editing state** in fs-state singleton — one source of truth
+- **DeleteConfirmation stays** as a modal (correct—destructive actions need explicit confirmation)
diff --git a/specs/20260314T170000-surgical-sqlite-index-updates.md b/specs/20260314T170000-surgical-sqlite-index-updates.md
new file mode 100644
index 0000000000..3cbb02f34c
--- /dev/null
+++ b/specs/20260314T170000-surgical-sqlite-index-updates.md
@@ -0,0 +1,190 @@
+# Surgical SQLite Index Updates
+
+**Date**: 2026-03-14
+**Status**: Implemented
+**Author**: AI-assisted
+
+## Overview
+
+Replace the full nuke-and-rebuild strategy in the SQLite index extension with surgical per-row updates. When a file is edited, only that file's row in SQLite + FTS is updated—not the entire database.
+
+## Motivation
+
+### Current State
+
+`packages/filesystem/src/extensions/sqlite-index/index.ts` rebuilds the entire in-memory SQLite database on every Yjs table mutation:
+
+```typescript
+// Line 182 — observe fires on ANY table change
+unobserve = filesTable.observe(() => scheduleSync());
+
+// scheduleSync debounces 100ms, then calls rebuild()
+
+async function rebuild(): Promise {
+ const rows = filesTable.getAllValid(); // Read ALL rows
+ const paths = computePaths(rows); // Compute ALL paths
+ for (const row of rows) { // Read ALL content docs
+ const handle = await contentDocs.open(row.id);
+ const text = handle.read();
+ }
+ // DELETE everything, INSERT everything
+ await client.batch([
+ 'DELETE FROM files_fts',
+ 'DELETE FROM files',
+ ...insertStatements
+ ], 'write');
+}
+```
+
+This creates problems:
+
+1. **O(N) content reads on every mutation**: Editing one file triggers `contentDocs.open()` for every file in the workspace. Content reads are async and sequential—this is the bottleneck.
+2. **Wasted work**: A single rename re-reads all content, recomputes all paths, and rewrites all rows.
+3. **Missed mutations under load**: The `rebuilding` guard flag silently drops a rebuild if one is already in progress. If a rebuild takes >100ms (the debounce window), a mutation can be lost.
+
+### Desired State
+
+The observer receives `changedIds: Set`. For each changed ID:
+- Deleted row → `DELETE` from `files` + `files_fts`
+- Added/updated row → read only that row's content, compute only that row's path, upsert only that row
+
+Full rebuild only happens on initial load and manual recovery.
+
+## Design Decisions
+
+| Decision | Choice | Rationale |
+|---|---|---|
+| Keep debouncing | Yes, same 100ms | Coalesces rapid edits (typing) into one batch of surgical updates |
+| Path cascade on folder rename | Query SQLite for descendants | Folder renames are rare; querying `WHERE path LIKE '/old/%'` is fast on in-memory SQLite |
+| Keep full `rebuild()` | Yes, for init + recovery | No SQLite state on page load—need a full build. Exposed `rebuild()` stays for corruption recovery |
+| Path computation for single row | Walk `parentId` chain via `filesTable.get()` | Reuses existing Yjs reads, no need for a separate index |
+
+## Architecture
+
+```
+CURRENT FLOW (every mutation):
+────────────────────────────────
+filesTable.observe() → debounce 100ms → rebuild()
+ → getAllValid() (N rows)
+ → computePaths() (N walks)
+ → contentDocs.open() (N async reads)
+ → DELETE all + INSERT all (2N+2 statements)
+
+NEW FLOW (per mutation):
+────────────────────────────────
+filesTable.observe(changedIds) → debounce 100ms → syncRows(changedIds)
+ → for each changedId:
+ → filesTable.get(id) (1 read)
+ → computePathForRow() (1 walk)
+ → contentDocs.open(id) (1 async read, files only)
+ → DELETE + INSERT row (4 statements per row)
+ → if folder renamed:
+ → query descendants (1 SELECT)
+ → recompute descendant paths
+ → batch upsert descendants
+
+INITIAL LOAD (unchanged):
+────────────────────────────────
+rebuild() — same as current full nuke-and-rebuild
+```
+
+## Implementation Plan
+
+### Phase 1: Add `syncRows()` and single-row path computation
+
+- [x] **1.1** Add `computePathForRow(id, filesTable)` function that walks the `parentId` chain using `filesTable.get()` calls (not `getAllValid()`)
+- [x] **1.2** Add `syncRows(changedIds: Set)` async function that handles add/update/delete per row
+- [x] **1.3** Change the observer from `filesTable.observe(() => scheduleSync())` to `filesTable.observe((changedIds) => scheduleSync(changedIds))` — pass changed IDs through the debounce
+
+### Phase 2: Handle folder rename cascading
+
+- [x] **2.1** In `syncRows`, detect when a changed row is a folder whose `path` in SQLite differs from its newly computed path
+- [x] **2.2** Query SQLite for descendants: `SELECT id FROM files WHERE path LIKE ?` using the old path prefix
+- [x] **2.3** Recompute paths for all descendants and include their upserts in the same batch
+
+### Phase 3: Wire up and clean up
+
+- [x] **3.1** Switch the observer to call `scheduleSync(changedIds)` instead of `scheduleSync()`
+- [x] **3.2** Update `scheduleSync` to accumulate changed IDs across debounce window (union of all sets)
+- [x] **3.3** Keep `rebuild()` for initial load (`whenReady`) and the public `rebuild()` export
+- [x] **3.4** Remove the `rebuilding` guard flag—no longer needed since `syncRows` is incremental
+
+## Edge Cases
+
+### Rapid folder rename + file edit in same debounce window
+
+1. User renames folder `/docs` → `/notes`
+2. Within 100ms, user edits `/notes/readme.md`
+3. Both IDs land in the same `changedIds` set
+4. `syncRows` processes the folder first (recomputes descendants), then the file (which now has the correct parent path)
+5. **Resolution**: Process folders before files in each batch. Sort `changedIds` so folder-type rows are handled first.
+
+### File deleted before syncRows runs
+
+1. User deletes a file
+2. 100ms later, `syncRows` fires with that ID
+3. `filesTable.get(id)` returns `not_found`
+4. **Resolution**: Already handled—`not_found` triggers `DELETE` from SQLite.
+
+### Initial load (empty SQLite)
+
+1. Page loads, SQLite is `:memory:` with empty tables
+2. No prior state to diff against
+3. **Resolution**: `rebuild()` runs as before during `whenReady`. Surgical updates only kick in after init.
+
+### Content doc fails to open
+
+1. `contentDocs.open(id)` throws for a specific file
+2. **Resolution**: Same as current—catch and set content to `null`. File is still searchable by name.
+
+## Open Questions
+
+1. **Should we accumulate or replace changedIds across debounce resets?**
+ - Current code resets the timer on each new mutation. If we accumulate IDs, rapid edits to different files get batched together. If we replace, only the latest mutation's IDs survive.
+ - **Recommendation**: Accumulate (union). This ensures no mutations are lost during rapid activity.
+
+2. **Should folders be processed before files in a batch?**
+ - If a folder rename and a child file edit happen in the same batch, processing order matters for path correctness.
+ - **Recommendation**: Yes, sort changedIds so folders come first. Query `filesTable.get(id)` for each to check type.
+
+## Success Criteria
+
+- [ ] Editing a file only triggers 1 `contentDocs.open()` call (not N)
+- [ ] Renaming a file updates 1 row in SQLite (not N)
+- [ ] Renaming a folder updates the folder + its descendants (not all rows)
+- [ ] Deleting a file removes 1 row from SQLite (not rebuilds everything)
+- [ ] Initial page load still does a full rebuild
+- [ ] `rebuild()` is still callable for manual recovery
+- [ ] FTS search results remain correct after surgical updates
+- [ ] No regressions in existing behavior
+
+## References
+
+- `packages/filesystem/src/extensions/sqlite-index/index.ts` — Main file being modified
+- `packages/filesystem/src/extensions/sqlite-index/schema.ts` — SQLite schema (unchanged)
+- `packages/filesystem/src/extensions/sqlite-index/ddl.ts` — DDL generation (unchanged)
+- `packages/workspace/src/workspace/table-helper.ts` — Observer API providing `changedIds: Set`
+- `packages/workspace/src/shared/y-keyvalue/y-keyvalue-lww.ts` — Underlying change types (`add`/`update`/`delete`)
+- `apps/opensidian/src/lib/fs/fs-state.svelte.ts` — Consumer wiring `createSqliteIndex()` into workspace
+
+## Review
+
+**Completed**: 2026-03-14
+
+### Summary
+
+Replaced the full nuke-and-rebuild strategy with surgical per-row updates in `index.ts`. The observer now forwards `changedIds` through a debounced `scheduleSync` that accumulates IDs and flushes them to `syncRows`. Editing a file touches only that file's row. Folder renames cascade to descendants via a SQLite `LIKE` query. Full `rebuild()` is preserved for initial load and manual recovery.
+
+### Changes
+
+- **`computePathForRow(id, filesTable)`**: New module-level function. Walks `parentId` chain via `filesTable.get()` calls (not bulk `getAllValid()`). Mirrors `computePaths` behavior for cycles/orphans.
+- **`syncRows(changedIds)`**: New function inside factory closure. Classifies rows as deleted/folder/file, processes folders before files, reads content only for non-folders, batches all statements in one `client.batch()` call.
+- **Folder rename cascading**: After upserting a folder, queries SQLite for descendants whose path starts with the old prefix. Recomputes descendant paths by string replacement and includes `UPDATE` statements in the same batch.
+- **`scheduleSync(changedIds)`**: Now accepts and accumulates `Set` across debounce resets. Flushes accumulated set to `syncRows` when timer fires.
+- **Observer**: Changed from `() => scheduleSync()` to `(changedIds) => scheduleSync(changedIds)`.
+- **`rebuilding` guard removed**: No longer needed since `syncRows` is incremental and doesn't conflict with itself.
+- **`rebuild()`**: Kept exactly as-is for initial load and public export, minus the `try/finally` wrapper that only existed for the guard flag.
+
+### Deviations from Spec
+
+- None. Implementation followed the spec exactly.