Skip to content

refactor: extract shared command palette, inline file tree editing, surgical SQLite index sync#1537

Merged
braden-w merged 15 commits intomainfrom
opencode/neon-forest
Mar 18, 2026
Merged

refactor: extract shared command palette, inline file tree editing, surgical SQLite index sync#1537
braden-w merged 15 commits intomainfrom
opencode/neon-forest

Conversation

@braden-w
Copy link
Member

@braden-w braden-w commented Mar 17, 2026

Extracts a shared CommandPalette component into packages/ui, replaces opensidian's modal file dialogs with inline tree inputs, and swaps the SQLite index extension's full nuke-and-rebuild with surgical per-row syncing.

Both opensidian and tab-manager had their own command palette implementations with duplicated logic—filtering, grouping, keyboard navigation. This PR unifies them behind a single CommandPaletteItem[] contract so apps just provide data:

// Any app can feed the shared palette with this shape
export const items: CommandPaletteItem[] = [
  {
    id: 'dedup',
    label: 'Remove Duplicates',
    icon: CopyMinusIcon,
    keywords: ['dedup', 'duplicate', 'remove'],
    group: 'Quick Actions',
    onSelect() { /* ... */ },
  },
];

The keywords field uses bits-ui's native keywords prop for independent scoring instead of concatenating into value. Tab-manager's palette items moved from a disconnected quick-actions.ts into components/command-palette/items.ts, co-located next to the component. The commandsFromActions bridge in workspace was deleted—it was dead code (every tab-manager action requires input, so the default filter produced zero items).

┌──────────────────────────────────────────────────┐
│  Before: each app builds its own palette         │
│                                                  │
│  opensidian/CommandPalette.svelte  ──┐           │
│  tab-manager/CommandPalette.svelte ──┤ duplicate │
│                                      │ logic     │
│  (filtering, grouping, keyboard)   ──┘           │
├──────────────────────────────────────────────────┤
│  After: shared component, apps provide data      │
│                                                  │
│  packages/ui/CommandPalette ◀── CommandPaletteItem[]
│    ├── opensidian: palette items                 │
│    └── tab-manager: palette items                │
└──────────────────────────────────────────────────┘

Opensidian's CreateDialog and RenameDialog modals felt disconnected from the tree—every major file explorer (VS Code, Obsidian, JetBrains) uses inline inputs at the insertion point instead. The new InlineNameInput renders directly in the tree with Enter/Escape/blur handling, auto-focus, and stem-only selection. Keyboard shortcuts (N, ⇧N, F2, Delete) are now scoped to the tree. Net result: -1 component, -8 lines, better UX.

The SQLite index extension previously nuked and rebuilt the entire database on every Y.Doc change—fine for a dozen files, unusable past a few hundred. Now syncRows() processes only the changed IDs accumulated across a debounce window, with folder renames cascading to descendants via LIKE query. rebuild() is preserved for initial load only.

// Before: every Yjs mutation triggers full rebuild
doc.on('update', () => rebuildEntireIndex(doc));

// After: debounced sync of only changed rows
doc.on('update', () => scheduleSync(changedIds));
// syncRows() does per-row upserts/deletes based on changedIds

Also in this PR: lucide-svelte@lucide/svelte migration for Svelte 5 compatibility in opensidian, consistent appendFile option forwarding in the filesystem package, and describe-workspace now exports actions for introspection.

braden-w added 15 commits March 14, 2026 10:16
…ow updates

Add syncRows() that processes only changed IDs instead of rebuilding
the entire SQLite index on every Yjs mutation. Folder renames cascade
to descendants via LIKE query. scheduleSync accumulates changedIds
across debounce window. rebuild() preserved for initial load.
…or file creation and rename

Modal dialogs for file/folder creation and rename felt disconnected from
the tree—every major file explorer (VS Code, Obsidian, JetBrains) uses
inline inputs at the insertion point instead.

- Add InlineNameInput component with Enter/Escape/blur, auto-focus, stem selection
- Centralize inline editing state in fs-state (inlineCreate, renamingId)
- Render inline inputs in-tree: inside folders for creation, replacing name for rename
- Add keyboard shortcuts scoped to tree: N, ⇧N, F2, Delete/Backspace
- Add shortcut hints to context menu items
- Delete CreateDialog and RenameDialog (net -1 component, -8 lines)
- Eliminate duplicated dialog state that was scattered across Toolbar and every FileTreeItem
…alette

Pass keywords as a separate prop instead of concatenating them into
value—lets bits-ui score value and keywords independently for better
fuzzy matching. Replace manual Map loop with Map.groupBy and inline
the select handler.
… compatibility

The old lucide-svelte package exports Svelte 4 class components which
are incompatible with the Component type expected by CommandPaletteItem.
Switch to @lucide/svelte (individual icon imports) to match the rest of
the monorepo and resolve the type error.
…omActions utility

Both opensidian and tab-manager now use the shared CommandPalette wrapper
instead of raw Command.* primitives, cutting boilerplate significantly.
Also adds commandsFromActions to convert workspace action trees into
palette-compatible items via structural typing (no UI package dependency).
…ommandsFromActions

These overrides had no consumers and the defaults are the only sane
behavior—actions with input can't run from a palette, and path[0] is
the natural group heading.
The query/actions.ts file exported its mutations as "commands", colliding
with the keyboard shortcut registry in commands.ts. Rename to "actions"
to match the filename and eliminate the naming ambiguity—commands.ts
owns the "commands" concept (shortcut definitions), query/actions.ts
owns "actions" (UI-boundary mutations).
Every tab-manager workspace action requires input, so the default filter
(skip actions with input) produced zero palette items. Quick actions are
the correct abstraction for the command palette—they close over reactive
UI state and handle confirmation dialogs, which workspace actions cannot.

Removes the dead export and cleans up the JSDoc reference in
CommandPaletteItem.
Move quick-actions.ts → components/command-palette/items.ts and rename
the export from quickActions to items. The "quick actions" name was a
VS Code-ism that obscured what these actually are: CommandPaletteItem[]
consumed exclusively by the palette component.

Co-locating items.ts next to CommandPalette.svelte makes the data flow
self-documenting—open the directory and you see both the UI and its
data source. Adds a barrel index.ts for clean consumer imports.
…gation

appendFile had two paths that delegate to writeFile with inconsistent
arguments. The not-found branch passed the already-resolved abs path and
forwarded _options; the non-text-mode branch passed the raw path but
dropped _options. Both now use the raw path (writeFile resolves
internally) and forward _options for API consistency.
# Conflicts:
#	apps/tab-manager/src/lib/quick-actions.ts
#	apps/whispering/src/lib/state/settings.svelte.ts
#	apps/whispering/src/routes/(app)/+page.svelte
# Conflicts:
#	packages/filesystem/src/file-system.ts
@braden-w braden-w merged commit 595df79 into main Mar 18, 2026
1 of 10 checks passed
@braden-w braden-w deleted the opencode/neon-forest branch March 18, 2026 02:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant