230 lines
10 KiB
Markdown
230 lines
10 KiB
Markdown
# Alpha Browser Inbox 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:** Preserve a manually captured link when it leaves the global Inbox, give Browser Inbox a usable archive, and make links durable across case lifecycle changes.
|
|
|
|
**Architecture:** A capture remains one canonical record. Its global archive state is independent of its workspaceId relation; domain bindings use the same stable identity. Browser Inbox owns capture UI and migration, while Desktop exposes a narrow URL-open capability for user-initiated links.
|
|
|
|
**Tech Stack:** Plain JavaScript plugin bundle, Go Wails API, Node smoke tests, Playwright.
|
|
|
|
## Global Constraints
|
|
|
|
- Only manual extension actions create Browser Inbox captures.
|
|
- Captures have at most one optional case assignment in this alpha.
|
|
- Remove from global Inbox archives; only Delete everywhere is permanent.
|
|
- Assignment and binding use workspaceId, never path as identity.
|
|
- Create .url files through files.write and never overwrite a name silently.
|
|
- URL opening is direct HTTP(S) opening, not a Linux .url association.
|
|
|
|
---
|
|
|
|
### Task 1: Migrate Browser Inbox records and implement archive state
|
|
|
|
**Files:**
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/en.json
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/ru.json
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
|
|
|
**Interfaces:**
|
|
- Canonical capture fields: globalState, workspaceId, workspaceState, workspaceRootPath, workspaceTrashId.
|
|
- Domain binding value: workspaceId plus workspaceRootPath cache plus state.
|
|
- Mutations: archive, restore, assign, unassign, deleteEverywhere, migrate.
|
|
|
|
- [ ] **Step 1: Write failing migration and archive smoke tests**
|
|
|
|
Use a legacy path assignment and assert it resolves to a stable ID. Test the
|
|
original data-loss regression:
|
|
|
|
~~~js
|
|
await inbox.assignWorkspace('capture-1', activeWorkspace);
|
|
await inbox.archiveCapture('capture-1');
|
|
assert.equal(globalRows().some(row => row.captureId === 'capture-1'), false);
|
|
assert.equal(workspaceRows(activeWorkspace.workspaceId).some(row => row.captureId === 'capture-1'), true);
|
|
await inbox.restoreCapture('capture-1');
|
|
assert.equal(globalRows().some(row => row.captureId === 'capture-1'), true);
|
|
~~~
|
|
|
|
- [ ] **Step 2: Run red**
|
|
|
|
Run:
|
|
|
|
~~~bash
|
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins
|
|
node scripts/smoke-browser-inbox-plugin.js
|
|
~~~
|
|
|
|
Expected: FAIL because archive and assignment are currently one mutable path field.
|
|
|
|
- [ ] **Step 3: Implement canonical state and idempotent migration**
|
|
|
|
Normalize every capture on read. Set legacy captures globalState active. Resolve
|
|
legacy paths through Desktop identities: active marker gives workspaceId,
|
|
otherwise set workspaceState unavailable. Store global captures once; workspace
|
|
views filter the canonical collection by workspaceId rather than duplicate
|
|
storage keys. Reject permanent deletion without an explicit confirm token from
|
|
the UI.
|
|
|
|
- [ ] **Step 4: Run green and commit**
|
|
|
|
Run:
|
|
|
|
~~~bash
|
|
node scripts/smoke-browser-inbox-plugin.js
|
|
./scripts/check.sh
|
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
|
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: preserve archived browser inbox captures"
|
|
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/api/app.go internal/api/app_test.go
|
|
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: migrate browser inbox relations"
|
|
~~~
|
|
|
|
Expected: migration, archive, restore, and assignment tests pass.
|
|
|
|
### Task 2: Handle rename, Trash, restore, purge, and archive UI
|
|
|
|
**Files:**
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/browser-inbox.spec.js
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
|
|
|
|
**Interfaces:**
|
|
- Receives workspace.renamed, workspace.trashed, workspace.restored, and workspace.purged.
|
|
- Status filter values are active, archive, all.
|
|
- Archive bulk action is restore; bulk permanent delete is not offered.
|
|
|
|
- [ ] **Step 1: Write failing lifecycle and UI tests**
|
|
|
|
Assert a rename changes only path cache, trash disables routing, restore restores
|
|
the same workspace ID, purge makes captures unassigned and bindings orphaned:
|
|
|
|
~~~js
|
|
await emitWorkspaceEvent('workspace.trashed', { workspaceId: 'w-1', trashId: 't-1' });
|
|
assert.equal(capture.workspaceState, 'trashed');
|
|
assert.equal(domainBinding('client.example').state, 'trashed');
|
|
await emitWorkspaceEvent('workspace.purged', { workspaceId: 'w-1', trashId: 't-1' });
|
|
assert.equal(capture.workspaceState, 'unassigned');
|
|
assert.equal(domainBinding('client.example').state, 'orphaned');
|
|
~~~
|
|
|
|
In Playwright, select Archive, restore one visible item, and bulk-restore only
|
|
the filtered archive rows. Also click Delete everywhere for an assigned capture,
|
|
assert the dialog names its assigned Дело, cancel, and assert that the global
|
|
and assigned rows still exist.
|
|
|
|
- [ ] **Step 2: Run red**
|
|
|
|
Run:
|
|
|
|
~~~bash
|
|
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
|
|
npm run test:e2e -- browser-inbox.spec.js
|
|
~~~
|
|
|
|
Expected: FAIL because the current UI has only processed filters and destructive Clear.
|
|
|
|
- [ ] **Step 3: Implement lifecycle and labels**
|
|
|
|
Subscribe once from the Browser Inbox background service. Update path caches on
|
|
rename only when workspaceId matches. On external unavailable, do not route to
|
|
the same path again. Add Active, Archive, and All filters; make search apply
|
|
inside the selected filter; show Archive badge inside a case; add Restore to
|
|
Inbox and visible filtered bulk restore. Rename Clear to an archive action with
|
|
count confirmation. Keep Delete everywhere separate and require a dialog that
|
|
names the assigned Дело; cancellation must perform no mutation.
|
|
|
|
- [ ] **Step 4: Run green and commit**
|
|
|
|
Run:
|
|
|
|
~~~bash
|
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js
|
|
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- browser-inbox.spec.js
|
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
|
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: add browser inbox archive lifecycle"
|
|
git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/browser-inbox.spec.js frontend/src/lib/test/wails-mock.js
|
|
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover browser inbox archive lifecycle"
|
|
~~~
|
|
|
|
Expected: archive filters, restore, rename, Trash, restore, and purge tests pass.
|
|
|
|
### Task 3: Save and open durable links without file-association reliance
|
|
|
|
**Files:**
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/permissions/registry.go
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/plugin.json
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
|
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
|
|
|
|
**Interfaces:**
|
|
- Permission and API: urls.openExternal(url string) for user-initiated HTTP(S) URLs only.
|
|
- SaveLinkInCase(captureId, workspaceId, filename) creates Links/name.url with InternetShortcut URL field.
|
|
- Collision response offers a proposed unique filename and never overwrites.
|
|
|
|
- [ ] **Step 1: Write failing API and plugin tests**
|
|
|
|
Add tests for invalid URL, readonly failure, unique filename, collision, and
|
|
direct opener argument:
|
|
|
|
~~~go
|
|
if errStr := app.OpenExternalURL("browser.plugin", "https://example.test/a"); errStr != "" {
|
|
t.Fatal(errStr)
|
|
}
|
|
if got := opener.Arguments[0]; got != "https://example.test/a" {
|
|
t.Fatalf("opened %q, want URL not .url path", got)
|
|
}
|
|
~~~
|
|
|
|
Add plugin smoke assertions for a title-derived name, Link fallback, and
|
|
proposal Link (2).url after a collision.
|
|
|
|
- [ ] **Step 2: Run red**
|
|
|
|
Run:
|
|
|
|
~~~bash
|
|
cd /home/mirivlad/git/verstak2/verstak-desktop
|
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'Test.*OpenExternalURL' -count=1
|
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins
|
|
node scripts/smoke-browser-inbox-plugin.js
|
|
~~~
|
|
|
|
Expected: FAIL because only file-path external opening exists.
|
|
|
|
- [ ] **Step 3: Implement safe save and open**
|
|
|
|
Register urls.openExternal as dangerous. Validate HTTP(S), invoke the existing
|
|
OS opener with the URL string, and expose api.urls.openExternal. Browser Inbox
|
|
requests that permission, creates Links through files.createFolder and
|
|
files.writeText using:
|
|
|
|
~~~text
|
|
[InternetShortcut]
|
|
URL=https://example.test/
|
|
~~~
|
|
|
|
Sanitize a 96-character filename stem. On collision show an editable dialog
|
|
with proposed name (2).url and Cancel; leave capture state unchanged on errors.
|
|
When opening a saved link, parse and validate URL= before invoking the URL API.
|
|
|
|
- [ ] **Step 4: Run green and commit**
|
|
|
|
Run:
|
|
|
|
~~~bash
|
|
cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/api -count=1
|
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js
|
|
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/permissions internal/api frontend/src/lib/plugin-host
|
|
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: open external URLs safely"
|
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
|
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: save browser inbox links safely"
|
|
~~~
|
|
|
|
Expected: URL API tests and link save/collision smoke tests pass.
|