feat: track capture context in inbox
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user