Implement milestone 6b workbench routing skeleton
This commit is contained in:
+41
-2
@@ -59,12 +59,51 @@ This validates:
|
||||
|
||||
## Desktop Runtime Scanning Paths
|
||||
|
||||
The desktop scans two directories for plugins:
|
||||
The desktop resolves plugin directories in one shared backend resolver. Priority:
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `VERSTAK_PLUGIN_DIR` | Dev/test override. Multiple paths can be separated with the OS path separator |
|
||||
| `./plugins/` | Bundled/dev plugins relative to the current working directory |
|
||||
| `<binary-dir>/plugins/` | Packaged plugins shipped next to the desktop executable |
|
||||
| `~/.config/verstak/plugins/` | User-installed plugins |
|
||||
| `./plugins/` | Bundled/dev plugins (project-local) |
|
||||
|
||||
The resolver normalizes paths and removes duplicates before scanning. Missing
|
||||
directories are ignored by discovery.
|
||||
|
||||
Discovery scans all resolved directories in order. If two plugin packages declare
|
||||
the same `id`, the first package wins and later duplicates are skipped. The
|
||||
warning includes both package paths, so during development check the log if an
|
||||
updated plugin appears to be ignored.
|
||||
|
||||
## Bundled Plugin API During Development
|
||||
|
||||
Frontend bundles are mounted with a plugin-scoped API created by
|
||||
`createPluginAPI(pluginId)`. The current API supports:
|
||||
|
||||
- `settings.read/write/writeAll`
|
||||
- `capabilities.list/get/has`
|
||||
- `commands.register/execute` for handlers declared in `contributes.commands`
|
||||
- `events.publish/subscribe` using the bundled frontend event bus
|
||||
- `files.list/metadata/readText/writeText/createFolder/move/trash` for
|
||||
canonical vault-relative slash paths guarded by `files.read`, `files.write`,
|
||||
and `files.delete`. Backslashes, Windows absolute paths, UNC paths,
|
||||
traversal, `.verstak` variants, and symlink read/write/move/trash operations
|
||||
are rejected. Text read/write is UTF-8 only and limited to 2 MB for reads.
|
||||
- `workbench.openResource/editResource` for routing vault resources to
|
||||
contributed `openProviders`. Plugins must declare `workbench.open`; this is a
|
||||
policy/contract check. Files and Notes plugins call this API and do not import
|
||||
a concrete editor plugin.
|
||||
|
||||
Editor/viewer plugins contribute providers with `contributes.openProviders`.
|
||||
Workbench selects by resource kind, extension/mime, context (`generic-text`,
|
||||
`generic-markdown`, `notes-markdown`), user preference, priority, then
|
||||
deterministic `pluginId/providerId` tie-break. If nothing matches, Workbench
|
||||
shows `no-provider` fallback instead of a core editor.
|
||||
|
||||
This is a cooperative contract, not a sandbox. Bundled plugins run in the same JS
|
||||
context as the desktop frontend; real isolation is deferred to the sidecar/sandbox
|
||||
milestone.
|
||||
|
||||
## Important Rules
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# GUI Testing
|
||||
|
||||
## Overview
|
||||
|
||||
Verstak Desktop uses **Playwright** for frontend E2E tests that run in a real
|
||||
Chromium browser with mocked Wails bindings. This tests the Svelte component
|
||||
logic, user interactions, and UI state transitions — without needing the actual
|
||||
Wails desktop shell.
|
||||
|
||||
## What is tested
|
||||
|
||||
### Frontend E2E (Playwright)
|
||||
|
||||
Located in `frontend/e2e/`, run via `npm run test:e2e`.
|
||||
|
||||
These tests:
|
||||
|
||||
- Launch a Vite dev server with mock Wails bindings
|
||||
- Open the app in a real Chromium browser via Playwright
|
||||
- Simulate user clicks, wait for UI transitions, assert DOM state
|
||||
- Collect console errors and page errors on failure
|
||||
- Capture screenshots on failure
|
||||
|
||||
### Test suites
|
||||
|
||||
| File | Suite | Tests | Status |
|
||||
|------|-------|-------|--------|
|
||||
| `plugin-manager-disable-enable.spec.js` | A: Disable/Enable refresh | 4 | 3 pass, 1 fail* |
|
||||
| `sidebar-opens-view.spec.js` | B: Sidebar → view routing | 3 | 3 pass |
|
||||
| `reload-updates-state.spec.js` | C: Reload updates UI | 3 | 2 pass, 1 fail* |
|
||||
|
||||
\* Failing tests document **known bugs** (see below).
|
||||
|
||||
## Known bugs detected by tests
|
||||
|
||||
### Bug M5-1: Sidebar does not update when plugin state changes
|
||||
|
||||
**Symptom:** After disabling a plugin in Plugin Manager, the sidebar item for
|
||||
that plugin remains visible. After re-enabling, it stays visible (doesn't
|
||||
disappear then reappear — it was never gone).
|
||||
|
||||
**Root cause:** `Sidebar.svelte` loads plugin/contribution data once in
|
||||
`onMount` and stores it in local `sidebarItems`. When `PluginManager`
|
||||
disables/enables a plugin and calls `ReloadPlugins`, the `PluginManager`
|
||||
component re-fetches data, but `Sidebar` does not react to the change — it
|
||||
still holds the stale list.
|
||||
|
||||
**Affected tests:**
|
||||
- `A: Disable plugin: button changes to Enable, sidebar item disappears`
|
||||
- `A: Disable → Enable full flow in sequence`
|
||||
- `C: Reload after mock state change reflects new plugin status`
|
||||
|
||||
**Fix needed:** Sidebar must either:
|
||||
1. Re-fetch contributions when it receives a custom event (e.g.
|
||||
`verstak:plugins-reloaded`), or
|
||||
2. Read plugin state reactively from a shared store that both
|
||||
PluginManager and Sidebar subscribe to.
|
||||
|
||||
## What is NOT tested
|
||||
|
||||
### Real desktop GUI (WebKitGTK + Wails native shell)
|
||||
|
||||
The Playwright tests run the frontend in a **standard Chromium browser** with
|
||||
mocked Wails bindings. They do **not** test:
|
||||
|
||||
- Actual WebKitGTK rendering (Wails uses WebKitGTK, not Chromium)
|
||||
- Native window management (minimize, maximize, resize)
|
||||
- Native file dialogs (SelectDirectory, SelectVaultForOpen)
|
||||
- Clipboard integration
|
||||
- System tray / menu bar
|
||||
- Plugin frontend bundle loading from real filesystem
|
||||
- Wails event system (window.runtime.EventsOn/Emit)
|
||||
|
||||
For real Wails smoke tests, a separate layer is needed using:
|
||||
- **AT-SPI2** (Linux accessibility tree inspection)
|
||||
- **xdotool** / **ydotool** (input simulation)
|
||||
- **scrot** / **import** (screenshot capture)
|
||||
|
||||
## Running tests
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
|
||||
# Run all E2E tests (headless)
|
||||
npm run test:e2e
|
||||
|
||||
# Run with Playwright UI (interactive)
|
||||
npm run test:e2e:ui
|
||||
|
||||
# Run in headed browser (visible)
|
||||
npm run test:e2e:headed
|
||||
```
|
||||
|
||||
## Test infrastructure
|
||||
|
||||
### Mock bridge (`src/lib/test/wails-mock.js`)
|
||||
|
||||
Replaces `window['go']['api']['App']` with in-memory mock implementations of
|
||||
all Wails backend methods. Provides:
|
||||
|
||||
- Mutable plugin state (enable/disable/status)
|
||||
- Mutable vault state
|
||||
- Mutable contributions (views, commands, sidebar items, settings panels)
|
||||
- Test helpers via `window.__wailsMock`:
|
||||
- `reset()` — reset all state to defaults
|
||||
- `setPluginStatus(id, status, enabled)` — change plugin state
|
||||
- `getPluginState(id)` — read current state
|
||||
- `setVaultStatus(status)` — change vault state
|
||||
|
||||
### Test harness (`index.html`)
|
||||
|
||||
The same `index.html` is used for both production and test. It detects whether
|
||||
the Wails runtime (`window['go']`) is present. If not (i.e. running in a plain
|
||||
browser), it loads the mock bridge before the Svelte app.
|
||||
|
||||
### Playwright config (`playwright.config.js`)
|
||||
|
||||
- Dev server: `vite --mode test --port 5174`
|
||||
- Browser: Chromium headless
|
||||
- Timeouts: 30s test, 10s expect
|
||||
- Workers: 1 (sequential)
|
||||
- Screenshots: on failure
|
||||
- Traces: on first retry
|
||||
- Results: `e2e-results/test-results.json`
|
||||
|
||||
## Adding new tests
|
||||
|
||||
1. Create `e2e/your-test.spec.js`
|
||||
2. Import helpers from `./helpers.js`
|
||||
3. Use `test.beforeEach` to reset mock state and navigate to `/`
|
||||
4. Use `test.afterEach` to assert no console errors
|
||||
5. Write scenarios as user actions + assertions
|
||||
6. Run with `npm run test:e2e`
|
||||
|
||||
### Selector conventions
|
||||
|
||||
- Plugin cards: `.plugin-card` filtered by text
|
||||
- Buttons: `.btn-disable`, `.btn-enable`, `.btn-settings`, `.reload-btn`
|
||||
- Sidebar items: `.sidebar .plugin-item`
|
||||
- View container: `.view-container`
|
||||
- View header: `.view-header h2`
|
||||
- Status badges: `.status-badge`
|
||||
- Toast: `.toast`
|
||||
@@ -0,0 +1,403 @@
|
||||
# Milestone 6a Files Core Service Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> `superpowers:subagent-driven-development` or `superpowers:executing-plans` to
|
||||
> implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for
|
||||
> tracking.
|
||||
|
||||
**Goal:** Add a safe backend Files core service and plugin bridge for
|
||||
vault-relative text file operations.
|
||||
|
||||
**Architecture:** Files Core is a backend service under `internal/core/files`
|
||||
that accepts vault-relative paths, enforces reserved path policy, and performs
|
||||
atomic text writes. The Wails API exposes plugin-scoped methods guarded by plugin
|
||||
state and `files.*` permissions; SDK types describe the bridge shape.
|
||||
|
||||
**Tech Stack:** Go backend services/tests, Wails-bound API methods, TypeScript
|
||||
SDK type definitions, existing Playwright/Vitest checks.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Milestone 6a is implemented.
|
||||
|
||||
Actual backend package:
|
||||
|
||||
- `internal/core/files/types.go`
|
||||
- `internal/core/files/path_policy.go`
|
||||
- `internal/core/files/service.go`
|
||||
- `internal/core/files/*_test.go`
|
||||
|
||||
Actual plugin-scoped Wails methods:
|
||||
|
||||
```go
|
||||
func (a *App) ListVaultFiles(pluginID string, relativeDir string) ([]files.FileEntry, string)
|
||||
func (a *App) GetVaultFileMetadata(pluginID string, relativePath string) (files.FileMetadata, string)
|
||||
func (a *App) ReadVaultTextFile(pluginID string, relativePath string) (string, string)
|
||||
func (a *App) WriteVaultTextFile(pluginID string, relativePath string, content string, options files.WriteOptions) string
|
||||
func (a *App) CreateVaultFolder(pluginID string, relativePath string) string
|
||||
func (a *App) MoveVaultPath(pluginID string, fromRelativePath string, toRelativePath string, options files.MoveOptions) string
|
||||
func (a *App) TrashVaultPath(pluginID string, relativePath string) (files.TrashResult, string)
|
||||
```
|
||||
|
||||
Actual bundled frontend API:
|
||||
|
||||
- `api.files.list(relativeDir)`
|
||||
- `api.files.metadata(relativePath)`
|
||||
- `api.files.readText(relativePath)`
|
||||
- `api.files.writeText(relativePath, content, options)`
|
||||
- `api.files.createFolder(relativePath)`
|
||||
- `api.files.move(fromRelativePath, toRelativePath, options)`
|
||||
- `api.files.trash(relativePath)`
|
||||
|
||||
Implemented limits:
|
||||
|
||||
- canonical vault-relative slash paths only;
|
||||
- backslashes, POSIX absolute paths, Windows drive paths, UNC paths, traversal,
|
||||
null bytes, and empty file paths are rejected;
|
||||
- `.verstak/` is reserved case-insensitively and hidden from public Files API;
|
||||
- metadata may report symlinks, but list-through-symlink and
|
||||
read/write/move/trash through symlink are forbidden;
|
||||
- text read/write only, with `readText` limited to UTF-8 files up to 2 MB;
|
||||
- trash uses `.verstak/trash/files/<trashId>/...` with restore metadata, but
|
||||
restore itself is deferred;
|
||||
- binary streaming, watcher, external editor, Files UI, Notes service, sidecar,
|
||||
sandbox/security isolation deferred.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
Implement:
|
||||
|
||||
- Backend Files service.
|
||||
- Safe vault-relative path handling.
|
||||
- Reserved `.verstak/` policy.
|
||||
- List files.
|
||||
- Read/write text files.
|
||||
- Create folder.
|
||||
- Move path.
|
||||
- Trash path.
|
||||
- Atomic writes.
|
||||
- Backend tests.
|
||||
- SDK bridge shape draft.
|
||||
|
||||
Do not implement:
|
||||
|
||||
- Full Notes plugin.
|
||||
- Notes UI.
|
||||
- Sync.
|
||||
- Watcher.
|
||||
- Binary streaming.
|
||||
- External editor integration.
|
||||
- Sidecar/security isolation.
|
||||
|
||||
## Canonical Policy
|
||||
|
||||
All public Files API methods use canonical vault-relative slash paths.
|
||||
|
||||
Rejected inputs:
|
||||
|
||||
- absolute paths;
|
||||
- backslashes;
|
||||
- Windows drive paths and UNC/network paths;
|
||||
- paths containing `..` after normalization;
|
||||
- null bytes;
|
||||
- empty paths where a file path is required;
|
||||
- access to `.verstak/` through the public plugin Files API, including
|
||||
`.Verstak` case variants.
|
||||
|
||||
Delete behavior:
|
||||
|
||||
- `TrashVaultPath` moves files/folders into `.verstak/trash`.
|
||||
- Trash metadata includes `originalPath`, `deletedAt`, `originalType`,
|
||||
`trashId`, and `basename`.
|
||||
- Permanent delete is out of scope.
|
||||
- Restore is out of scope.
|
||||
|
||||
Write behavior:
|
||||
|
||||
- Text writes use a temporary file in the target directory and rename into place.
|
||||
- Existing files are overwritten only when the method explicitly allows overwrite.
|
||||
- Parent directory must exist unless the method explicitly creates it.
|
||||
|
||||
Binary behavior:
|
||||
|
||||
- Binary files can appear in list/metadata results.
|
||||
- Binary read/write streaming is out of scope.
|
||||
|
||||
## Public Backend Shape
|
||||
|
||||
Implemented plugin-scoped Wails methods:
|
||||
|
||||
```go
|
||||
func (a *App) ListVaultFiles(pluginID string, relativeDir string) ([]files.FileEntry, string)
|
||||
func (a *App) GetVaultFileMetadata(pluginID string, relativePath string) (files.FileMetadata, string)
|
||||
func (a *App) ReadVaultTextFile(pluginID string, relativePath string) (string, string)
|
||||
func (a *App) WriteVaultTextFile(pluginID string, relativePath string, content string, options files.WriteOptions) string
|
||||
func (a *App) CreateVaultFolder(pluginID string, relativePath string) string
|
||||
func (a *App) MoveVaultPath(pluginID string, fromRelativePath string, toRelativePath string, options files.MoveOptions) string
|
||||
func (a *App) TrashVaultPath(pluginID string, relativePath string) (files.TrashResult, string)
|
||||
```
|
||||
|
||||
Permission mapping:
|
||||
|
||||
- `ListVaultFiles`, `GetVaultFileMetadata`, `ReadVaultTextFile`: `files.read`.
|
||||
- `WriteVaultTextFile`, `CreateVaultFolder`, `MoveVaultPath`: `files.write`.
|
||||
- `TrashVaultPath`: `files.delete`.
|
||||
|
||||
## Data Types
|
||||
|
||||
Create `internal/core/files/types.go`:
|
||||
|
||||
```go
|
||||
package files
|
||||
|
||||
type EntryKind string
|
||||
|
||||
const (
|
||||
KindFile EntryKind = "file"
|
||||
KindDirectory EntryKind = "directory"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Kind EntryKind `json:"kind"`
|
||||
Size int64 `json:"size"`
|
||||
ModifiedAt string `json:"modifiedAt"`
|
||||
IsText bool `json:"isText"`
|
||||
IsBinary bool `json:"isBinary"`
|
||||
IsHidden bool `json:"isHidden"`
|
||||
IsReserved bool `json:"isReserved"`
|
||||
}
|
||||
```
|
||||
|
||||
## Task 1: Path Policy
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `internal/core/files/path.go`
|
||||
- Create: `internal/core/files/path_test.go`
|
||||
|
||||
- [ ] Add `NormalizeVaultRelativePath(relative string) (string, error)`.
|
||||
- [ ] Reject absolute paths, null bytes, `..`, and empty file paths.
|
||||
- [ ] Preserve path case, including canonical `Notes`.
|
||||
- [ ] Add `IsReservedPath(relative string) bool` returning true for `.verstak`
|
||||
and `.verstak/...`.
|
||||
- [ ] Add tests:
|
||||
`TestNormalizeRejectsAbsolutePath`,
|
||||
`TestNormalizeRejectsTraversal`,
|
||||
`TestNormalizeRejectsNullByte`,
|
||||
`TestNormalizePreservesCase`,
|
||||
`TestReservedPathPolicy`.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/core/files
|
||||
```
|
||||
|
||||
Expected: all `internal/core/files` tests pass.
|
||||
|
||||
## Task 2: Files Service
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `internal/core/files/service.go`
|
||||
- Create/modify: `internal/core/files/service_test.go`
|
||||
|
||||
- [ ] Define `Service` with a vault dependency that can return the current vault
|
||||
root and status.
|
||||
- [ ] Implement `List(relativeDir string) ([]Entry, error)`.
|
||||
- [ ] Implement `Metadata(relativePath string) (Entry, error)`.
|
||||
- [ ] Implement `ReadText(relativePath string) (string, error)`.
|
||||
- [ ] Implement `WriteText(relativePath, content string, overwrite bool) (Entry, error)`.
|
||||
- [ ] Implement `Mkdir(relativePath string) (Entry, error)`.
|
||||
- [ ] Implement `Move(fromRelativePath, toRelativePath string, overwrite bool) (Entry, error)`.
|
||||
- [ ] Implement `Trash(relativePath string) (Entry, error)`.
|
||||
- [ ] Use the shared path policy for every public method.
|
||||
- [ ] Block `.verstak` paths in every public method.
|
||||
- [ ] Add tests for closed vault, list, metadata, text read/write, mkdir, move,
|
||||
trash, overwrite false conflict, overwrite true replace, and reserved path
|
||||
rejection.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/core/files
|
||||
```
|
||||
|
||||
Expected: all `internal/core/files` tests pass.
|
||||
|
||||
## Task 3: Atomic Writes
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `internal/core/files/service.go`
|
||||
- Modify: `internal/core/files/service_test.go`
|
||||
|
||||
- [ ] Write text content to a temp file in the target directory.
|
||||
- [ ] Rename the temp file into the final path only after successful write.
|
||||
- [ ] Remove temp file on write failure.
|
||||
- [ ] Add test `TestWriteTextIsAtomicOnFailure` using a controlled failing path
|
||||
or permission-denied directory.
|
||||
- [ ] Add test `TestWriteTextDoesNotLeaveTempFile`.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/core/files
|
||||
```
|
||||
|
||||
Expected: all `internal/core/files` tests pass.
|
||||
|
||||
## Task 4: Permissions And Capabilities
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `internal/core/permissions/registry.go`
|
||||
- Modify: `main.go`
|
||||
- Modify: `internal/api/app_test.go`
|
||||
|
||||
- [ ] Register permissions: `files.read`, `files.write`, `files.delete`.
|
||||
- [ ] Register core capability `verstak/core/files/v1` when vault services are
|
||||
initialized.
|
||||
- [ ] Add API guard tests proving each Files bridge method rejects plugins that
|
||||
are missing the required permission.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/core/permissions ./internal/api
|
||||
```
|
||||
|
||||
Expected: permission registry and API tests pass.
|
||||
|
||||
## Task 5: Wails API Bridge
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `internal/api/app.go`
|
||||
- Modify: `internal/api/app_test.go`
|
||||
- Modify after Wails generation or by hand if generation is unavailable:
|
||||
`frontend/wailsjs/go/api/App.d.ts`
|
||||
- Modify after Wails generation or by hand if generation is unavailable:
|
||||
`frontend/wailsjs/go/api/App.js`
|
||||
|
||||
- [ ] Add `files.Service` to `api.App`.
|
||||
- [ ] Add plugin-scoped methods listed in "Public Backend Shape".
|
||||
- [ ] Use `requirePluginAccess(pluginID, permission)` for every method.
|
||||
- [ ] Return readable errors for closed vault, missing file, reserved path,
|
||||
conflict, and missing permission.
|
||||
- [ ] Add tests for successful read/write/list/mkdir/move/trash through `App`.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/api
|
||||
```
|
||||
|
||||
Expected: API tests pass.
|
||||
|
||||
## Task 6: Frontend Plugin API Draft
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `frontend/src/lib/plugin-host/VerstakPluginAPI.js`
|
||||
- Modify: `frontend/src/lib/test/wails-mock.js`
|
||||
- Add/modify focused frontend tests under `frontend/e2e/` only if existing test
|
||||
coverage cannot validate the shape outside Playwright.
|
||||
|
||||
- [ ] Add `api.files.list(relativeDir)`.
|
||||
- [ ] Add `api.files.metadata(relativePath)`.
|
||||
- [ ] Add `api.files.readText(relativePath)`.
|
||||
- [ ] Add `api.files.writeText(relativePath, content, options)`.
|
||||
- [ ] Add `api.files.mkdir(relativePath)`.
|
||||
- [ ] Add `api.files.move(fromRelativePath, toRelativePath, options)`.
|
||||
- [ ] Add `api.files.trash(relativePath)`.
|
||||
- [ ] Keep all calls plugin-scoped; plugin code must not pass `pluginId`.
|
||||
- [ ] Mock readable errors for reserved path and missing permission.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: frontend build passes.
|
||||
|
||||
## Task 7: SDK Bridge Shape Draft
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `../verstak-sdk/src/plugin-api.ts`
|
||||
- Modify: `../verstak-sdk/src/test-utils.ts`
|
||||
- Modify: `../verstak-sdk/src/plugin-api.test.ts`
|
||||
|
||||
- [ ] Add `files` API TypeScript interfaces matching the frontend API names.
|
||||
- [ ] Add mock Files API methods in `createMockPluginAPI`.
|
||||
- [ ] Add contract tests for API shape, text write/read, reserved path error, and
|
||||
trash result shape.
|
||||
- [ ] Run:
|
||||
|
||||
```bash
|
||||
cd ../verstak-sdk
|
||||
./scripts/check.sh
|
||||
./scripts/build.sh
|
||||
./scripts/test.sh
|
||||
```
|
||||
|
||||
Expected: SDK check, build, and tests pass.
|
||||
|
||||
## Task 8: Documentation
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/PLUGIN_RUNTIME.md`
|
||||
- Modify: `docs/NOTES_FILES_PLUGIN_PLAN.md`
|
||||
|
||||
- [ ] Document Files Core API as functional for Milestone 6a.
|
||||
- [ ] Keep Notes API documented as planned until Milestone 6b or later.
|
||||
- [ ] Document `.verstak` reserved path policy.
|
||||
- [ ] Document slash-only path policy, Windows/UNC rejection, and symlink policy.
|
||||
- [ ] Document text-only write support and deferred binary streaming.
|
||||
|
||||
## Task 9: Final Verification
|
||||
|
||||
- [ ] Run desktop backend tests:
|
||||
|
||||
```bash
|
||||
cd verstak-desktop
|
||||
go test ./...
|
||||
```
|
||||
|
||||
- [ ] Run desktop frontend build:
|
||||
|
||||
```bash
|
||||
cd verstak-desktop/frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
- [ ] Run desktop e2e:
|
||||
|
||||
```bash
|
||||
cd verstak-desktop/frontend
|
||||
npm run test:e2e -- --reporter=list
|
||||
```
|
||||
|
||||
- [ ] Run official plugins checks:
|
||||
|
||||
```bash
|
||||
cd verstak-official-plugins
|
||||
./scripts/check.sh
|
||||
./scripts/build.sh
|
||||
```
|
||||
|
||||
- [ ] Run SDK checks:
|
||||
|
||||
```bash
|
||||
cd verstak-sdk
|
||||
./scripts/check.sh
|
||||
./scripts/build.sh
|
||||
./scripts/test.sh
|
||||
```
|
||||
|
||||
Expected: all commands exit 0. Existing Svelte unused CSS warnings are acceptable
|
||||
only if they remain warnings and do not fail the build.
|
||||
@@ -0,0 +1,135 @@
|
||||
# Milestone 6b - Open/Edit Provider Registry + Workbench Routing Skeleton
|
||||
|
||||
This milestone adds the minimal infrastructure layer for open/edit routing before
|
||||
Files UI, Notes UI, or editor implementation starts. It does not implement those
|
||||
plugins and does not add a concrete core-owned Markdown editor.
|
||||
|
||||
## Existing Architecture Found
|
||||
|
||||
| Path | Section/title | Summary |
|
||||
|---|---|---|
|
||||
| `../verstak-docs/00_README.md` | Main architecture invariant | Core does not know concrete notes, file manager, or markdown editor features. It owns vault, plugin runtime, capability registry, contribution points, permissions, settings, events, storage, and UI shell. |
|
||||
| `../verstak-docs/01_Product_Vision.md` | What is not in core / platform goal | Markdown editor, file manager, preview, and notes workflow are plugins. Users should be able to replace the markdown editor or install multiple editors. |
|
||||
| `../verstak-docs/02_Platform_Architecture.md` | UI Shell / Capability Registry | UI Shell knows contribution points, not concrete note editor or file preview implementations. Files plugin checks editor/viewer capabilities instead of depending on `official.markdown-editor`. |
|
||||
| `../verstak-docs/03_Repositories.md` | Repository split | `verstak-desktop` is Core Platform + UI Shell and does not contain mandatory notes, file manager, or markdown editor modules. Official plugins live separately. |
|
||||
| `../verstak-docs/04_Plugin_System.md` | Goal / capabilities instead of plugin names / contribution points | Notes, file manager, editor, and viewer are plugin functions. The docs explicitly reject `requires: ["official.markdown-editor"]` and prefer `optionalRequires: ["editor.text.markdown"]`. |
|
||||
| `../verstak-docs/05_Official_Plugins.md` | official.files / official.notes / official.markdown-editor | Files optionally depends on editor/viewer capabilities. Markdown editor provides `editor.text`, `editor.text.markdown`, and `editor.note.markdown`, and must not own note storage or depend directly on `official.notes`. |
|
||||
| `../verstak-docs/06_Migration_Strategy.md` | Do not / Definition of Done | Do not make notes/files/editor mandatory core parts. The platform transition is done only when notes/files/editor/preview/activity work as plugins. |
|
||||
| `docs/NOTES_FILES_PLUGIN_PLAN.md` | Canonical Notes Model | Notes are ordinary Markdown files in canonical `Notes/` folders. No lowercase `notes`, no `.verstak/notes`, no plugin-data note content, no UUID-only filenames. |
|
||||
| `docs/NOTES_FILES_PLUGIN_PLAN.md` | Files Service Model | Files Core is raw vault file access and does not understand note semantics. Milestone 6a exposes safe text file methods and defers UI, watcher, binary streaming, external editor, and restore. |
|
||||
| `docs/PLUGIN_RUNTIME.md` | Contribution Points / Bundled Frontend Plugin API | Current runtime hosts views/sidebar/settings/commands and exposes plugin-scoped Files API. `fileActions`, `noteActions`, `contextMenuEntries`, search/activity/status bar entries are registered but not hosted. |
|
||||
| `internal/core/contribution/registry.go` | Contribution registry | Registry has the established contribution points and now extends that model with `openProviders`. |
|
||||
| `internal/core/plugin/plugin.go` | Manifest contributions | Plugin manifest types now include `openProviders` alongside existing contribution points. |
|
||||
| `frontend/src/lib/shell/WorkbenchHost.svelte` and `frontend/src/lib/plugin-host/PluginBundleHost.svelte` | Frontend plugin host | Workbench mounts the selected provider component by plugin id and component id. It remains generic and does not know a concrete editor. |
|
||||
| `frontend/src/lib/plugin-host/VerstakPluginAPI.js` | Plugin API | Bundled plugins can call `api.workbench.openResource()` and `api.workbench.editResource()` in addition to settings, capabilities, events, commands, and Files API. |
|
||||
| `../verstak-sdk/src/types.ts` and `../verstak-sdk/schemas/manifest.json` | SDK contribution contracts | SDK types/schema define `openProviders`, `OpenResourceRequest`, provider supports, and `files.*` permissions. |
|
||||
|
||||
## What Matches The Desired Model
|
||||
|
||||
- Files/Notes/Editor are already documented as plugins, not core modules.
|
||||
- Official plugins are expected to use the same runtime as community plugins.
|
||||
- Files plugin is already documented as capability-driven, not hard-wired to a markdown editor.
|
||||
- Markdown editor is already documented as replaceable via capabilities.
|
||||
- Notes are already documented as Markdown files under canonical `Notes/` folders, without `.verstak/notes`, UUID note entities, or a second storage truth.
|
||||
- Desktop code has no hardcoded Markdown editor component.
|
||||
- Existing plugin host can mount arbitrary plugin components, which is enough foundation for an editor provider host.
|
||||
|
||||
## Contradictions Found
|
||||
|
||||
- `docs/NOTES_FILES_PLUGIN_PLAN.md` still said Files Core API/capability/permissions were unavailable, while later sections and code show Milestone 6a implemented them.
|
||||
- `../verstak-docs/05_Official_Plugins.md` calls notes "first-class Verstak entities". That is acceptable only as UI semantics; implementation must not create a separate note storage entity.
|
||||
- `../verstak-docs/05_Official_Plugins.md` lists "Open externally" as a Files fallback. External open remains deferred and must not enter Milestone 6b.
|
||||
- `../verstak-sdk/schemas/manifest.json` previously did not include `files.read`, `files.write`, or `files.delete` in the permissions enum, while SDK TS types, desktop permissions, and official `platform-test` manifest already used them. Milestone 6b resolves this 6a contract cleanup item.
|
||||
|
||||
## Missing Before 6b
|
||||
|
||||
- `openProviders` contribution point.
|
||||
- `OpenResourceRequest` contract.
|
||||
- Workbench open/edit routing API for Files/Notes plugins.
|
||||
- Provider selection model using resource kind, extension/mime, notes context, user preference, provider priority, deterministic fallback, and disabled provider fallback.
|
||||
- User preferences for default text editor provider, default markdown editor provider, and default notes-context markdown editor provider.
|
||||
- Host slot/tab that mounts the selected provider component with an open resource request.
|
||||
- Tests for provider registration, selection, preferences, disabled provider fallback, and notes-context routing.
|
||||
|
||||
## Added In 6b
|
||||
|
||||
- `contributes.openProviders` in SDK schema/types and desktop manifest structs.
|
||||
- Desktop contribution registry support for registering, replacing, listing, and
|
||||
unregistering open providers.
|
||||
- `OpenResourceRequest`, `OpenResourceContext`, `OpenResourceResult`, and
|
||||
opened-resource state in the Workbench routing skeleton.
|
||||
- Provider selection by resource kind, extension/mime, `generic-text`,
|
||||
`generic-markdown`, or `notes-markdown` context, user preference, priority,
|
||||
deterministic `pluginId/providerId` fallback, and active plugin filtering.
|
||||
- `workbench.open` policy permission for plugins that request Workbench routing.
|
||||
- Draft app settings preferences for default text, markdown, and notes-context
|
||||
markdown providers.
|
||||
- `api.workbench.openResource()` and `api.workbench.editResource()` exposed to
|
||||
frontend plugin bundles.
|
||||
- Minimal Workbench host that mounts the selected provider component from the
|
||||
selected provider plugin.
|
||||
- `no-provider` fallback state when no matching provider exists.
|
||||
- `platform-test` diagnostic open provider used only to prove routing.
|
||||
|
||||
## Decision
|
||||
|
||||
Open/edit provider should be an extension of the existing contribution registry,
|
||||
not a parallel system. It should add `openProviders` beside `views`, `commands`,
|
||||
`fileActions`, and `noteActions`.
|
||||
|
||||
Capabilities remain useful for broad availability and degraded mode, but they are
|
||||
too coarse for choosing between multiple providers. Provider selection needs a
|
||||
declarative provider contribution with `supports`, priority, and component id.
|
||||
|
||||
## Correct Notes Model
|
||||
|
||||
- Notes are a contextual view over ordinary Markdown files under canonical
|
||||
`Notes/` folders.
|
||||
- `.md` inside `Notes/` opens through the selected markdown editor with
|
||||
notes-context.
|
||||
- `.md` outside `Notes/` opens through the selected markdown editor in generic
|
||||
markdown mode.
|
||||
- Plain text opens through the selected text editor provider in `generic-text`
|
||||
context.
|
||||
- Files and Notes call open/edit resource; neither embeds a concrete editor.
|
||||
- Editor provider selection belongs to Workbench/provider registry.
|
||||
- No `.verstak/notes`, no UUID note entities, no second truth separate from the
|
||||
`.md` file.
|
||||
|
||||
## Minimal Infrastructure Changes
|
||||
|
||||
1. Add `openProviders` to SDK manifest/schema/types.
|
||||
2. Add `openProviders` to desktop plugin manifest structs and contribution
|
||||
registry.
|
||||
3. Add `OpenResourceRequest` and provider support match types.
|
||||
4. Add Workbench/provider selection service with deterministic rules.
|
||||
5. Add user preferences for `defaultTextEditorProvider`,
|
||||
`defaultMarkdownEditorProvider`, and `defaultNotesMarkdownEditorProvider`.
|
||||
6. Add frontend host plumbing so Workbench can mount the selected plugin
|
||||
component.
|
||||
7. Fix `verstak-sdk/schemas/manifest.json` permissions enum for `files.*`.
|
||||
|
||||
## Proposed Milestone 6b Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- Contribution/types/schema support for `openProviders`.
|
||||
- Workbench `openResource`/`editResource` routing API.
|
||||
- Provider selection with notes context and user preferences.
|
||||
- Minimal host tab/slot for provider component mounting.
|
||||
- Diagnostic provider plugin contribution sufficient to prove routing.
|
||||
- Tests for routing, provider selection, disabled fallback, and notes-context
|
||||
markdown.
|
||||
|
||||
Out of scope:
|
||||
|
||||
- Full Files UI feature set.
|
||||
- Full Notes UI feature set.
|
||||
- Full editor implementation.
|
||||
- Real default editor plugin (Milestone 6c).
|
||||
- Files plugin open/edit integration.
|
||||
- Notes plugin open/edit integration.
|
||||
- Hardcoded core Markdown editor.
|
||||
- Watcher/sync/binary streaming/external editor.
|
||||
- Sidecar/security boundary.
|
||||
- Large rewrite.
|
||||
@@ -0,0 +1,425 @@
|
||||
# Notes/Files Plugin Architecture Plan
|
||||
|
||||
This document locks the Notes/Files/Open architecture for the next milestones.
|
||||
Files Core Service was implemented in Milestone 6a; this document still does not
|
||||
start Notes UI, Notes plugin, Files UI plugin, editor plugin, watcher, sync, or
|
||||
binary streaming.
|
||||
|
||||
## Current Readiness
|
||||
|
||||
The platform is ready for bundled plugin UI experiments. Files Core is available
|
||||
as a safe vault-scoped text file API. Notes, Files UI, and editor/viewer UI still
|
||||
need plugin-level implementations and host surfaces before real product use.
|
||||
|
||||
Already available:
|
||||
|
||||
- Plugin discovery, lifecycle, settings, capabilities, bundled commands, and
|
||||
bundled frontend events.
|
||||
- Workspace tree APIs for `space`, `case`, and `folder`.
|
||||
- Plugin-owned internal storage directories:
|
||||
`.verstak/plugin-data/<pluginId>`, `.verstak/plugin-settings/<pluginId>`, and
|
||||
`.verstak/plugin-cache/<pluginId>`.
|
||||
- Contribution registry entries for `fileActions`, `noteActions`,
|
||||
`contextMenuEntries`, `searchProviders`, `activityProviders`, and
|
||||
`statusBarItems`.
|
||||
|
||||
Not available yet:
|
||||
|
||||
- Notes plugin/API as a semantic view over Markdown files.
|
||||
- Files UI plugin.
|
||||
- Editor/viewer plugin.
|
||||
- Open/edit resource routing and provider selection.
|
||||
- UI hosts for file actions, note actions, context menus, search providers,
|
||||
activity providers, or status bar items.
|
||||
- Watcher/indexer for external filesystem changes.
|
||||
- Real plugin isolation. Current permission checks are contract/policy checks,
|
||||
not a security boundary for bundled frontend JavaScript.
|
||||
|
||||
## Canonical Notes Model
|
||||
|
||||
Notes are ordinary human-readable Markdown files inside the vault. They must be
|
||||
visible and editable outside Verstak.
|
||||
|
||||
Canonical rules:
|
||||
|
||||
- Notes are `.md` files, not opaque records.
|
||||
- Canonical folder name is exactly `Notes`.
|
||||
- Do not create lowercase `notes`.
|
||||
- Do not store user notes in `.verstak/notes/`.
|
||||
- Do not store user notes in `.verstak/plugin-data/verstak.notes/`.
|
||||
- Do not use UUID-only filenames for notes.
|
||||
- The note title is the source of truth.
|
||||
- The filename is a normalized projection from the title.
|
||||
- `RenameNote` must update both the title and the `.md` filename.
|
||||
|
||||
Canonical scoped paths:
|
||||
|
||||
- Workspace/root overview notes live under `Notes/`.
|
||||
- Case/project/folder scoped notes live under `<case-or-parent>/Notes/`.
|
||||
- The default overview note is `<case-or-parent>/Notes/Overview.md`.
|
||||
|
||||
Visibility requirements:
|
||||
|
||||
- Notes UI must show notes as semantic notes.
|
||||
- Files UI must show the same `.md` files as ordinary files.
|
||||
- External file managers must show the same `.md` files.
|
||||
- Outside Verstak, the files must remain useful as normal Markdown.
|
||||
|
||||
The workspace tree can remain `space`/`case`/`folder`. Adding `note` as a
|
||||
workspace node type is not part of the next milestone because it would require a
|
||||
schema migration. The Notes service can index and manage Markdown files inside
|
||||
canonical `Notes/` folders without changing workspace node types.
|
||||
|
||||
## Title To Filename Contract
|
||||
|
||||
The title is the source of truth. The filename is derived from the title when a
|
||||
note is created or renamed.
|
||||
|
||||
Normalization rules:
|
||||
|
||||
- Replace spaces with `_`.
|
||||
- Replace typographic dashes with `-`.
|
||||
- Allow only letters, digits, `.`, `_`, and `-`.
|
||||
- Append `.md` if the normalized name does not already end with `.md`.
|
||||
- Reject empty normalized names.
|
||||
- Preserve canonical `Notes` folder casing.
|
||||
|
||||
Examples:
|
||||
|
||||
| Title | Filename |
|
||||
|---|---|
|
||||
| `Overview` | `Overview.md` |
|
||||
| `Meeting Notes` | `Meeting_Notes.md` |
|
||||
| `Plan — Phase 1` | `Plan_-_Phase_1.md` |
|
||||
| `A/B Test: Result` | `AB_Test_Result.md` |
|
||||
|
||||
## Collision Policy
|
||||
|
||||
Same-folder collisions must not be solved silently with `_2`, `_3`, or timestamp
|
||||
suffixes.
|
||||
|
||||
`CreateNote` and `RenameNote` must return a conflict error if the normalized
|
||||
target filename already exists in the target `Notes/` folder. The UI should show a
|
||||
clear dialog or notification and ask the user to change the title.
|
||||
|
||||
Required conflict metadata:
|
||||
|
||||
- requested title;
|
||||
- normalized filename;
|
||||
- target vault-relative path;
|
||||
- existing vault-relative path.
|
||||
|
||||
## Files Service Model
|
||||
|
||||
Files service is the raw vault file layer. It works with vault-relative paths and
|
||||
does not understand note semantics.
|
||||
|
||||
Rules:
|
||||
|
||||
- All public Files API paths are canonical vault-relative slash paths.
|
||||
- Backslashes are rejected instead of normalized.
|
||||
- Absolute POSIX paths, Windows drive paths, and UNC/network paths are rejected.
|
||||
- `..` traversal is rejected.
|
||||
- Null bytes are rejected.
|
||||
- `.verstak/` is reserved case-insensitively and hidden/forbidden by default.
|
||||
- Access to `.verstak/` is allowed only through internal APIs, not through the
|
||||
normal plugin Files API.
|
||||
- Symlink read/write/move/trash operations are forbidden in Milestone 6a.
|
||||
Metadata may report a final symlink as `type: "symlink"`.
|
||||
- Writes must be atomic: write a temp file in the same directory, close it, then
|
||||
rename.
|
||||
- Delete must follow the trash policy until permanent delete is explicitly
|
||||
designed.
|
||||
- Trash metadata records `originalPath`, `deletedAt`, `originalType`, `trashId`,
|
||||
and `basename` for future restore work. Restore is deferred.
|
||||
- Binary files are deferred for write/streaming APIs. Milestone 6a lists binary
|
||||
metadata but read/write is UTF-8 text only with a 2 MB read limit.
|
||||
|
||||
Minimum Files methods:
|
||||
|
||||
- `ListVaultFiles(relativeDir)`.
|
||||
- `GetVaultFileMetadata(relativePath)`.
|
||||
- `ReadVaultTextFile(relativePath)`.
|
||||
- `WriteVaultTextFile(relativePath, content, options)`.
|
||||
- `CreateVaultFolder(relativePath)`.
|
||||
- `MoveVaultPath(fromRelativePath, toRelativePath)`.
|
||||
- `TrashVaultPath(relativePath)`.
|
||||
|
||||
Milestone 6a status: implemented in `internal/core/files` and exposed to bundled
|
||||
plugins as `api.files.list`, `api.files.metadata`, `api.files.readText`,
|
||||
`api.files.writeText`, `api.files.createFolder`, `api.files.move`, and
|
||||
`api.files.trash`. It is still text-only for reads/writes and has no watcher,
|
||||
binary streaming, external editor integration, or Files UI plugin.
|
||||
|
||||
Later Files methods:
|
||||
|
||||
- `WatchVaultFiles(scope)` once watcher/event delivery is ready.
|
||||
- `ReadVaultFileBytes` / `WriteVaultFileBytes` for binary files.
|
||||
- `OpenExternal(relativePath)` with explicit permission and UX confirmation.
|
||||
- `RevealInFileManager(relativePath)`.
|
||||
|
||||
## Notes Service Model
|
||||
|
||||
Notes API is a semantic layer over Markdown files managed by the Files/path
|
||||
policy.
|
||||
|
||||
Rules:
|
||||
|
||||
- A note physically is a `.md` file.
|
||||
- Notes API and Files API must not create two sources of truth.
|
||||
- Notes API reads/writes the same files that Files API lists.
|
||||
- The note title is the semantic source of truth and is projected to the filename.
|
||||
If frontmatter or a first-heading convention is introduced later, `RenameNote`
|
||||
must keep that visible title metadata and the filename synchronized.
|
||||
- Other note metadata should be derived from the file path and filesystem
|
||||
metadata, or from Markdown frontmatter if a future milestone introduces it.
|
||||
- If a note is changed through Files API or an external editor, the future
|
||||
watcher/indexer must observe it.
|
||||
- Until watcher/indexer exists, external changes require reload/rescan.
|
||||
|
||||
Minimum Notes methods:
|
||||
|
||||
- `ListNotes(scope)` where `scope` resolves to a canonical `Notes/` folder.
|
||||
- `GetNote(notePath)`.
|
||||
- `CreateNote(scope, title, initialBody)`.
|
||||
- `RenameNote(notePath, newTitle)`.
|
||||
- `UpdateNoteBody(notePath, body)`.
|
||||
- `TrashNote(notePath)`.
|
||||
|
||||
Later Notes methods:
|
||||
|
||||
- `SearchNotes(query, filters)`.
|
||||
- `ListBacklinks(notePath)`.
|
||||
- `ResolveNoteLinks(notePath)`.
|
||||
- `ExportNote(notePath, format)`.
|
||||
|
||||
## Notes Vs Files Relationship
|
||||
|
||||
Files owns safe raw vault file access. Notes owns note semantics.
|
||||
|
||||
The same physical note must be visible through both APIs:
|
||||
|
||||
- Files sees `SomeCase/Notes/Overview.md` as a file.
|
||||
- Notes sees `SomeCase/Notes/Overview.md` as a note with title `Overview`.
|
||||
|
||||
There must be no duplicate note content stored in plugin settings, plugin data,
|
||||
or a separate `.verstak` note database. Indexes and caches may exist later, but
|
||||
they must be rebuildable from the canonical Markdown files.
|
||||
|
||||
## Capabilities And Permissions
|
||||
|
||||
Existing permissions that remain useful:
|
||||
|
||||
- `vault.read` for existing vault-level read policy.
|
||||
- `vault.write` for existing vault-level write policy.
|
||||
- `vault.watch` for future watcher support.
|
||||
- `ui.register` for sidebar/views/settings contributions.
|
||||
- `commands.register` for bundled command handlers.
|
||||
- `events.publish` and `events.subscribe` for frontend/backend event flows.
|
||||
|
||||
New permissions required before real Notes/Files plugins ship:
|
||||
|
||||
- `files.read`
|
||||
- `files.write`
|
||||
- `files.delete`
|
||||
- `workbench.open`
|
||||
|
||||
Future Notes semantic permissions are deferred until a real Notes plugin/API
|
||||
ships:
|
||||
|
||||
- `notes.read`
|
||||
- `notes.write`
|
||||
- `notes.delete`
|
||||
|
||||
Those permissions are still policy checks until sidecar/sandbox work provides a
|
||||
real isolation boundary.
|
||||
|
||||
Recommended capabilities:
|
||||
|
||||
- `verstak/core/files/v1`
|
||||
- `verstak/core/notes/v1`
|
||||
- `verstak/files/v1` provided by the official Files plugin.
|
||||
- `verstak/notes/v1` provided by the official Notes plugin.
|
||||
|
||||
## Frontend Components And Extension Points
|
||||
|
||||
Contribution points already registered but not fully hosted:
|
||||
|
||||
- `fileActions`
|
||||
- `noteActions`
|
||||
- `contextMenuEntries`
|
||||
- `searchProviders`
|
||||
- `activityProviders`
|
||||
- `statusBarItems`
|
||||
|
||||
UI work needed:
|
||||
|
||||
- Files view host with tree/list modes and selection state.
|
||||
- Notes view host with note list, open/edit entry points, and preview/details
|
||||
region.
|
||||
- Context menu host that merges core actions with plugin contributions.
|
||||
- Command palette host for contributed commands.
|
||||
- Search provider host with cancellation/debounce and result ownership.
|
||||
- Status bar host for lightweight plugin state.
|
||||
- Selection/event model for active file, active note, and active workspace node.
|
||||
|
||||
The first implementation should host only the contribution points needed by the
|
||||
official Notes and Files plugins.
|
||||
|
||||
## Open/Edit Resource Model
|
||||
|
||||
Files and Notes must not embed a concrete editor or viewer. They request that the
|
||||
Workbench open or edit a resource. The Workbench/provider registry selects the
|
||||
plugin component.
|
||||
|
||||
Required model:
|
||||
|
||||
- Files plugin lists files and calls open/edit for a vault file.
|
||||
- Notes plugin presents Markdown files under canonical `Notes/` folders and calls
|
||||
open/edit for the same vault file with notes context.
|
||||
- `.md` or `.markdown` inside a canonical `Notes/` folder opens in markdown mode
|
||||
with notes context.
|
||||
- `.md` or `.markdown` outside `Notes/` opens in generic markdown mode.
|
||||
- Plain text opens in `generic-text` mode.
|
||||
- The same editor provider may support text, generic markdown, and notes-context
|
||||
markdown.
|
||||
- User preferences can select another provider for text, markdown, and
|
||||
notes-context markdown.
|
||||
- Community editor plugins can replace the default editor through the same
|
||||
provider registry.
|
||||
- Core desktop owns registry, routing, Workbench host slot/tab, and preferences.
|
||||
- Core desktop does not own concrete Files UI, Notes UI, Markdown editor, or file
|
||||
preview UI.
|
||||
|
||||
Minimal contribution extension:
|
||||
|
||||
```json
|
||||
{
|
||||
"contributes": {
|
||||
"openProviders": [
|
||||
{
|
||||
"id": "verstak.platform-test.markdown-diagnostic",
|
||||
"title": "Platform Test Markdown Diagnostic",
|
||||
"priority": 100,
|
||||
"component": "MarkdownDiagnosticProvider",
|
||||
"supports": [
|
||||
{
|
||||
"kind": "vault-file",
|
||||
"extensions": [".md", ".markdown"],
|
||||
"contexts": ["generic-markdown", "notes-markdown"]
|
||||
},
|
||||
{
|
||||
"kind": "vault-file",
|
||||
"mime": ["text/plain"],
|
||||
"extensions": [".txt", ".log", ".json", ".yaml", ".yml", ".toml", ".ini", ".conf"],
|
||||
"contexts": ["generic-text"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Open request shape:
|
||||
|
||||
```ts
|
||||
type OpenResourceRequest = {
|
||||
kind: "vault-file";
|
||||
path: string;
|
||||
mode?: "view" | "edit";
|
||||
mime?: string;
|
||||
extension?: string;
|
||||
context?: {
|
||||
sourcePluginId?: string;
|
||||
sourceView?: "files" | "notes" | string;
|
||||
isInsideNotesFolder?: boolean;
|
||||
notesScopePath?: string;
|
||||
notesMode?: boolean;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Provider selection rules:
|
||||
|
||||
1. Match resource kind.
|
||||
2. Match extension and/or mime.
|
||||
3. Prefer providers that explicitly support the request context.
|
||||
4. Apply user preference for text, markdown, or notes-context markdown when the
|
||||
preferred provider is enabled and still supports the resource.
|
||||
5. Otherwise choose highest priority.
|
||||
6. Break ties deterministically by plugin id, then provider id.
|
||||
7. If no provider matches, return a Workbench `no-provider` state rather than
|
||||
hardcoding a core editor.
|
||||
8. If a preferred provider plugin is disabled or unavailable, fall back to the
|
||||
deterministic default and surface a non-blocking preference warning later.
|
||||
|
||||
Initial preferences:
|
||||
|
||||
- `defaultTextEditorProvider`;
|
||||
- `defaultMarkdownEditorProvider`;
|
||||
- `defaultNotesMarkdownEditorProvider`.
|
||||
|
||||
Per-extension overrides are deferred.
|
||||
|
||||
## Migration Risks
|
||||
|
||||
- Adding `note` as a workspace node type is a workspace schema migration and is
|
||||
explicitly out of scope for the next milestone.
|
||||
- The canonical path rules must be locked before writing real files into user
|
||||
vaults.
|
||||
- Rename behavior can break external links if link rewriting is not designed.
|
||||
- Case/folder path ownership must be clear before scoped `Notes/` folders are
|
||||
created.
|
||||
- Raw Files API can expose `.verstak` internals unless reserved paths are blocked.
|
||||
- File writes need atomic behavior and conflict handling before sync.
|
||||
- Large/binary files require streaming or byte APIs; text APIs are not enough.
|
||||
- External editor changes require reload/rescan until watcher/indexer exists.
|
||||
- Bundled frontend plugins are trusted/cooperative and not isolated from the
|
||||
shared JS context.
|
||||
|
||||
## Test Plan
|
||||
|
||||
Backend Go tests:
|
||||
|
||||
- Vault-relative path normalization and traversal rejection.
|
||||
- Reserved `.verstak` path behavior.
|
||||
- Files list/read/write/mkdir/move/trash with vault closed/open states.
|
||||
- Atomic text writes and temp-file cleanup on failure.
|
||||
- Notes `Notes/` folder casing and no lowercase `notes`.
|
||||
- Title to filename normalization.
|
||||
- `CreateNote` and `RenameNote` conflict errors without silent suffixes.
|
||||
- Notes and Files read the same physical `.md` file.
|
||||
- Permission checks for `files.*`, `notes.*`, `vault.read`, and `vault.write`.
|
||||
|
||||
Frontend/unit tests:
|
||||
|
||||
- SDK and plugin API shape for Files and Notes draft methods.
|
||||
- Readable errors for missing permissions, closed vault, missing file, missing
|
||||
note, reserved path, and collision.
|
||||
- Contribution host rendering for note/file actions and context menus.
|
||||
|
||||
Playwright e2e tests:
|
||||
|
||||
- Create a text file, reload, and verify it is visible in Files.
|
||||
- Create a note, reload, and verify it is visible in both Notes and Files.
|
||||
- Rename a note and verify title plus filename change together.
|
||||
- Attempt same-folder collision and verify user-facing conflict handling.
|
||||
- External file change requires reload/rescan until watcher exists.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Define canonical vault-relative path rules and reserved path policy.
|
||||
2. Implement Files core service with safe list/read/write/mkdir/move/trash.
|
||||
3. Define open/edit resource request, provider contribution shape, and provider
|
||||
selection rules.
|
||||
4. Extend contribution registry/types with `openProviders`.
|
||||
5. Add Workbench open/edit routing and a provider-hosted tab/slot.
|
||||
6. Add preferences for text, markdown, and notes-context markdown provider ids.
|
||||
7. Use `platform-test` diagnostic provider to verify routing; real default
|
||||
editor plugin is deferred to Milestone 6c.
|
||||
8. Build official Files plugin that calls open/edit resource.
|
||||
9. Build official Notes plugin as a contextual view over Markdown files in
|
||||
canonical `Notes/` folders.
|
||||
10. Implement future Notes semantic helpers only as a facade over Markdown files,
|
||||
never as a second source of truth.
|
||||
+227
-12
@@ -6,19 +6,34 @@
|
||||
|
||||
### Discovery Directories
|
||||
|
||||
Plugins ищутся в двух директориях (порядок приоритета):
|
||||
Plugins ищутся через единый resolver `internal/core/plugin.ResolveDiscoveryDirs`.
|
||||
Порядок приоритета:
|
||||
|
||||
| Путь | Назначение | Коммитится |
|
||||
|---|---|---|
|
||||
| `VERSTAK_PLUGIN_DIR` | Override для тестов/dev; можно передать несколько путей через OS path separator | Нет |
|
||||
| `./plugins/` | Dev plugins относительно текущей рабочей директории/repo | Нет (`.gitignore`) |
|
||||
| `<binary-dir>/plugins/` | Packaged plugins рядом с desktop binary | Зависит от дистрибутива |
|
||||
| `~/.config/verstak/plugins/` | User-installed plugins | Нет (user home) |
|
||||
| `./plugins/` | Bundled / dev plugins | Нет (`.gitignore`) |
|
||||
|
||||
Resolver нормализует пути, удаляет дубликаты и передает discovery только канонический
|
||||
список директорий. Отсутствующие директории просто пропускаются на этапе scanning.
|
||||
|
||||
Discovery сканирует **все** resolved директории в указанном порядке. Если один и тот
|
||||
же `plugin.id` найден несколько раз, применяется правило **first plugin wins**:
|
||||
первый найденный plugin загружается, последующие plugins с тем же id пропускаются.
|
||||
Конфликт логируется и возвращается как discovery warning с двумя путями: путь
|
||||
пропущенного duplicate и путь уже загруженного winner.
|
||||
|
||||
### ./plugins/ как Dev/Install Target
|
||||
|
||||
Директория `./plugins/` в корне `verstak-desktop` используется как:
|
||||
Директория `./plugins/` от текущей рабочей директории используется как:
|
||||
|
||||
- **Dev target** — `install-dev-plugins.sh` коприрует сюда собранные пакеты из `verstak-official-plugins/dist/`.
|
||||
- **Bundled plugins** — при дистрибутиве core может поставлять плагины здесь.
|
||||
- **Local override** — при запуске desktop из repo позволяет быстро проверять packaged bundles.
|
||||
|
||||
В packaged-сборке bundled plugins должны лежать в `plugins/` рядом с executable.
|
||||
Для тестов и локальных сценариев можно задать `VERSTAK_PLUGIN_DIR=/path/to/plugins`.
|
||||
|
||||
Директория **не коммитится**. Каждый разработчик устанавливает плагины через `install-dev-plugins.sh`.
|
||||
|
||||
@@ -83,6 +98,8 @@ coreCaps := []string{
|
||||
"verstak/core/contribution-registry/v1",
|
||||
"verstak/core/permissions/v1",
|
||||
"verstak/core/events/v1",
|
||||
"verstak/core/files/v1",
|
||||
"verstak/core/workbench/v1",
|
||||
}
|
||||
capRegistry.Register("verstak-desktop", coreCaps)
|
||||
|
||||
@@ -150,7 +167,7 @@ foreach plugin:
|
||||
"provides": ["verstak/platform-test/v1"],
|
||||
"requires": ["verstak/core/plugin-manager/v1"],
|
||||
"optionalRequires": ["verstak/core/vault/v1", "verstak/core/sync/v1"],
|
||||
"permissions": ["vault.read", "events.publish", "ui.register"],
|
||||
"permissions": ["vault.read", "events.publish", "ui.register", "workbench.open"],
|
||||
"frontend": { "entry": "frontend/dist/index.js" },
|
||||
"contributes": {
|
||||
"views": [{ "id": "my.view", "title": "My View", "component": "MyPanel" }],
|
||||
@@ -171,6 +188,7 @@ foreach plugin:
|
||||
| Основные панели | `views` | Полноценные страницы/панели | ✅ ViewContainer.svelte (PluginBundleHost — real frontend bundle) |
|
||||
| Панели настроек | `settingsPanels` | Панели в Plugin Manager | ✅ PluginManager.svelte (кнопка Settings, открывает modal) |
|
||||
| Команды | `commands` | Команды для command palette | ✅ ContributionRegistry (UI command palette не реализован) |
|
||||
| Open/edit providers | `openProviders` | Провайдеры viewer/editor для Workbench routing | ✅ ContributionRegistry + минимальный Workbench host |
|
||||
|
||||
### Планируемые contribution points
|
||||
|
||||
@@ -233,6 +251,176 @@ foreach plugin:
|
||||
5. Enable plugin → `Register` при следующем Reload
|
||||
6. Registry idempotent: Register удаляет старые записи перед добавлением новых
|
||||
|
||||
## Bundled Frontend Plugin API
|
||||
|
||||
Bundled frontend plugins получают API от host через `createPluginAPI(pluginId)`.
|
||||
Обычный plugin code не передает `pluginId` в методы API: scope закрепляется в
|
||||
host при mount компонента. Это защищает нормальный cooperative path от случайного
|
||||
доступа к чужому namespace.
|
||||
|
||||
Текущая модель безопасности честно ограничена:
|
||||
|
||||
- bundled frontend plugins исполняются в общем JS-контексте приложения;
|
||||
- проверки permissions/capabilities сейчас являются contract/policy checks, а не
|
||||
полноценной security boundary;
|
||||
- malicious JS в общем контексте теоретически может обойти frontend wrapper;
|
||||
- настоящая изоляция будет только после отдельного sidecar/sandbox milestone.
|
||||
|
||||
## Workbench Open/Edit Routing
|
||||
|
||||
Files and Notes plugins do not import or embed a concrete editor plugin. They
|
||||
call `api.workbench.openResource(request)` or `api.workbench.editResource(request)`.
|
||||
The backend requires the source plugin to be enabled, loaded/degraded, and to
|
||||
declare `workbench.open`. This is a policy/contract check, not a security
|
||||
boundary.
|
||||
|
||||
`OpenResourceRequest`:
|
||||
|
||||
```ts
|
||||
type OpenResourceRequest = {
|
||||
kind: "vault-file";
|
||||
path: string;
|
||||
mode?: "view" | "edit";
|
||||
mime?: string;
|
||||
extension?: string;
|
||||
context?: {
|
||||
sourcePluginId?: string;
|
||||
sourceView?: "files" | "notes" | string;
|
||||
isInsideNotesFolder?: boolean;
|
||||
notesScopePath?: string;
|
||||
notesMode?: boolean;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Routing contexts are fixed as `generic-text`, `generic-markdown`, and
|
||||
`notes-markdown`. `.md`/`.markdown` inside canonical `Notes/` folders uses
|
||||
`notes-markdown`; markdown outside Notes uses `generic-markdown`; ordinary text
|
||||
uses `generic-text`. Milestone 6b derives context from request fields; future
|
||||
Files/Notes integrations can centralize canonical Notes folder auto-detection in
|
||||
the Workbench helper.
|
||||
|
||||
`contributes.openProviders` extends the existing contribution registry:
|
||||
|
||||
```json
|
||||
{
|
||||
"contributes": {
|
||||
"openProviders": [
|
||||
{
|
||||
"id": "verstak.platform-test.markdown-diagnostic",
|
||||
"title": "Platform Test Markdown Diagnostic",
|
||||
"priority": 100,
|
||||
"component": "MarkdownDiagnosticProvider",
|
||||
"supports": [
|
||||
{
|
||||
"kind": "vault-file",
|
||||
"extensions": [".md", ".markdown"],
|
||||
"contexts": ["generic-markdown", "notes-markdown"]
|
||||
},
|
||||
{
|
||||
"kind": "vault-file",
|
||||
"mime": ["text/plain"],
|
||||
"extensions": [".txt", ".log"],
|
||||
"contexts": ["generic-text"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Selection uses enabled loaded/degraded provider plugins, resource kind,
|
||||
extension/mime, context, user preference, priority, then deterministic
|
||||
`pluginId/providerId` fallback. If nothing matches, Workbench returns
|
||||
`status: "no-provider"` and shows the fallback view instead of a core editor.
|
||||
|
||||
Draft app-global preferences are `defaultTextEditorProvider`,
|
||||
`defaultMarkdownEditorProvider`, and `defaultNotesMarkdownEditorProvider`.
|
||||
Vault-scoped and per-extension overrides are deferred.
|
||||
|
||||
### API methods
|
||||
|
||||
`settings`
|
||||
|
||||
- `settings.read()` — читает весь settings namespace текущего plugin.
|
||||
- `settings.read(key)` — читает один ключ.
|
||||
- `settings.write(key, value)` — обновляет один ключ и пишет namespace обратно.
|
||||
- `settings.writeAll(settings)` — заменяет settings namespace.
|
||||
- Backend требует plugin exists, enabled, status `loaded`/`degraded` и permission
|
||||
`storage.namespace`.
|
||||
|
||||
`capabilities`
|
||||
|
||||
- `capabilities.list()` — возвращает текущий capability registry.
|
||||
- `capabilities.get(name)` — возвращает `{ available, name, pluginId, status }`.
|
||||
- `capabilities.has(name)` — boolean wrapper над `get`.
|
||||
- Backend требует, чтобы plugin был enabled/loaded и декларировал dependency на
|
||||
`verstak/core/capability-registry/v1` в `requires` или `optionalRequires`.
|
||||
|
||||
`commands`
|
||||
|
||||
- `commands.register(commandId, handler)` — регистрирует bundled frontend handler.
|
||||
Возвращает `Promise<unsubscribe>`.
|
||||
- `commands.execute(commandId, args)` — backend сначала проверяет plugin status,
|
||||
permission `commands.register` и что command объявлен в `contributes.commands`
|
||||
именно этим plugin. Затем frontend registry вызывает зарегистрированный handler.
|
||||
- Если command объявлен в manifest, но handler не зарегистрирован, API возвращает
|
||||
понятную ошибку `declared-but-unhandled`.
|
||||
- Handler registry очищается при component unmount, reload/disable flow и
|
||||
`api.dispose()`.
|
||||
|
||||
`events`
|
||||
|
||||
- `events.subscribe(eventName, handler)` — frontend-local subscription с backend
|
||||
validation permission `events.subscribe`. Возвращает `Promise<unsubscribe>`.
|
||||
- `events.publish(eventName, payload)` — backend проверяет `events.publish`, затем
|
||||
событие dispatch'ится в bundled frontend event bus.
|
||||
- Handler получает envelope `{ name, pluginId, payload, timestamp }`.
|
||||
- Subscriptions очищаются при component unmount, reload/disable flow и
|
||||
`api.dispose()`.
|
||||
|
||||
`files`
|
||||
|
||||
- `files.list(relativeDir)` — list directory using a vault-relative path.
|
||||
- `files.metadata(relativePath)` — returns file/folder/symlink metadata.
|
||||
- `files.readText(relativePath)` — reads a UTF-8 regular file, with a size limit.
|
||||
- `files.writeText(relativePath, content, options)` — atomically writes text via
|
||||
temp-file-and-rename. `options.createIfMissing` and `options.overwrite`
|
||||
control conflicts.
|
||||
- `files.createFolder(relativePath)` — creates one folder when the parent exists.
|
||||
- `files.move(from, to, options)` — moves a file or folder; rejects moving a
|
||||
folder into itself and conflicts unless `options.overwrite` is true.
|
||||
- `files.trash(relativePath)` — moves a file/folder into internal
|
||||
`.verstak/trash/files/<trashId>/...` and returns trash metadata.
|
||||
- Backend requires plugin exists, enabled, status `loaded`/`degraded`, open
|
||||
vault, and `files.read`, `files.write`, or `files.delete`.
|
||||
- All paths are canonical vault-relative slash paths. Backslashes, POSIX
|
||||
absolute paths, Windows drive paths, UNC/network paths, `..`, null bytes,
|
||||
symlink traversal, and public access to `.verstak/` are rejected.
|
||||
- `.verstak` is reserved case-insensitively: `.verstak`, `.Verstak`, and any
|
||||
first path segment with that spelling are internal-only.
|
||||
- `files.metadata` may report a final symlink as `type: "symlink"`, but
|
||||
`files.list` through a symlink directory and all read/write/move/trash
|
||||
operations through symlinks are forbidden in Milestone 6a.
|
||||
- Files API is text-only for read/write in Milestone 6a. `readText` is limited
|
||||
to UTF-8 regular files up to 2 MB. Binary streaming, watcher, restore,
|
||||
external editor integration, and Files UI plugin are deferred.
|
||||
|
||||
`dispose`
|
||||
|
||||
- `dispose()` вызывается host'ом при cleanup. Plugin code обычно не вызывает его
|
||||
напрямую. Он удаляет зарегистрированные command handlers и event subscriptions.
|
||||
|
||||
### Runtime boundaries
|
||||
|
||||
| Layer | Current status |
|
||||
|---|---|
|
||||
| Bundled frontend runtime | Functional for settings, capabilities, commands, events and text Files API |
|
||||
| Backend validation | Checks plugin exists, enabled/loaded state, permissions and declarations |
|
||||
| Security boundary | Not implemented; bundled plugins share the desktop frontend JS context |
|
||||
| Sidecar/RPC/sandbox | Not implemented |
|
||||
|
||||
### Error boundary
|
||||
|
||||
- Ошибка в plugin view/settings placeholder не роняет shell
|
||||
@@ -265,17 +453,44 @@ window.VerstakPluginRegister('plugin.id', {
|
||||
|
||||
### VerstakPluginAPI
|
||||
|
||||
API объект передаётся в `mount()` и содержит только ограниченный набор методов:
|
||||
API объект передаётся в `mount()` и содержит plugin-scoped методы текущего
|
||||
bundled runtime. Это реальный runtime contract для cooperative bundled plugins,
|
||||
но не sandbox/security boundary.
|
||||
|
||||
| Свойство | Статус | Описание |
|
||||
|---|---|---|
|
||||
| `api.pluginId` | ✅ Работает | ID плагина |
|
||||
| `api.capabilities.has(id)` | 🔧 Stub | Запрос capability registry (planned) |
|
||||
| `api.events.publish(type, payload)` | 🔧 Stub | Публикация события (planned) |
|
||||
| `api.events.subscribe(type, handler)` | 🔧 Stub | Подписка на события (planned) |
|
||||
| `api.settings.read(key)` | 🔧 Stub | Чтение настроек плагина (planned) |
|
||||
| `api.settings.write(key, value)` | 🔧 Stub | Запись настроек плагина (planned) |
|
||||
| `api.commands.execute(id, args)` | 🔧 Stub | Выполнение команды (planned) |
|
||||
| `api.settings.read(key?)` | ✅ Работает | Читает plugin-scoped settings через backend bridge |
|
||||
| `api.settings.write(key, value)` | ✅ Работает | Пишет один settings key через backend bridge |
|
||||
| `api.settings.writeAll(settings)` | ✅ Работает | Заменяет settings namespace плагина |
|
||||
| `api.capabilities.list()` | ✅ Работает | Возвращает capability registry |
|
||||
| `api.capabilities.get(id)` | ✅ Работает | Возвращает capability entry/status |
|
||||
| `api.capabilities.has(id)` | ✅ Работает | Boolean wrapper над `get` |
|
||||
| `api.commands.register(id, handler)` | ✅ Работает | Регистрирует bundled frontend handler для объявленной command |
|
||||
| `api.commands.execute(id, args)` | ✅ Работает | Валидирует declaration/permission/backend state и вызывает bundled handler |
|
||||
| `api.events.publish(type, payload)` | ✅ Работает | Валидирует permission и публикует во frontend event bus |
|
||||
| `api.events.subscribe(type, handler)` | ✅ Работает | Валидирует permission и подписывает handler на frontend event bus |
|
||||
| `api.files.list(relativeDir)` | ✅ Работает | Список vault-relative директории, `.verstak` скрыта |
|
||||
| `api.files.metadata(relativePath)` | ✅ Работает | Metadata для файла/папки/symlink без чтения содержимого |
|
||||
| `api.files.readText(relativePath)` | ✅ Работает | Читает UTF-8 regular file до 2 MB |
|
||||
| `api.files.writeText(relativePath, content, options)` | ✅ Работает | Atomic text write с явным create/overwrite policy |
|
||||
| `api.files.createFolder(relativePath)` | ✅ Работает | Создаёт vault-relative folder |
|
||||
| `api.files.move(from, to, options)` | ✅ Работает | Move file/folder с conflict и path-policy checks |
|
||||
| `api.files.trash(relativePath)` | ✅ Работает | Перемещает в internal trash, permanent delete нет |
|
||||
| `api.workbench.openResource(request)` | ✅ Работает | Routes vault resources to `openProviders` |
|
||||
| `api.workbench.editResource(request)` | ✅ Работает | Same routing, forcing `mode: "edit"` |
|
||||
| `api.dispose()` | ✅ Работает | Очищает command handlers и event subscriptions текущего API instance |
|
||||
|
||||
Ограничения:
|
||||
|
||||
- permissions/capabilities checks являются contract/policy checks;
|
||||
- bundled frontend plugins исполняются в общем JS-контексте;
|
||||
- malicious JS не изолирован;
|
||||
- sidecar process lifecycle, RPC transport и sandbox enforcement ещё не
|
||||
реализованы.
|
||||
- Files paths are slash-only vault-relative contract paths; backslashes,
|
||||
Windows absolute paths, UNC paths, `.verstak` variants, traversal and symlink
|
||||
operations are rejected by backend policy checks.
|
||||
|
||||
### Загрузка бандла
|
||||
|
||||
|
||||
Reference in New Issue
Block a user