From 1ca759cc2e509c0da6a63518994dc5a810d078f4 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Sat, 18 Jul 2026 18:06:44 +0800 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=86=D0=B8=D1=8F:=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20milestone-=D0=BF=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD=D1=8B,=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D1=91=D0=BD=20PLUGIN=5FRUNTIME=20=D0=B8=20NOTES=5FFILES=5FPLUG?= =?UTF-8?q?IN=5FPLAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/MILESTONE_6A_FILES_CORE_PLAN.md | 403 ------------------ docs/MILESTONE_6B_PLAN.md | 148 ------- docs/MILESTONE_6C_DEFAULT_EDITOR_PLAN.md | 65 --- docs/MILESTONE_6D_FILES_PLUGIN_PLAN.md | 64 --- docs/NOTES_FILES_PLUGIN_PLAN.md | 49 +-- docs/PLUGIN_RUNTIME.md | 11 + ...026-07-14-native-notifications-and-tray.md | 350 --------------- ...14-native-notifications-and-tray-design.md | 182 -------- 8 files changed, 31 insertions(+), 1241 deletions(-) delete mode 100644 docs/MILESTONE_6A_FILES_CORE_PLAN.md delete mode 100644 docs/MILESTONE_6B_PLAN.md delete mode 100644 docs/MILESTONE_6C_DEFAULT_EDITOR_PLAN.md delete mode 100644 docs/MILESTONE_6D_FILES_PLUGIN_PLAN.md delete mode 100644 docs/superpowers/plans/2026-07-14-native-notifications-and-tray.md delete mode 100644 docs/superpowers/specs/2026-07-14-native-notifications-and-tray-design.md diff --git a/docs/MILESTONE_6A_FILES_CORE_PLAN.md b/docs/MILESTONE_6A_FILES_CORE_PLAN.md deleted file mode 100644 index 064463f..0000000 --- a/docs/MILESTONE_6A_FILES_CORE_PLAN.md +++ /dev/null @@ -1,403 +0,0 @@ -# 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//...` 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` - -- [x] Add `NormalizeVaultRelativePath(relative string) (string, error)`. -- [x] Reject absolute paths, null bytes, `..`, and empty file paths. -- [x] Preserve path case, including canonical `Notes`. -- [x] Add `IsReservedPath(relative string) bool` returning true for `.verstak` - and `.verstak/...`. -- [x] Add tests: - `TestNormalizeRejectsAbsolutePath`, - `TestNormalizeRejectsTraversal`, - `TestNormalizeRejectsNullByte`, - `TestNormalizePreservesCase`, - `TestReservedPathPolicy`. -- [x] 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` - -- [x] Define `Service` with a vault dependency that can return the current vault - root and status. -- [x] Implement `List(relativeDir string) ([]Entry, error)`. -- [x] Implement `Metadata(relativePath string) (Entry, error)`. -- [x] Implement `ReadText(relativePath string) (string, error)`. -- [x] Implement `WriteText(relativePath, content string, overwrite bool) (Entry, error)`. -- [x] Implement `Mkdir(relativePath string) (Entry, error)`. -- [x] Implement `Move(fromRelativePath, toRelativePath string, overwrite bool) (Entry, error)`. -- [x] Implement `Trash(relativePath string) (Entry, error)`. -- [x] Use the shared path policy for every public method. -- [x] Block `.verstak` paths in every public method. -- [x] Add tests for closed vault, list, metadata, text read/write, mkdir, move, - trash, overwrite false conflict, overwrite true replace, and reserved path - rejection. -- [x] 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` - -- [x] Write text content to a temp file in the target directory. -- [x] Rename the temp file into the final path only after successful write. -- [x] Remove temp file on write failure. -- [x] Add test `TestWriteTextIsAtomicOnFailure` using a controlled failing path - or permission-denied directory. -- [x] Add test `TestWriteTextDoesNotLeaveTempFile`. -- [x] 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` - -- [x] Register permissions: `files.read`, `files.write`, `files.delete`. -- [x] Register core capability `verstak/core/files/v1` when vault services are - initialized. -- [x] Add API guard tests proving each Files bridge method rejects plugins that - are missing the required permission. -- [x] 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` - -- [x] Add `files.Service` to `api.App`. -- [x] Add plugin-scoped methods listed in "Public Backend Shape". -- [x] Use `requirePluginAccess(pluginID, permission)` for every method. -- [x] Return readable errors for closed vault, missing file, reserved path, - conflict, and missing permission. -- [x] Add tests for successful read/write/list/mkdir/move/trash through `App`. -- [x] 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. - -- [x] Add `api.files.list(relativeDir)`. -- [x] Add `api.files.metadata(relativePath)`. -- [x] Add `api.files.readText(relativePath)`. -- [x] Add `api.files.writeText(relativePath, content, options)`. -- [x] Add `api.files.mkdir(relativePath)`. -- [x] Add `api.files.move(fromRelativePath, toRelativePath, options)`. -- [x] Add `api.files.trash(relativePath)`. -- [x] Keep all calls plugin-scoped; plugin code must not pass `pluginId`. -- [x] Mock readable errors for reserved path and missing permission. -- [x] 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` - -- [x] Add `files` API TypeScript interfaces matching the frontend API names. -- [x] Add mock Files API methods in `createMockPluginAPI`. -- [x] Add contract tests for API shape, text write/read, reserved path error, and - trash result shape. -- [x] 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` - -- [x] Document Files Core API as functional for Milestone 6a. -- [x] Keep Notes API documented as planned until Milestone 6b or later. -- [x] Document `.verstak` reserved path policy. -- [x] Document slash-only path policy, Windows/UNC rejection, and symlink policy. -- [x] Document text-only write support and deferred binary streaming. - -## Task 9: Final Verification - -- [x] Run desktop backend tests: - -```bash -cd verstak-desktop -go test ./... -``` - -- [x] Run desktop frontend build: - -```bash -cd verstak-desktop/frontend -npm run build -``` - -- [x] Run desktop e2e: - -```bash -cd verstak-desktop/frontend -npm run test:e2e -- --reporter=list -``` - -- [x] Run official plugins checks: - -```bash -cd verstak-official-plugins -./scripts/check.sh -./scripts/build.sh -``` - -- [x] 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. diff --git a/docs/MILESTONE_6B_PLAN.md b/docs/MILESTONE_6B_PLAN.md deleted file mode 100644 index e7bd087..0000000 --- a/docs/MILESTONE_6B_PLAN.md +++ /dev/null @@ -1,148 +0,0 @@ -# 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. - -## 6b-fix: Infrastructure Gaps Closed - -Post-review fixes before Milestone 6c: - -- Added `openProviders` to `verstak-sdk/schemas/contributions.json` contribution points. -- Added provider matching tests: text/markdown preference, MIME matching, extension - case-insensitivity, multiple supports entries, kind mismatch. -- Added disabled provider exclusion test in `api/app_test.go`. -- Fixed SDK `build.sh`/`test.sh` to detect incomplete `node_modules`. -- Documented disabled provider lifecycle: contributions remain in registry until - ReloadPlugins; `activeOpenProviders()` filters disabled/unloaded at request time. -- Installed Playwright browsers for E2E tests. diff --git a/docs/MILESTONE_6C_DEFAULT_EDITOR_PLAN.md b/docs/MILESTONE_6C_DEFAULT_EDITOR_PLAN.md deleted file mode 100644 index f78c2a6..0000000 --- a/docs/MILESTONE_6C_DEFAULT_EDITOR_PLAN.md +++ /dev/null @@ -1,65 +0,0 @@ -# Milestone 6c — Default Editor Plugin - -## Goal - -Create the official Default Editor Plugin as an openProvider for text, generic -markdown, and notes-context markdown files. Core desktop does not own or import -the editor component. - -## What was built - -### Plugin: `verstak.default-editor` - -Location: `verstak-official-plugins/plugins/default-editor/` - -**Manifest declares 3 openProviders:** - -| Provider ID | Context | Extensions | -|-------------|---------|------------| -| `verstak.default-editor.text` | `generic-text` | `.txt`, `.log`, `.conf`, `.ini`, `.toml`, `.yaml`, `.yml`, `.json`, `.csv` | -| `verstak.default-editor.markdown` | `generic-markdown` | `.md`, `.markdown` | -| `verstak.default-editor.notes-markdown` | `notes-markdown` | `.md`, `.markdown` | - -All providers use the same `DefaultEditor` component (unified, not 3 separate editors). - -**Permissions:** `files.read`, `files.write`, `workbench.open` - -**Capabilities required:** `verstak/core/files/v1`, `verstak/core/workbench/v1` - -### Frontend component: `DefaultEditor` - -- **Modes:** text (textarea), generic-markdown (editor + preview), notes-markdown (editor + preview + notes badge) -- **File loading:** `api.files.readText(path)` with loading/error states -- **Saving:** `api.files.writeText(path, content, { overwrite: true })` with dirty/saved/error states -- **Keyboard:** Ctrl+S / Cmd+S save, Tab indentation -- **Markdown preview:** Simple renderer (no raw HTML, no script injection) -- **Notes context:** Badge + info bar, no separate note entity, no `.verstak/notes` - -## Verification - -- `go test ./...` — PASS -- `go vet ./...` — PASS -- `npm run build` (frontend) — PASS -- `npm run test:e2e` — 28/28 PASS (20 existing + 8 new) -- `scripts/check.sh` (official-plugins) — PASS -- `scripts/build.sh` (official-plugins) — PASS -- SDK checks — PASS - -## Manual testing - -Until Files UI plugin exists, use platform-test diagnostics panel to manually -open files via workbench: click "Open Text Diagnostic", "Open Markdown Diagnostic", -or "Open Notes Diagnostic" buttons. These call `api.workbench.editResource` and -route through the default-editor provider. - -## Deferred - -- CodeMirror/Monaco editor -- Backlinks, internal link navigation -- Secret widgets -- Image asset pipeline -- Files UI plugin -- Notes UI plugin -- Watcher/sync -- External open -- Sidecar/security isolation diff --git a/docs/MILESTONE_6D_FILES_PLUGIN_PLAN.md b/docs/MILESTONE_6D_FILES_PLUGIN_PLAN.md deleted file mode 100644 index 2a109a8..0000000 --- a/docs/MILESTONE_6D_FILES_PLUGIN_PLAN.md +++ /dev/null @@ -1,64 +0,0 @@ -# Milestone 6d — Minimal Files Plugin - -## Goal - -Create a minimal Files plugin that shows vault files/folders and opens files -through Workbench openResource. No editor embedded. - -## What was built - -### Plugin: `verstak.files` - -Location: `verstak-official-plugins/plugins/files/` - -**Contributions:** -- views: `verstak.files.view` → `FilesView` component -- No sidebarItems — Files is not a global sidebar item - -**Permissions:** `files.read`, `files.write`, `workbench.open`, `ui.register` - -### Files View - -- Root listing on mount -- Folder navigation (double-click) -- File open via `api.workbench.openResource()` -- Breadcrumb navigation -- Create folder/file buttons -- Refresh button -- Loading/error/empty states -- `.verstak` filtered out - -### Provider priority - -- default-editor: priority 50 -- platform-test diagnostic: priority 10 -- default-editor wins for normal file opens - -### Bundle fix - -Fixed missing opening quote in STYLES string (`.files-empty` → `'.files-empty'`). -Added automated bundle execution check to `scripts/check.sh`. - -## 6d-hotfix - -- Removed sidebarItems from Files plugin (Files is not a global sidebar item) -- Added `[frontend bundle execution]` check to `check.sh` — verifies all plugin - bundles parse via `new Function()` and register via `VerstakPluginRegister` -- Updated E2E tests: Files no longer expected in global sidebar -- Documented: sidebarItems are global shell navigation, not workspace template tabs - -## Verification - -- `go test ./...` — PASS -- `go vet ./...` — PASS -- `npm run build` — PASS -- `npm run test:e2e` — 34/34 PASS -- Official plugins — 3 plugins built, bundle execution check passes -- SDK — 11/11 tests pass - -## Deferred - -- Notes plugin, rename/move/trash UI, drag-and-drop, context menu, - watcher/inotify, sync, external open, binary streaming, sidecar/security, - workspace template host (Milestone 6d2) - diff --git a/docs/NOTES_FILES_PLUGIN_PLAN.md b/docs/NOTES_FILES_PLUGIN_PLAN.md index dbf4997..5bf11d8 100644 --- a/docs/NOTES_FILES_PLUGIN_PLAN.md +++ b/docs/NOTES_FILES_PLUGIN_PLAN.md @@ -1,39 +1,30 @@ # 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. +This document captures the architectural decisions for Notes, Files, and +Workbench routing. Most items described below are now implemented; this +document remains as a reference for the canonical data model and rules. ## 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. +All key components are implemented: -Already available: - -- Plugin discovery, lifecycle, settings, capabilities, bundled commands, and - bundled frontend events. -- Workspace lifecycle APIs for top-level physical folders under the vault root. -- Plugin-owned internal storage directories: - `.verstak/plugin-data/`, `.verstak/plugin-settings/`, and - `.verstak/plugin-cache/`. -- 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. +- Files Core API with safe vault-scoped text and binary file operations. +- Notes plugin as a semantic view over canonical `Notes/` Markdown files. +- Files UI plugin with folder navigation and Workbench-based file opening. +- Default Editor plugin providing text/markdown openProviders. +- File Preview plugin for images. +- Workbench open/edit routing with 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. + activity providers, and status bar items. +- File watcher for external filesystem changes. +- Trash plugin for restore and permanent delete. + +Not yet available: + +- Real plugin isolation (bundled plugins run in shared JS context; + sidecar/sandbox is a future milestone). +- Chunked streaming/large-file import (bounded to 2 MB text / 8 MB bytes). +- Production-grade auto-update and release channel. ## Canonical Notes Model diff --git a/docs/PLUGIN_RUNTIME.md b/docs/PLUGIN_RUNTIME.md index cbebef8..4a94d5e 100644 --- a/docs/PLUGIN_RUNTIME.md +++ b/docs/PLUGIN_RUNTIME.md @@ -729,6 +729,17 @@ not-created ──CreateVault──▶ open ──CloseVault──▶ closed | `internal/core/permissions/registry.go` | PermissionsRegistry | | `internal/core/events/bus.go` | EventBus | | `internal/api/app.go` | Wails API, ReloadPlugins | +| `internal/core/files/` | Files Core API: safe path policy, text/binary read/write, move, trash | +| `internal/core/filewatcher/` | Live filesystem watcher for external changes | +| `internal/core/workbench/` | Workbench open/edit provider routing | +| `internal/core/workspace/` | Workspace manager: create, rename, trash, templates, identity | +| `internal/core/notes/` | Notes file utilities (title↔filename, conflict detection) | +| `internal/core/sync/` | Sync scanner, snapshot, reconciliation, operation recording | +| `internal/core/secrets/` | AES-GCM secret store | +| `internal/core/browserreceiver/` | Local HTTP receiver for browser extension | +| `internal/core/notifications/` | Native desktop notifications | +| `internal/core/hostname/` | Hostname normalization for browser domain activity | +| `internal/core/externalopen/` | Platform-specific external file/folder opening | | `internal/core/vault/vault.go` | Vault service: CreateVault, OpenVault, CloseVault, ResolveSafePath, plugin namespace paths | || `internal/core/vault/vault_test.go` | Vault tests: layout creation, open/close, path traversal, events | || `internal/core/storage/api.go` | Plugin storage API: settings/data/cache JSON with namespace isolation | diff --git a/docs/superpowers/plans/2026-07-14-native-notifications-and-tray.md b/docs/superpowers/plans/2026-07-14-native-notifications-and-tray.md deleted file mode 100644 index aab7b0b..0000000 --- a/docs/superpowers/plans/2026-07-14-native-notifications-and-tray.md +++ /dev/null @@ -1,350 +0,0 @@ -# Native Notifications, Tray, and Public README Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Deliver native Todo reminders and Windows/Linux tray operation, then publish updated alpha artifacts and public README documentation with real application screenshots. - -**Architecture:** Desktop core stores and delivers plugin-owned notification schedules and controls the tray lifecycle. Todo uses the public plugin API to replace its desired reminders. A small core tray adapter keeps the Wails process alive after window close. Documentation uses the supplied English and Russian README sources plus screenshots from an actual test vault. - -**Tech Stack:** Go 1.24, Wails v2.12 runtime notifications, `fyne.io/systray`, plain JavaScript plugin API, Node smoke tests, Playwright, bash packaging. - -## Global constraints - -- Support Windows and Linux only; no background daemon after an explicit Quit. -- Closing a window hides it only after the tray reports ready; otherwise normal - window close ends the process. -- `verstak.todo` requires `verstak/core/notifications/v1` and `notifications.schedule`. -- Plugins cannot receive Wails runtime access. -- Keep Wails generated bindings out of commits. -- Commit each independent change and immediately push it to GitHub and `mirror`. - ---- - -### Task 1: Persisted notification scheduler - -**Files:** -- Create: `internal/core/notifications/manager.go` -- Create: `internal/core/notifications/store.go` -- Create: `internal/core/notifications/manager_test.go` - -**Interfaces:** -- `type Request struct { ID, DueAt, Title, Body string }` -- `type Item struct { PluginID, ID, DueAt, Title, Body, SentForDueAt string }` -- `type Sender interface { Send(context.Context, Item) error }` -- `Manager.Replace(pluginID string, requests []Request) error`, `Clear(pluginID string) error`, `Start(context.Context)`, and `Stop()`. -- Persistent state: `/.verstak/notifications/schedules.json`. - -- [ ] **Step 1: Add failing scheduler tests** - -```go -func TestTickRetriesFailedSendAndAcknowledgesOneDelivery(t *testing.T) { - sender := &fakeSender{err: errors.New("unavailable")} - m := newTestManager(t, sender, time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC)) - requireNoError(t, m.Replace("verstak.todo", []Request{{ID: "todo-1", DueAt: "2026-07-14T11:00:00Z", Title: "Reminder"}})) - m.Tick(context.Background()) - if sender.calls != 1 || m.Items()[0].SentForDueAt != "" { t.Fatal("failed send was acknowledged") } - sender.err = nil - m.Tick(context.Background()) - m.Tick(context.Background()) - if sender.calls != 2 || m.Items()[0].SentForDueAt != "2026-07-14T11:00:00Z" { t.Fatal("delivery was not exactly once") } -} -``` - -Also test stale records are removed by `Replace`, changing `DueAt` rearms a record, state reloads from disk, and a past-due unsent item is sent once. - -- [ ] **Step 2: Confirm red** - -Run: `GOCACHE=/tmp/verstak-go-cache go test ./internal/core/notifications -count=1` - -Expected: fails because the package does not exist. - -- [ ] **Step 3: Implement the minimal manager** - -Use one mutex, injected clock/sender, an atomic same-directory temp-file rename, and a 30-second ticker. Preserve `SentForDueAt` only if the requested due time is unchanged. Validate non-empty unique IDs, RFC3339 UTC dates, at most 500 requests, and bounded text. Mark sent only after `Sender.Send` succeeds. - -- [ ] **Step 4: Verify and commit** - -Run: -```bash -gofmt -w internal/core/notifications/*.go -GOCACHE=/tmp/verstak-go-cache go test ./internal/core/notifications ./internal/core/... -count=1 -git diff --check -``` - -Commit/push: -```bash -git add internal/core/notifications -git commit -m "feat: add persisted notification scheduler" -git push https://github.com/mirivlad/verstak.git main -git push mirror main -``` - -### Task 2: Permission-gated core/plugin notification API - -**Files:** -- Modify: `main.go` -- Modify: `internal/api/app.go` -- Modify: `internal/api/app_test.go` -- Modify: `internal/core/permissions/registry.go` -- Modify: `frontend/src/lib/plugin-host/VerstakPluginAPI.js` -- Modify: `frontend/tests/plugin-api-contributions-test.mjs` - -**Interfaces:** -- Core capability: `verstak/core/notifications/v1`. -- Permission: `notifications.schedule`, non-dangerous. -- Bound methods: `ReplacePluginNotifications(pluginID string, items []notifications.Request) string` and `ClearPluginNotifications(pluginID string) string`. -- Plugin methods: `api.notifications.replace(items)` and `api.notifications.clear()`. - -- [ ] **Step 1: Add failing backend and host-API tests** - -```go -func TestReplacePluginNotificationsRequiresCapabilityAndPermission(t *testing.T) { - app, scheduler := newNotificationTestApp(t, manifestWithoutNotificationPermission) - if got := app.ReplacePluginNotifications("example", []notifications.Request{{ID: "r", DueAt: "2026-07-14T10:00:00Z", Title: "R"}}); got == "" { - t.Fatal("missing permission was accepted") - } - if scheduler.replaceCalls != 0 { t.Fatalf("replace calls = %d", scheduler.replaceCalls) } -} -``` - -The JS test mocks `App.ReplacePluginNotifications`, calls `api.notifications.replace`, and asserts both plugin ID forwarding and a namespaced rejection for a non-empty backend error. - -- [ ] **Step 2: Confirm red** - -```bash -GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run TestReplacePluginNotificationsRequiresCapabilityAndPermission -count=1 -node frontend/tests/plugin-api-contributions-test.mjs -``` - -Expected: missing API methods. - -- [ ] **Step 3: Implement guarded bridge** - -Register the capability in `main.go`, construct the scheduler, and guard bound calls with both existing access helpers before forwarding. Add `notifications.schedule` to the registry. Add the public host methods using `callBackendErrorString`; do not expose `window.runtime` to plugins. - -- [ ] **Step 4: Verify and commit** - -```bash -gofmt -w main.go internal/api/app.go internal/api/app_test.go internal/core/permissions/registry.go -GOCACHE=/tmp/verstak-go-cache go test ./internal/api -count=1 -node frontend/tests/plugin-api-contributions-test.mjs -git diff --check -``` - -```bash -git add main.go internal/api/app.go internal/api/app_test.go internal/core/permissions/registry.go frontend/src/lib/plugin-host/VerstakPluginAPI.js frontend/tests/plugin-api-contributions-test.mjs -git commit -m "feat: expose scheduled notifications to plugins" -git push https://github.com/mirivlad/verstak.git main -git push mirror main -``` - -### Task 3: Todo reminder synchronization and Wails lifecycle - -**Files:** -- Modify: `main.go` -- Modify: `internal/api/app.go` -- Modify: `internal/api/app_test.go` -- Modify: `../verstak-official-plugins/plugins/todo/plugin.json` -- Modify: `../verstak-official-plugins/plugins/todo/frontend/src/index.js` -- Modify: `../verstak-official-plugins/plugins/todo/locales/en.json` -- Modify: `../verstak-official-plugins/plugins/todo/locales/ru.json` -- Modify: `../verstak-official-plugins/scripts/smoke-todo-plugin.js` - -**Interfaces:** -- `App.DomReady(ctx)` initializes Wails notifications before starting the scheduler. -- `App.Shutdown(ctx)` stops it before Wails cleanup. -- Todo replaces the complete reminder list only after Todo persistence succeeds and after Todo data loads. - -- [ ] **Step 1: Add failing lifecycle and Todo smoke tests** - -The App test uses function variables for Wails calls and proves initialize → start and stop → cleanup order. The Todo smoke mock records `notifications.replace`; it asserts one open reminder request with the Todo ID and a later empty replacement after completion or deletion. - -- [ ] **Step 2: Confirm red** - -```bash -GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'TestDomReadyInitializesNotificationsBeforeScheduler|TestShutdownStopsNotifications' -count=1 -cd ../verstak-official-plugins && node scripts/smoke-todo-plugin.js -``` - -Expected: lifecycle methods and schedule calls are absent. - -- [ ] **Step 3: Implement lifecycle and Todo desired state** - -Use `runtime.InitializeNotifications`, `runtime.SendNotification`, and `runtime.CleanupNotifications` only in core. Register `OnDomReady`/ `OnShutdown` in Wails options. Todo maps open valid `reminderAt` values to ISO UTC using `new Date(todo.reminderAt).toISOString()`, localizes title/body, and calls `api.notifications.replace`. A scheduling failure reports status but never rolls back already-saved Todo data. - -- [ ] **Step 4: Verify and commit both repositories** - -```bash -GOCACHE=/tmp/verstak-go-cache go test ./internal/api -count=1 -cd ../verstak-official-plugins && node scripts/smoke-todo-plugin.js && ./scripts/check.sh -``` - -```bash -cd ../verstak-desktop -git add main.go internal/api/app.go internal/api/app_test.go -git commit -m "feat: start native notifications with desktop app" -git push https://github.com/mirivlad/verstak.git main -git push mirror main -cd ../verstak-official-plugins -git add plugins/todo scripts/smoke-todo-plugin.js -git commit -m "feat: schedule native Todo reminders" -git push https://github.com/mirivlad/verstak-official-plugins.git main -git push mirror main -``` - -### Task 4: Windows/Linux tray, close policy, and single instance - -**Files:** -- Create: `internal/shell/tray/controller.go` -- Create: `internal/shell/tray/controller_test.go` -- Modify: `main.go` -- Modify: `internal/api/app.go` -- Modify: `internal/api/app_test.go` -- Modify: `go.mod` -- Modify: `go.sum` - -**Interfaces:** -- `tray.Start(icon []byte, actions Actions) error`, with `Actions.Show` and `Actions.Quit`. -- `App.BeforeClose(ctx) bool` hides and returns true unless `App.Quit()` made shutdown explicit. -- `App.ShowWindow()` reveals the existing window. - -- [ ] **Step 1: Add failing close-policy and tray action tests** - -```go -func TestBeforeCloseHidesWindowUntilExplicitQuit(t *testing.T) { - app, window := newWindowTestApp(t) - if prevent := app.BeforeClose(context.Background()); !prevent || window.hideCalls != 1 { t.Fatal("ordinary close must hide") } - app.Quit() - if prevent := app.BeforeClose(context.Background()); prevent { t.Fatal("explicit quit was prevented") } -} -``` - -Use a fake tray adapter to assert exactly two labels, **Show Verstak** and **Quit**, and one callback invocation per click. - -- [ ] **Step 2: Confirm red** - -```bash -GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run TestBeforeCloseHidesWindowUntilExplicitQuit -count=1 -GOCACHE=/tmp/verstak-go-cache go test ./internal/shell/tray -count=1 -``` - -Expected: absent packages/methods. - -- [ ] **Step 3: Implement adapter and Wails wiring** - -Use `fyne.io/systray`. The production adapter calls `RunWithExternalLoop` so the -Windows native message loop runs alongside Wails, embeds a multi-resolution -ICO on Windows and PNG on Linux, routes one left click to `app.ShowWindow`, and -leaves the platform-native right-click menu active. `main.go` registers -`OnBeforeClose` and uses `options.SingleInstanceLock` whose second launch calls -`app.ShowWindow`. Do not embed ignored Wails-generated files from `build/`. - -- [ ] **Step 4: Verify and commit** - -```bash -gofmt -w main.go internal/api/app.go internal/api/app_test.go internal/shell/tray/*.go -GOCACHE=/tmp/verstak-go-cache go test ./internal/shell/tray ./internal/api -count=1 -./scripts/build.sh -! rg -n 'getlantern|appindicator' go.mod go.sum packaging scripts/build.sh -git diff --check -``` - -```bash -git add main.go internal/api/app.go internal/api/app_test.go internal/shell/tray go.mod go.sum -git commit -m "feat: keep desktop app in system tray" -git push https://github.com/mirivlad/verstak.git main -git push mirror main -``` - -### Task 5: Packaging and public README screenshots - -**Files:** -- Modify: `packaging/deb/control` -- Modify: `scripts/build.sh` -- Modify: `scripts/package-appimage.sh` -- Modify: `scripts/test-package-formats.sh` -- Modify: `README.md` -- Create: `README.ru.md` -- Create: `docs/screenshots/overview.png` -- Create: `docs/screenshots/workspace-files-notes.png` -- Create: `docs/screenshots/activity-journal.png` - -- [ ] **Step 1: Add failing package-contract checks** - -Add assertions that the Linux build guidance, Debian metadata, and AppImage -packager no longer require the removed AppIndicator backend. - -- [ ] **Step 2: Confirm red** - -Run: `./scripts/test-package-formats.sh` - -Expected: the first tray dependency assertion fails. - -- [ ] **Step 3: Implement portable package support** - -Keep Debian and AppImage focused on the Wails/WebKitGTK runtime. Do not add a -tray-specific AppIndicator dependency. Keep the Windows system-WebView2 policy -unchanged. - -- [ ] **Step 4: Install the supplied public README sources** - -Replace this repository's `README.md` with the supplied English source at the workspace root and add the supplied Russian source as `README.ru.md`. Add an image strip after “What is Verstak?” / “Что такое Верстак?” that links to three tracked PNG screenshots and uses matching localized alt text. - -- [ ] **Step 5: Produce real screenshots** - -Build and run the desktop app against `/home/mirivlad/Nextcloud/Verstak/VerstakVault` on the active X display. Capture: -1. Overview showing recent work and entry points; -2. a populated workspace Files/Notes view; -3. Activity with a suggested session and its Journal review. - -Use `scrot` or ImageMagick `import`, crop only surrounding desktop chrome, inspect every PNG visually, and do not add generated mockups. Remove all non-user-safe text from the test vault before capture. - -- [ ] **Step 6: Verify documentation and package contracts, commit, push** - -```bash -./scripts/test-package-formats.sh -git diff --check -test -s docs/screenshots/overview.png -test -s docs/screenshots/workspace-files-notes.png -test -s docs/screenshots/activity-journal.png -``` - -```bash -git add packaging/deb/control scripts/build.sh scripts/package-appimage.sh scripts/test-package-formats.sh README.md README.ru.md docs/screenshots -git commit -m "docs: add public README and product screenshots" -git push https://github.com/mirivlad/verstak.git main -git push mirror main -``` - -### Task 6: Complete verification and GitHub alpha releases - -**Files:** -- No source changes expected unless verification exposes a defect. - -- [ ] **Step 1: Run all checks** - -```bash -GOCACHE=/tmp/verstak-go-cache ./scripts/test.sh -./scripts/check.sh -./scripts/test-package-formats.sh -./scripts/test-build-windows.sh -cd ../verstak-official-plugins && ./scripts/check.sh && ./scripts/test-package-portable.sh && ./scripts/test-publish-github-release.sh -``` - -- [ ] **Step 2: Build and checksum both alpha releases** - -```bash -cd ../verstak-official-plugins && ./scripts/release.sh v0.1.0-alpha.2 && (cd release && sha256sum --check SHA256SUMS) -cd ../verstak-desktop && ./scripts/release.sh v0.1.0-alpha.2 && (cd release && sha256sum --check SHA256SUMS) -``` - -- [ ] **Step 3: Publish and inspect releases** - -```bash -cd ../verstak-official-plugins && ./scripts/publish-github-release.sh v0.1.0-alpha.2 -cd ../verstak-desktop && ./scripts/publish-github-release.sh v0.1.0-alpha.2 -gh release view v0.1.0-alpha.2 -R mirivlad/verstak-official-plugins -gh release view v0.1.0-alpha.2 -R mirivlad/verstak -``` - -The final report distinguishes automated verification from the two required real desktop manual checks: tray close/show/quit and a near-future Todo notification while the window is hidden. diff --git a/docs/superpowers/specs/2026-07-14-native-notifications-and-tray-design.md b/docs/superpowers/specs/2026-07-14-native-notifications-and-tray-design.md deleted file mode 100644 index 29c4328..0000000 --- a/docs/superpowers/specs/2026-07-14-native-notifications-and-tray-design.md +++ /dev/null @@ -1,182 +0,0 @@ -# Native Notifications and System Tray Design - -**Status:** implemented; tray reliability update recorded on 2026-07-15 - -## Goal - -Deliver Todo reminders as native notifications on Windows and Linux, and keep -Verstak running in the system tray when its main window is closed. The feature -must work in the portable Windows archive, Debian package, and AppImage. - -## Scope - -- The desktop core owns notification delivery, scheduling, and tray lifetime. -- `verstak.todo` owns the Todo-specific reminder policy and text. -- No new official notifications plugin is introduced. A dynamic plugin cannot - call Wails directly, so it cannot be the native-notification transport. -- macOS is out of scope for this alpha. The interfaces remain platform-neutral - where that costs nothing. - -## Tray behavior - -On Windows and Linux, a tray icon is initialized after Wails reaches -`OnDomReady`. Its menu contains exactly two actions: - -1. **Show Verstak** — shows and focuses the existing main window. -2. **Quit** — exits the process deliberately. - -One left click restores and focuses the existing main window. A native right -click opens the menu. Closing the main window with its window-manager close -control hides the window only after the tray has successfully initialized; it -then keeps the process, plugins, local browser receiver, and reminder scheduler -alive. If tray initialization fails or the native message loop exits, the -ordinary close path exits normally rather than leaving an unreachable process. -The quit action allows the close lifecycle to finish and exits normally. - -The app has a single-instance lock. If a user launches the executable while an -instance is hidden in the tray, the existing instance shows its window instead -of creating a second process. - -The implementation uses `fyne.io/systray` through a small -`internal/shell/tray` adapter. `RunWithExternalLoop` starts the Windows native -message loop without making Wails relinquish ownership of its GUI lifecycle. -The icon is a source-controlled multi-resolution ICO on Windows (16, 20, 24, -32, 48, and 256 pixels with transparency) and a PNG on Linux. Both are embedded -in the binary, so a clean build does not depend on ignored Wails-generated -files. Tray readiness is published only after icon, tooltip, and menu creation -all succeed; lifecycle diagnostics are logged for startup, readiness, clicks, -failure fallback, and shutdown. - -## Notification capability and permission - -The core registers the capability: - -``` -verstak/core/notifications/v1 -``` - -Plugins that use it must both require that capability and declare the -`notifications.schedule` permission. The plugin-host API exposes only two -operations within the calling plugin namespace: - -``` -api.notifications.replace(items) -api.notifications.clear() -``` - -`replace` is an atomic desired-state replacement, not an append operation. An -item contains a plugin-local stable `id`, an ISO-8601 UTC `dueAt`, a title, and -a body. The core supplies the plugin ID and rejects calls from disabled, -missing-permission, or undeclared-capability plugins. It also validates empty -IDs, duplicate IDs, invalid timestamps, and unsafe oversized text. - -No plugin can send arbitrary immediate native notifications or address another -plugin's schedules in this alpha. - -## Scheduler and persistence - -`internal/core/notifications` persists one canonical schedule file at: - -``` -/.verstak/notifications/schedules.json -``` - -Each record contains `{pluginId, id, dueAt, title, body, sentForDueAt}`. -The composite `(pluginId, id)` is unique. Replacing an item with the same due -time preserves `sentForDueAt`; changing `dueAt` clears it. Replacing a plugin's -list removes its stale records. This provides deterministic cancellation for -completed, deleted, and rescheduled Todos. - -The manager starts after Wails reaches `OnDomReady`, initializes Wails native -notifications, and evaluates the persisted schedule immediately and then at -least every 30 seconds. A sender is injected behind an interface for unit -tests. After a successful delivery, the manager atomically records -`sentForDueAt`. A delivery error leaves the schedule pending and is logged for -a later retry. - -An expired record that has not been sent is delivered once after the next app -start. A record already sent for its current due time is never sent again. -Completely quitting Verstak stops the scheduler: no separate daemon or OS -background service is added. Hiding the window in the tray does **not** stop it. - -The core calls `CleanupNotifications` during shutdown, including on Linux where -it releases the D-Bus connection. - -## Todo behavior - -`verstak.todo` adds the core notifications capability to `requires` and adds -the `notifications.schedule` permission. After every successful Todo storage -write, it derives the complete desired reminder list: - -- include only open Todos with a valid `reminderAt`; -- use the Todo ID as the stable notification ID; -- convert local `datetime-local` input to an ISO-8601 UTC instant; -- use the Todo title in the notification body and locale-aware reminder text; -- call `api.notifications.replace` with the full list. - -The same replacement runs after loading persisted Todos, so a transient -schedule-write failure repairs itself next time the Todo view is opened. A -schedule API failure does not roll back Todo data; the UI reports the failure -instead. The existing in-view overdue/reminder badge remains useful context and -is not removed. - -## Packaging - -`fyne.io/systray` supplies the Windows message loop and uses the session D-Bus -on Linux. It does not require the removed AppIndicator development or runtime -package. The Windows release build still uses `x86_64-w64-mingw32-gcc` for the -Wails application itself. - -The existing AppImage packager traverses `ldd` for the desktop executable and -copies non-glibc runtime libraries; it does not require a tray-specific shared -library. - -## Public README and product screenshots - -The workspace-root `README.md` and `README.ru.md` supplied by the maintainer -become the public repository documents: the English source replaces this -repository's `README.md`, and the Russian source is committed as -`README.ru.md`. Their existing language links and public-release instructions -are retained. - -Three screenshots are captured from the real desktop application using the -test vault, then committed under `docs/screenshots/`: - -1. `overview.png` — returning to recent work and useful next actions; -2. `workspace-files-notes.png` — ordinary vault files and Markdown notes in a - workspace; -3. `activity-journal.png` — review of a factual activity session as a Journal - entry. - -Both README variants include the same three images with localized alt text. -They are factual UI captures, not generated illustrative mockups. Capture data -must be limited to the disposable test vault and inspected before commit so no -credentials or personal content are published. - -## Test and manual verification - -Automated tests cover: - -- schedule replacement, cancellation, rescheduling, persistence, one-time - overdue delivery, failed-send retry, and permission/capability rejection; -- Todo desired-list derivation and calls after create/edit/status/delete; -- close policy: ordinary close hides only after tray readiness, while explicit - quit permits shutdown; -- tray controller action wiring, readiness/failure fallback, idempotent stop, - left-click reveal, and second-instance window reveal; -- true multi-resolution Windows ICO data, Linux PNG data, and Linux/Windows - build and package dependency expectations. - -Manual smoke tests are required because neither unit tests nor Playwright can -assert a real desktop notification area or OS toast: - -1. On Linux and Windows, start Verstak, verify the tray icon and tooltip, use - one left click to reveal the window, use the right-click menu to reveal it, - close the window, and use **Quit** to terminate it. -2. Set a Todo reminder for a near future time, hide the window in the tray, and - observe one native notification. -3. Quit before a future reminder, relaunch after it expires, and observe one - overdue notification with no duplicate on the next scheduler scan. -4. Inspect all three README screenshots at their committed size and confirm - that they show only intended test-vault data and explain the feature named - in their localized captions.