verstak/docs/superpowers/plans/2026-06-05-unified-capture-...

13 KiB
Raw Blame History

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:

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

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 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:

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

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:

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, paste event 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 repositorys 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 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.