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

390 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 repositorys 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.