13 KiB
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" -
captureContextNodeIdequals the target node ID for node context -
suggestedTargetNodeIdequals the node ID for node context -
local inbox lookup uses node ID, not title
-
Step 2: Verify RED
Run:
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:
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:
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:
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:
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:
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:
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:
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 existingMoveNodetext: move note into target via existingMoveNodeurl: create a row inlinks, then remove/hide the unresolved artifact
Keep AssignInboxNode as a compatibility alias to ResolveInboxNode.
- Step 5: Verify GREEN and commit
Run:
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:
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:
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:
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:
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:
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.writeTextonly as an optional best-effort UI action -
Step 3: Verify GUI smoke and commit
Run:
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:
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:
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,
pasteevent for Ctrl+V -
Step 2: Run full required checks
Run:
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
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
pasteeventclipboardData. - 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/inboxand 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.