390 lines
13 KiB
Markdown
390 lines
13 KiB
Markdown
# Unified Capture Inbox Links Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Build one predictable Capture / Inbox / Links pipeline for external drag-and-drop, paste, and clipboard-button input.
|
||
|
||
**Architecture:** All external payloads become unresolved inbox artifacts first. Backend stores capture status/context/source metadata on artifact nodes, stages binary payloads under `.verstak/inbox`, resolves artifacts by source kind into Files, Notes, or a dedicated Links table, and exposes global/local inbox APIs. Frontend uses one `resolveCaptureContext()` and one capture dispatcher for drop, paste, and clipboard button instead of screen-specific handlers.
|
||
|
||
**Tech Stack:** Go, SQLite migrations, Wails v2 bindings, Svelte 4, Vite 5, existing rendered GUI smoke harness.
|
||
|
||
---
|
||
|
||
### Task 1: Backend Capture Metadata And Inbox Queries
|
||
|
||
**Files:**
|
||
- Modify: `cmd/verstak-gui/bindings_capture.go`
|
||
- Modify: `cmd/verstak-gui/bindings_inbox.go`
|
||
- Modify: `cmd/verstak-gui/capture_test.go`
|
||
- Modify: `cmd/verstak-gui/inbox_test.go`
|
||
|
||
- [ ] **Step 1: Write failing tests for context metadata**
|
||
|
||
Add tests that capture text on a section and a file while a node is active. Assert:
|
||
- `captureStatus == "unresolved"`
|
||
- `captureContextType == "section"` or `"node"`
|
||
- `captureContextNodeId` equals the target node ID for node context
|
||
- `suggestedTargetNodeId` equals the node ID for node context
|
||
- local inbox lookup uses node ID, not title
|
||
|
||
- [ ] **Step 2: Verify RED**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env GOCACHE=/tmp/verstak-go-cache go test ./cmd/verstak-gui -run 'TestCapture.*Context|TestListInboxNodesForTarget' -count=1
|
||
```
|
||
|
||
Expected: fail because context-aware capture APIs and local inbox query do not exist yet.
|
||
|
||
- [ ] **Step 3: Implement metadata helpers**
|
||
|
||
Add a JSON context DTO:
|
||
|
||
```go
|
||
type CaptureContextDTO struct {
|
||
ContextType string `json:"contextType"`
|
||
NodeID string `json:"nodeId,omitempty"`
|
||
Section string `json:"section,omitempty"`
|
||
SuggestedTargetNodeID string `json:"suggestedTargetNodeId,omitempty"`
|
||
}
|
||
```
|
||
|
||
Extend capture metadata with:
|
||
|
||
```text
|
||
capture.status
|
||
capture.context_type
|
||
capture.context_node_id
|
||
capture.context_section
|
||
capture.suggested_target_node_id
|
||
capture.source_kind
|
||
capture.source
|
||
capture.created_at
|
||
capture.inbox
|
||
capture.kind
|
||
```
|
||
|
||
Keep old `CaptureText`, `CaptureURL`, `CapturePath`, and `CaptureFileData` as compatibility wrappers.
|
||
|
||
- [ ] **Step 4: Implement local inbox API**
|
||
|
||
Add:
|
||
|
||
```go
|
||
func (a *App) ListInboxNodesForTarget(nodeID string) ([]InboxNodeDTO, error)
|
||
```
|
||
|
||
Return unresolved artifacts where `capture.context_node_id = nodeID OR capture.suggested_target_node_id = nodeID`.
|
||
|
||
- [ ] **Step 5: Verify GREEN and commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env GOCACHE=/tmp/verstak-go-cache go test ./cmd/verstak-gui -run 'TestCapture.*Context|TestListInboxNodesForTarget|TestListInboxNodesReturnsOnlyCapturedArtifacts' -count=1
|
||
git add cmd/verstak-gui/bindings_capture.go cmd/verstak-gui/bindings_inbox.go cmd/verstak-gui/capture_test.go cmd/verstak-gui/inbox_test.go docs/superpowers/plans/2026-06-05-unified-capture-inbox-links.md
|
||
git commit -m "feat: track capture context in inbox"
|
||
git push
|
||
```
|
||
|
||
### Task 2: Dedicated Link Artifacts And Resolve Routing
|
||
|
||
**Files:**
|
||
- Create: `internal/core/storage/migrations_015.sql.go`
|
||
- Modify: `internal/core/storage/storage.go`
|
||
- Create: `cmd/verstak-gui/bindings_links.go`
|
||
- Modify: `cmd/verstak-gui/bindings_capture.go`
|
||
- Modify: `cmd/verstak-gui/bindings_inbox.go`
|
||
- Modify: `cmd/verstak-gui/inbox_test.go`
|
||
|
||
- [ ] **Step 1: Write failing tests for URL capture and resolve**
|
||
|
||
Add tests that capture URL, resolve it into Project A, and assert:
|
||
- unresolved URL uses `sourceKind == "url"`
|
||
- it appears in global and local inbox before resolve
|
||
- after resolve it disappears from inbox
|
||
- `ListLinks(ProjectA)` contains exactly the resolved link
|
||
- `ListLinks(ProjectB)` does not contain it
|
||
|
||
- [ ] **Step 2: Verify RED**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env GOCACHE=/tmp/verstak-go-cache go test ./cmd/verstak-gui -run 'TestResolve.*URL|TestListLinks' -count=1
|
||
```
|
||
|
||
Expected: fail because link table/API and URL routing do not exist.
|
||
|
||
- [ ] **Step 3: Add links table and bindings**
|
||
|
||
Create `links` table:
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS links (
|
||
id TEXT PRIMARY KEY,
|
||
node_id TEXT NOT NULL REFERENCES nodes(id),
|
||
title TEXT NOT NULL,
|
||
url TEXT NOT NULL,
|
||
hostname TEXT NOT NULL DEFAULT '',
|
||
note TEXT NOT NULL DEFAULT '',
|
||
source TEXT NOT NULL DEFAULT '',
|
||
captured_at TEXT NULL,
|
||
created_at TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_links_node ON links(node_id);
|
||
```
|
||
|
||
Expose `ListLinks`, `UpdateLink`, `DeleteLink`, and `OpenLink`.
|
||
|
||
- [ ] **Step 4: Implement resolve routing**
|
||
|
||
Add:
|
||
|
||
```go
|
||
func (a *App) ResolveInboxNode(nodeID, targetParentID string) (*NodeDTO, error)
|
||
func (a *App) ResolveInboxNodeHere(nodeID string) (*NodeDTO, error)
|
||
```
|
||
|
||
Route source kinds:
|
||
- `file`, `folder`, `image`: move node into target via existing `MoveNode`
|
||
- `text`: move note into target via existing `MoveNode`
|
||
- `url`: create a row in `links`, then remove/hide the unresolved artifact
|
||
|
||
Keep `AssignInboxNode` as a compatibility alias to `ResolveInboxNode`.
|
||
|
||
- [ ] **Step 5: Verify GREEN and commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env GOCACHE=/tmp/verstak-go-cache go test ./cmd/verstak-gui -run 'TestResolve.*URL|TestAssignInboxNode|TestDeleteInboxNode|TestListLinks' -count=1
|
||
git add internal/core/storage/storage.go internal/core/storage/migrations_015.sql.go cmd/verstak-gui/bindings_links.go cmd/verstak-gui/bindings_capture.go cmd/verstak-gui/bindings_inbox.go cmd/verstak-gui/inbox_test.go
|
||
git commit -m "feat: resolve inbox links separately"
|
||
git push
|
||
```
|
||
|
||
### Task 3: Native Clipboard Bridge And Capture Bindings
|
||
|
||
**Files:**
|
||
- Create: `cmd/verstak-gui/bindings_clipboard.go`
|
||
- Modify: `frontend/src/wailsjs/go/main/App.js`
|
||
- Modify: `cmd/verstak-gui/capture_test.go`
|
||
|
||
- [ ] **Step 1: Write failing test for clipboard text routing**
|
||
|
||
Add a focused test for a helper that classifies clipboard text as URL before plain text.
|
||
|
||
- [ ] **Step 2: Verify RED**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env GOCACHE=/tmp/verstak-go-cache go test ./cmd/verstak-gui -run 'TestClassifyClipboardText' -count=1
|
||
```
|
||
|
||
Expected: fail because helper does not exist.
|
||
|
||
- [ ] **Step 3: Implement backend clipboard bridge**
|
||
|
||
Expose:
|
||
|
||
```go
|
||
func (a *App) ReadClipboardText() (string, error)
|
||
func (a *App) CaptureClipboardTextWithContext(contextJSON string) (*InboxNodeDTO, error)
|
||
```
|
||
|
||
Use Wails v2 `runtime.ClipboardGetText(a.ctx)`. Convert backend errors to a user-facing message; do not surface browser `NotAllowedError`.
|
||
|
||
- [ ] **Step 4: Verify GREEN and commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env GOCACHE=/tmp/verstak-go-cache go test ./cmd/verstak-gui -run 'TestClassifyClipboardText|TestCapture.*' -count=1
|
||
git add cmd/verstak-gui/bindings_clipboard.go cmd/verstak-gui/capture_test.go frontend/src/wailsjs/go/main/App.js
|
||
git commit -m "feat: add native clipboard capture bridge"
|
||
git push
|
||
```
|
||
|
||
### Task 4: Frontend Unified Capture Pipeline
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/App.svelte`
|
||
- Modify: `frontend/src/wailsjs/go/main/App.js`
|
||
- Modify: `frontend/src/lib/i18n/locales/ru.js`
|
||
- Modify: `frontend/src/lib/i18n/locales/en.js`
|
||
- Modify: `scripts/check-gui-render.mjs`
|
||
|
||
- [ ] **Step 1: Update smoke mock for new bindings**
|
||
|
||
Add mock methods:
|
||
- `CaptureTextWithContext`
|
||
- `CaptureURLWithContext`
|
||
- `CapturePathWithContext`
|
||
- `CaptureFileDataWithContext`
|
||
- `CaptureClipboardTextWithContext`
|
||
- `ResolveInboxNode`
|
||
- `ResolveInboxNodeHere`
|
||
- `ListInboxNodesForTarget`
|
||
- `ListLinks`
|
||
- `UpdateLink`
|
||
- `DeleteLink`
|
||
- `OpenLink`
|
||
|
||
- [ ] **Step 2: Refactor capture inputs**
|
||
|
||
Implement:
|
||
|
||
```js
|
||
function resolveCaptureContext()
|
||
async function captureTextPayload(text, source)
|
||
async function captureUrlPayload(url, title, source)
|
||
async function capturePathPayload(paths, source)
|
||
async function captureFilePayload(file, source)
|
||
async function handleGlobalPaste(event)
|
||
async function handleGlobalDrop(event)
|
||
```
|
||
|
||
Use `event.clipboardData` for Ctrl+V and Wails/native clipboard binding for the button.
|
||
|
||
- [ ] **Step 3: Add drop overlay**
|
||
|
||
Show:
|
||
- `Будет добавлено в Неразобранное для: <node title/path>` when a node is selected
|
||
- `Будет добавлено в глобальное Неразобранное` on system/global sections
|
||
|
||
- [ ] **Step 4: Verify frontend build and commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env PATH=/home/mirivlad/.config/nvm/versions/node/v24.13.1/bin:$PATH npm run build
|
||
git add frontend/src/App.svelte frontend/src/wailsjs/go/main/App.js frontend/src/lib/i18n/locales/ru.js frontend/src/lib/i18n/locales/en.js scripts/check-gui-render.mjs
|
||
git commit -m "feat: unify frontend capture pipeline"
|
||
git push
|
||
```
|
||
|
||
### Task 5: Local Inbox And Links UI
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/App.svelte`
|
||
- Modify: `frontend/src/lib/i18n/locales/ru.js`
|
||
- Modify: `frontend/src/lib/i18n/locales/en.js`
|
||
- Modify: `scripts/check-gui-render.mjs`
|
||
|
||
- [ ] **Step 1: Add tabs**
|
||
|
||
Add node tabs:
|
||
- `Неразобранное`
|
||
- `Ссылки`
|
||
|
||
Local Inbox shows unresolved artifacts for the current node only. Links shows resolved link artifacts for the current node only.
|
||
|
||
- [ ] **Step 2: Add link actions**
|
||
|
||
Implement minimal actions:
|
||
- open external browser via `OpenLink`
|
||
- edit title / URL / note via modal
|
||
- delete via `DeleteLink`
|
||
- copy URL using `navigator.clipboard.writeText` only as an optional best-effort UI action
|
||
|
||
- [ ] **Step 3: Verify GUI smoke and commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env PATH=/home/mirivlad/.config/nvm/versions/node/v24.13.1/bin:$PATH GOCACHE=/tmp/verstak-go-cache ./scripts/check-gui.sh
|
||
git add frontend/src/App.svelte frontend/src/lib/i18n/locales/ru.js frontend/src/lib/i18n/locales/en.js scripts/check-gui-render.mjs
|
||
git commit -m "feat: add local inbox and links tabs"
|
||
git push
|
||
```
|
||
|
||
### Task 6: Editable Hotkey Guard
|
||
|
||
**Files:**
|
||
- Modify: `frontend/src/App.svelte`
|
||
- Modify: `frontend/src/lib/FilePreviewModal.svelte`
|
||
- Modify: `frontend/src/lib/FirstRun.svelte`
|
||
- Modify: `scripts/check-gui-render.mjs`
|
||
|
||
- [ ] **Step 1: Add failing smoke assertion**
|
||
|
||
Open the add-action modal, type a title containing spaces, and assert the modal remains open until save/cancel.
|
||
|
||
- [ ] **Step 2: Implement editable target guard**
|
||
|
||
Use:
|
||
|
||
```js
|
||
function isEditableTarget(target) {
|
||
if (!target || !(target instanceof Element)) return false
|
||
return !!target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]')
|
||
}
|
||
```
|
||
|
||
Global hotkeys and `onKeyActivate` must return early for editable targets.
|
||
|
||
- [ ] **Step 3: Verify and commit**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env PATH=/home/mirivlad/.config/nvm/versions/node/v24.13.1/bin:$PATH GOCACHE=/tmp/verstak-go-cache ./scripts/check-gui.sh
|
||
git add frontend/src/App.svelte frontend/src/lib/FilePreviewModal.svelte frontend/src/lib/FirstRun.svelte scripts/check-gui-render.mjs
|
||
git commit -m "fix: ignore global hotkeys in editable fields"
|
||
git push
|
||
```
|
||
|
||
### Task 7: Full Verification And Documentation Alignment
|
||
|
||
**Files:**
|
||
- Modify: `docs/01_Product_Spec.md`
|
||
- Modify: `docs/03_Data_Model_Storage.md`
|
||
- Modify: `docs/05_UI_UX.md`
|
||
- Modify: `docs/06_Roadmap.md`
|
||
|
||
- [ ] **Step 1: Update docs**
|
||
|
||
Document current behavior:
|
||
- all external capture creates unresolved inbox artifacts
|
||
- local/global inbox semantics
|
||
- capture context metadata by node ID
|
||
- Links tab and link artifact model
|
||
- backend clipboard bridge for button, `paste` event for Ctrl+V
|
||
|
||
- [ ] **Step 2: Run full required checks**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
env GOCACHE=/tmp/verstak-go-cache go test ./...
|
||
env PATH=/home/mirivlad/.config/nvm/versions/node/v24.13.1/bin:$PATH npm run build
|
||
env PATH=/home/mirivlad/.config/nvm/versions/node/v24.13.1/bin:$PATH node scripts/check-gui-render.mjs
|
||
env PATH=/home/mirivlad/.config/nvm/versions/node/v24.13.1/bin:$PATH node scripts/check-i18n.sh
|
||
```
|
||
|
||
If `scripts/check-i18n.sh` is not a Node script or does not exist, inspect scripts and run the repository’s actual i18n check.
|
||
|
||
- [ ] **Step 3: Commit and push**
|
||
|
||
```bash
|
||
git add docs/01_Product_Spec.md docs/03_Data_Model_Storage.md docs/05_UI_UX.md docs/06_Roadmap.md
|
||
git commit -m "docs: describe unified capture inbox flow"
|
||
git push
|
||
```
|
||
|
||
### Self-Review Checklist
|
||
|
||
- [ ] External drop anywhere creates unresolved artifact, never direct case content.
|
||
- [ ] Ctrl+V uses `paste` event `clipboardData`.
|
||
- [ ] Clipboard button uses backend/native bridge.
|
||
- [ ] URL artifacts resolve into `links`, not notes.
|
||
- [ ] Local Inbox is metadata view by node ID.
|
||
- [ ] Global Inbox shows all unresolved artifacts.
|
||
- [ ] Files/folders/images are staged under `.verstak/inbox` and moved to canonical target filesystem path on resolve.
|
||
- [ ] Space inside editable fields never triggers global UI actions.
|
||
- [ ] GUI smoke covers capture, resolve, links, and modal-space regression.
|