docs: plan first alpha product UX
This commit is contained in:
parent
139b2a67b7
commit
a7563b6016
|
|
@ -0,0 +1,240 @@
|
|||
# Alpha Activity And Journal 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:** Make Activity a durable, background-maintained session log that safely proposes Journal entries without creating a Дело automatically.
|
||||
|
||||
**Architecture:** Desktop adds a plugin background-service lifecycle, unsubscribe-capable event bridge, and append-only Activity log. The Activity plugin owns session reconstruction and watermarks; Journal receives an explicit reviewed candidate and requires a destination for unassigned sessions.
|
||||
|
||||
**Tech Stack:** Go, Wails, Svelte, plain JavaScript plugin bundles, Go tests, Node smoke tests, Playwright.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Activity raw events are retained for 60 days, 10,000 events, or 8 MiB, whichever is reached first.
|
||||
- Raw activity uses append/compaction, not settings.json rewrites.
|
||||
- Session scope is workspaceId plus path cache or unassigned.
|
||||
- Point-event duration is capped at 10 minutes per adjacent pair; gap over 20 minutes starts another session.
|
||||
- Accepted/dismissed watermarks consume only the reviewed slice.
|
||||
- Journal save is always an explicit user action.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add background services, event unsubscription, and an append-only Activity log
|
||||
|
||||
**Files:**
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin_test.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/events/bus.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/internal/core/storage/api.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/storage/api_test.go
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/BackgroundPluginHost.svelte
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/App.svelte
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/types.ts
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/contributions.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/manifest.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/plugin-api.test.ts
|
||||
|
||||
**Interfaces:**
|
||||
- Adds contributes.backgroundServices with id and component fields.
|
||||
- Bus Subscribe returns subscription ID; Unsubscribe removes only that handler.
|
||||
- Adds ActivityLogAppend, ActivityLogRead, ActivityLogDeleteWorkspace, and ActivityLogCompact backend methods.
|
||||
- Background bundle registration exposes start(api) returning a cleanup function.
|
||||
|
||||
- [ ] **Step 1: Write failing manifest, bus, and storage tests**
|
||||
|
||||
Add a manifest validation test and isolated bus-unsubscribe test:
|
||||
|
||||
~~~go
|
||||
subA := bus.Subscribe("browser.activity.domain", handlerA)
|
||||
subB := bus.Subscribe("browser.activity.domain", handlerB)
|
||||
bus.Unsubscribe("browser.activity.domain", subA)
|
||||
bus.Publish(events.Event{Name: "browser.activity.domain"})
|
||||
if gotA != 0 || gotB != 1 { t.Fatalf("wrong selective unsubscribe: %d %d", gotA, gotB) }
|
||||
~~~
|
||||
|
||||
Add an append test asserting two appends create two NDJSON records and a
|
||||
compaction test keeping only the newest record under a small test limit.
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -run 'Test.*(Background|Unsubscribe|ActivityLog)' -count=1
|
||||
~~~
|
||||
|
||||
Expected: FAIL because subscriptions have no IDs and the log API does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement the platform boundary**
|
||||
|
||||
Define:
|
||||
|
||||
~~~go
|
||||
type ContributionBackgroundService struct {
|
||||
ID string `json:"id"`
|
||||
Component string `json:"component"`
|
||||
}
|
||||
|
||||
type ActivityLogRecord struct {
|
||||
ActivityID string `json:"activityId"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
~~~
|
||||
|
||||
Append NDJSON under plugin-data/verstak.activity/events.ndjson using a locked
|
||||
append. Compact to the declared retention limits by rewriting a temporary file.
|
||||
Keep only compact candidate state in plugin data. BackgroundPluginHost loads
|
||||
enabled plugins with a background contribution once at app startup and calls the
|
||||
registered start function; it calls cleanup on disable/reload/destroy.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -count=1
|
||||
git add internal/core/plugin internal/core/events internal/core/storage internal/api frontend/src/App.svelte frontend/src/lib/plugin-host
|
||||
git commit -m "feat: add background activity runtime"
|
||||
git -C /home/mirivlad/git/verstak2/verstak-sdk add src/types.ts schemas/contributions.json schemas/manifest.json src/plugin-api.test.ts
|
||||
git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define plugin background services"
|
||||
~~~
|
||||
|
||||
Expected: focused tests prove one subscription, selective cleanup, append, and compaction.
|
||||
|
||||
### Task 2: Rebuild Activity sessions and persistent candidate watermarks
|
||||
|
||||
**Files:**
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/plugin.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/en.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/ru.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-activity-plugin.js
|
||||
|
||||
**Interfaces:**
|
||||
- Activity background service registers ActivityService.start(api).
|
||||
- Event record includes activityId, scope, occurredAt, durationSeconds, and sessionId.
|
||||
- Candidate contains sessionId, handledThrough, estimatedMinutes, dateSlices, and source activities.
|
||||
|
||||
- [ ] **Step 1: Write failing smoke scenarios**
|
||||
|
||||
Add three explicit scenarios:
|
||||
|
||||
~~~js
|
||||
const nineMinutes = eventsAt('2026-07-12T10:00:00Z', '2026-07-12T10:09:00Z');
|
||||
assert.equal(buildSessions(nineMinutes)[0].estimatedMinutes, 9);
|
||||
const late = appendLateEvent(existingSession, eventAt('2026-07-12T09:58:00Z'));
|
||||
assert.equal(late.sessionId, existingSession.sessionId);
|
||||
assert.equal(candidateAfterDismiss.sourceActivityIds.includes('a'), false);
|
||||
~~~
|
||||
|
||||
Also assert a 23:50 to 00:30 session has one session ID and date slices for both
|
||||
local dates.
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-official-plugins
|
||||
node scripts/smoke-activity-plugin.js
|
||||
~~~
|
||||
|
||||
Expected: FAIL because Activity derives candidate IDs from first and last events.
|
||||
|
||||
- [ ] **Step 3: Implement background session state**
|
||||
|
||||
Replace view-mounted event recording with ActivityService.start(api). Persist
|
||||
immutable generated session IDs and anchors in activity-state.json. Build scope
|
||||
as either workspaceId plus root path or unassigned. Sum explicit browser
|
||||
duration and only zero-duration adjacent point intervals, cap a point interval
|
||||
at 10 minutes, split at 20 minutes, and cap a session at 120 minutes.
|
||||
|
||||
For accept or dismiss, store the ordered handledThrough watermark. New events
|
||||
after the watermark require another 10 minutes before a new candidate appears;
|
||||
late events at or before the watermark remain diagnostic only.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
node scripts/smoke-activity-plugin.js
|
||||
./scripts/check.sh
|
||||
git add plugins/activity scripts/smoke-activity-plugin.js
|
||||
git commit -m "feat: persist activity sessions and watermarks"
|
||||
~~~
|
||||
|
||||
Expected: session, late-event, dismissal, browser-duration, and midnight smoke tests pass.
|
||||
|
||||
### Task 3: Review candidates safely in Journal and expose sessions in UI
|
||||
|
||||
**Files:**
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/frontend/src/index.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/en.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/ru.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-journal-plugin.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/activity.spec.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/todo.spec.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
|
||||
|
||||
**Interfaces:**
|
||||
- Journal candidate request accepts destinationWorkspaceId only for unassigned scope.
|
||||
- Saved Journal entry stores sourceSessionId and handledThrough in addition to activity IDs.
|
||||
- Activity UI has Sessions default and Events diagnostic view.
|
||||
|
||||
- [ ] **Step 1: Write failing E2E and smoke assertions**
|
||||
|
||||
Require an unassigned candidate to show a destination selector and reject Save
|
||||
until an active workspace ID is selected:
|
||||
|
||||
~~~js
|
||||
await page.getByRole('button', { name: /review/i }).click();
|
||||
await expect(page.getByText(/choose.*дело/i)).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||
~~~
|
||||
|
||||
Require the mock WritePluginSetting path to retain sourceSessionId after reload.
|
||||
Require Clear Activity and Journal Delete to show cancellation-safe confirmation
|
||||
before changing stored rows.
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
|
||||
npm run test:e2e -- activity.spec.js todo.spec.js
|
||||
~~~
|
||||
|
||||
Expected: FAIL because the current mock discards settings and Journal only accepts path-scoped candidates.
|
||||
|
||||
- [ ] **Step 3: Implement review UI and local dates**
|
||||
|
||||
Render domain rows as hostname plus duration without URLs. Keep raw event
|
||||
details in the Events tab. For unassigned scope, list active workspace IDs and
|
||||
require a selection before handing the candidate to Journal. Preselect the
|
||||
largest date slice, show both date slices, use local date conversion, and save
|
||||
the session watermark with the Journal entry. Show a visible action if Journal
|
||||
is disabled instead of silently dropping the request. Put a confirm/cancel
|
||||
dialog in front of Clear Activity and Journal Delete; cancelling leaves the
|
||||
append log, candidate state, and entry list untouched.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-activity-plugin.js && node scripts/smoke-journal-plugin.js
|
||||
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- activity.spec.js todo.spec.js
|
||||
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/activity plugins/journal scripts/smoke-activity-plugin.js scripts/smoke-journal-plugin.js
|
||||
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: review activity sessions in journal"
|
||||
git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/activity.spec.js frontend/e2e/todo.spec.js frontend/src/lib/test/wails-mock.js
|
||||
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover activity journal review flow"
|
||||
~~~
|
||||
|
||||
Expected: candidate-to-Journal, local date, and persisted mock workflows pass.
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
# Alpha Browser Domain Activity 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:** Add explicitly consented, privacy-minimal active-tab domain timing to the browser extension and deliver immutable activity batches to Desktop.
|
||||
|
||||
**Architecture:** Manual captures remain unchanged. A new tracker stores mutable accumulators, immutable pending batches, and acknowledged IDs locally; Desktop accepts a separate authenticated activity endpoint and emits browser.activity.domain.
|
||||
|
||||
**Tech Stack:** Plain WebExtension JavaScript, Node tests, Go HTTP handler tests, shared JSON vectors.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Passive tracking defaults to disabled and requires explicit informed consent.
|
||||
- Track only normalized hostname plus duration for an active tab in a focused window.
|
||||
- Never send URL, title, text, page content, keystrokes, or history.
|
||||
- A pending batch is byte-for-byte immutable; acknowledge only its own ID.
|
||||
- Discard ambiguous or negative clock deltas and gaps over 10 minutes.
|
||||
- Use hostname-normalization-v1 for extension, Desktop receiver, exclusions, and bindings.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Define and test canonical hostname normalization
|
||||
|
||||
**Files:**
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname.js
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-hostname.js
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize.go
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize_test.go
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json
|
||||
|
||||
**Interfaces:**
|
||||
- Produces JavaScript normalizeHostnameV1(value, mode).
|
||||
- Produces Go hostname.NormalizeV1(value string, mode Mode) (string, error).
|
||||
- Mode is URLSource for HTTP(S) URLs and BareHost for binding/exclusion input.
|
||||
|
||||
- [ ] **Step 1: Write the shared vector corpus and failing tests**
|
||||
|
||||
Include vectors for Unicode/punycode, trailing dot, port stripping/rejection,
|
||||
IPv4, bracketed IPv6, localhost, one-label internal names, malformed labels,
|
||||
and an overlong name:
|
||||
|
||||
~~~json
|
||||
[
|
||||
{"mode":"bare","input":"пример.рф.","want":"xn--e1afmkfd.xn--p1ai"},
|
||||
{"mode":"url","input":"https://Example.COM:8443/a","want":"example.com"},
|
||||
{"mode":"bare","input":"example.com:8443","error":"port"},
|
||||
{"mode":"bare","input":"[::1]","want":"::1"}
|
||||
]
|
||||
~~~
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-browser-extension
|
||||
node scripts/test-hostname.js
|
||||
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1
|
||||
~~~
|
||||
|
||||
Expected: both fail because the normalizers do not exist.
|
||||
|
||||
- [ ] **Step 3: Implement both normalizers**
|
||||
|
||||
Use the browser URL parser plus ASCII IDNA conversion in JavaScript. Use
|
||||
net/url, net/netip, and golang.org/x/net/idna.Lookup.ToASCII in Go. Store
|
||||
ASCII A-labels, lowercase names, no ports or IPv6 brackets, and reject malformed
|
||||
or over-limit values.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-browser-extension && npm test
|
||||
cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1
|
||||
cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json
|
||||
cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json
|
||||
git -C /home/mirivlad/git/verstak2/verstak-sdk add schemas/hostname-normalization-v1.json
|
||||
git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define hostname normalization vectors"
|
||||
git -C /home/mirivlad/git/verstak2/verstak-browser-extension add shared/hostname.js shared/hostname-normalization-v1.json scripts/test-hostname.js
|
||||
git -C /home/mirivlad/git/verstak2/verstak-browser-extension commit -m "feat: normalize activity hostnames"
|
||||
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/hostname
|
||||
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: normalize browser hostnames"
|
||||
~~~
|
||||
|
||||
Expected: every copied corpus passes in both implementations.
|
||||
|
||||
### Task 2: Add a separate authenticated Desktop activity receiver
|
||||
|
||||
**Files:**
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver_test.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
||||
|
||||
**Interfaces:**
|
||||
- Accepts POST /activities with BrowserDomainActivity.
|
||||
- Emits browser.activity.domain with normalized hostname, duration, ID, and optional workspaceId.
|
||||
- Returns status accepted and activityId for first delivery and duplicate delivery.
|
||||
|
||||
- [ ] **Step 1: Write failing handler tests**
|
||||
|
||||
Create a valid immutable request and then retry the same ID:
|
||||
|
||||
~~~go
|
||||
payload := "{\"schemaVersion\":1,\"activityId\":\"a-1\",\"hostname\":\"пример.рф\",\"durationSeconds\":300,\"startedAt\":\"2026-07-12T10:00:00Z\",\"endedAt\":\"2026-07-12T10:05:00Z\"}"
|
||||
req := httptest.NewRequest(http.MethodPost, "/activities", strings.NewReader(payload))
|
||||
req.Header.Set("X-Verstak-Receiver-Token", "pair-token")
|
||||
~~~
|
||||
|
||||
Assert invalid tokens, URL-like hostnames, zero or oversize duration, and a
|
||||
duplicate ID do not publish a second event.
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver -run Activity -count=1
|
||||
~~~
|
||||
|
||||
Expected: FAIL because /activities is not routed.
|
||||
|
||||
- [ ] **Step 3: Implement the bounded idempotent route**
|
||||
|
||||
Add BrowserDomainActivity, a 30-day bounded receiver ID cache, and
|
||||
handleActivity which invokes hostname.NormalizeV1. Do not reuse capture types
|
||||
or persistence. In App, resolve an active exact hostname binding to workspaceId
|
||||
and a path cache before Activity receives the event.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver ./internal/api -count=1
|
||||
git add internal/core/browserreceiver/receiver.go internal/core/browserreceiver/receiver_test.go internal/api/app.go internal/api/app_test.go
|
||||
git commit -m "feat: receive browser domain activity"
|
||||
~~~
|
||||
|
||||
Expected: authentication, normalization, idempotency, and binding tests pass.
|
||||
|
||||
### Task 3: Build the opt-in extension tracker and immutable delivery queue
|
||||
|
||||
**Files:**
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/activity-tracker.js
|
||||
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-activity-tracker.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/api.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/background.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.html
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.css
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/en.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/ru.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/chromium/manifest.json
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/firefox/manifest.json
|
||||
|
||||
**Interfaces:**
|
||||
- Persists activeAccumulator, pendingBatches, and acknowledgedIds under verstak.activityTracker.
|
||||
- Produces sendActivity(receiverUrl, token, immutablePayload).
|
||||
- Settings include passiveActivityEnabled false and excludedDomains empty.
|
||||
|
||||
- [ ] **Step 1: Write failing state-machine tests**
|
||||
|
||||
Cover consent, active-tab-only timing, long-gap discard, and the A/B batch case:
|
||||
|
||||
~~~js
|
||||
tracker.addElapsed('example.com', 600, start, end);
|
||||
const batchA = tracker.freeze('example.com');
|
||||
tracker.addElapsed('example.com', 300, laterStart, laterEnd);
|
||||
assert.equal(tracker.pendingBatches[0].payload.durationSeconds, 600);
|
||||
tracker.acknowledge(batchA.activityId);
|
||||
assert.equal(tracker.activeAccumulator['example.com'].durationSeconds, 300);
|
||||
~~~
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-browser-extension
|
||||
node scripts/test-activity-tracker.js
|
||||
~~~
|
||||
|
||||
Expected: FAIL because the state machine is absent.
|
||||
|
||||
- [ ] **Step 3: Implement tracker, disclosure, and listeners**
|
||||
|
||||
Implement the tracker as pure functions. In background.js, register
|
||||
tabs.onActivated, tabs.onUpdated, windows.onFocusChanged, idle.onStateChanged,
|
||||
and a five-minute alarm only while opted in. Set idle detection to 600 seconds.
|
||||
On a gap over 600 seconds, clock rollback, lock, or browser startup, reset the
|
||||
active checkpoint without adding time. Retry frozen batches oldest-first and
|
||||
remove only matching IDs.
|
||||
|
||||
Show an unchecked Russian/English consent explanation in settings. Add windows,
|
||||
alarms, and idle permissions to both manifests. Keep manual capture queue and
|
||||
popup actions unchanged.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
npm test
|
||||
node scripts/test-activity-tracker.js
|
||||
git add shared chromium/manifest.json firefox/manifest.json scripts/test-activity-tracker.js
|
||||
git commit -m "feat: add opt-in browser domain tracker"
|
||||
~~~
|
||||
|
||||
Expected: tracker unit tests and existing extension tests pass.
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
# 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.
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
# Alpha Shell UX 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:** Make the Desktop overview scoped and actionable, hide diagnostic plugin IDs by default, and remove alpha UI strings that expose implementation noise.
|
||||
|
||||
**Architecture:** The shell consumes stable workspace IDs and plugin capabilities rather than hard-coded namespaces. Overview builds deterministic case-scoped projections; debug display is a presentation preference plus a session-only command-line override.
|
||||
|
||||
**Tech Stack:** Svelte, Wails app settings, Go tests, Playwright E2E, existing i18n catalogs.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Default UI labels are Russian and use Дела, Входящие, Активности, Журнал.
|
||||
- A normal tab never shows a plugin ID.
|
||||
- Settings Debug Show plugin IDs defaults false; Desktop --debug enables it only for that run.
|
||||
- Overview never leaks an unscoped event into every case.
|
||||
- Todo entries are included only if todo.workspace capability is loaded.
|
||||
- No action opens or creates a Дело implicitly.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add effective debug display setting and human plugin tab labels
|
||||
|
||||
**Files:**
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/main.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager_test.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/shell/WorkspaceHost.svelte
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-manager/PluginManager.svelte
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/plugin-api-bridge.spec.js
|
||||
|
||||
**Interfaces:**
|
||||
- Persisted app setting showPluginIds bool defaults false.
|
||||
- GetAppSettings returns effectiveShowPluginIds and UpdateAppSettings accepts showPluginIds.
|
||||
- Runtime --debug sets effectiveShowPluginIds true without persisting it.
|
||||
|
||||
- [ ] **Step 1: Write failing app-setting and E2E tests**
|
||||
|
||||
Add a Go test proving the setting preserves theme and language, plus a browser
|
||||
test proving normal mode has no raw ID and debug mode has one:
|
||||
|
||||
~~~js
|
||||
await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.' })).toHaveCount(0);
|
||||
await mockAppSettings({ showPluginIds: true });
|
||||
await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.default-editor' })).toHaveCount(1);
|
||||
~~~
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -run 'Test.*PluginIds' -count=1
|
||||
cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js
|
||||
~~~
|
||||
|
||||
Expected: FAIL because no display preference exists.
|
||||
|
||||
- [ ] **Step 3: Implement presentation-only debug state**
|
||||
|
||||
Parse --debug before Wails startup and keep it in App runtime state. Merge it
|
||||
with the persisted bool when returning effective settings. Render the localized
|
||||
manifest title normally; append the plugin ID only when effectiveShowPluginIds
|
||||
is true. Add the Debug settings checkbox and localized explanatory copy.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -count=1
|
||||
cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js
|
||||
git add main.go internal/core/appsettings internal/api frontend/src frontend/e2e/plugin-api-bridge.spec.js
|
||||
git commit -m "feat: hide plugin IDs outside debug mode"
|
||||
~~~
|
||||
|
||||
Expected: normal and debug labels are both verified.
|
||||
|
||||
### Task 2: Implement deterministic case-scoped Overview
|
||||
|
||||
**Files:**
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/TodaySurface.svelte
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/WorkspaceHost.svelte
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-today.spec.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-p0.spec.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
|
||||
|
||||
**Interfaces:**
|
||||
- Every overview row has workspaceId and lastMeaningfulAt.
|
||||
- Needs attention maximum is five; Continue work maximum is four; Recent changes maximum is eight.
|
||||
- Hidden recommendation state is keyed by entity ID and invalidated by later meaningful change.
|
||||
|
||||
- [ ] **Step 1: Write failing scope and ranking tests**
|
||||
|
||||
Add fixture events for two workspace IDs plus an unscoped event:
|
||||
|
||||
~~~js
|
||||
await seedOverview({
|
||||
selectedWorkspaceId: 'client-a',
|
||||
events: [
|
||||
{ activityId: 'a', workspaceId: 'client-a', type: 'note.saved', occurredAt: now },
|
||||
{ activityId: 'b', workspaceId: 'client-b', type: 'file.changed', occurredAt: now },
|
||||
{ activityId: 'global', type: 'workspace.selected', occurredAt: now }
|
||||
]
|
||||
});
|
||||
await expect(page.getByText(/client-b/i)).toHaveCount(0);
|
||||
await expect(page.locator('[data-overview-section="continue"] [data-overview-item]')).toHaveCount(4);
|
||||
~~~
|
||||
|
||||
Also assert no selection shows only the select/create prompt and up to five
|
||||
unassigned active Inbox captures.
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
|
||||
npm run test:e2e -- ux-today.spec.js ux-p0.spec.js
|
||||
~~~
|
||||
|
||||
Expected: FAIL because rowsFor currently accepts untagged events for every workspace.
|
||||
|
||||
- [ ] **Step 3: Implement exact overview projections**
|
||||
|
||||
Replace path-only filtering with workspaceId equality. Build:
|
||||
|
||||
1. Needs attention: ready Journal candidates, active unprocessed captures, then
|
||||
urgent Todo rows only if todo.workspace capability exists; take five.
|
||||
2. Continue work: distinct entities updated in 14 days; sort by
|
||||
lastMeaningfulAt descending and the documented type tie-break; take four.
|
||||
3. Recent changes: note save/create, file create/rename or last change per
|
||||
file, saved link, and Journal create in seven days; take eight.
|
||||
|
||||
Persist Hide recommendation without changing underlying data. Use localized
|
||||
empty states and remove English technical strings.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
npm run test:e2e -- ux-today.spec.js ux-p0.spec.js
|
||||
git add src/lib/shell/TodaySurface.svelte src/lib/shell/WorkspaceHost.svelte src/lib/i18n frontend/e2e/ux-today.spec.js frontend/e2e/ux-p0.spec.js src/lib/test/wails-mock.js
|
||||
git commit -m "feat: make overview case-scoped and actionable"
|
||||
~~~
|
||||
|
||||
Expected: limits, scope, empty state, and recommendation hide tests pass.
|
||||
|
||||
### Task 3: Verify alpha UX as a user sees it
|
||||
|
||||
**Files:**
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-followup.spec.js
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/scripts/check-gui.sh
|
||||
|
||||
**Interfaces:**
|
||||
- E2E fixture uses effectiveShowPluginIds false by default.
|
||||
- GUI checker accepts a --debug-labels case.
|
||||
|
||||
- [ ] **Step 1: Write the final failing alpha scenarios**
|
||||
|
||||
Add a scenario that creates a capture, assigns it, archives it globally, opens
|
||||
the assigned case, and verifies the capture is still present. Add a session
|
||||
candidate path to Journal and assert normal tab labels are human-readable.
|
||||
|
||||
~~~js
|
||||
await archiveGlobalCapture('capture-1');
|
||||
await openWorkspace('client-a');
|
||||
await expect(page.getByTestId('browser-capture-capture-1')).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: /verstak\./i })).toHaveCount(0);
|
||||
~~~
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
npm run test:e2e -- ux-followup.spec.js
|
||||
~~~
|
||||
|
||||
Expected: FAIL until the prior plans are integrated.
|
||||
|
||||
- [ ] **Step 3: Add deterministic GUI smoke commands**
|
||||
|
||||
Extend check-gui.sh to start an isolated test vault, run normal and --debug
|
||||
screens, and save screenshots under frontend/e2e-results. The script must not
|
||||
read or mutate the user's normal vault.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
npm run test:e2e -- ux-followup.spec.js
|
||||
GOCACHE=/tmp/verstak-go-cache ./scripts/check-gui.sh
|
||||
git add frontend/e2e/ux-followup.spec.js scripts/check-gui.sh
|
||||
git commit -m "test: cover first alpha UX smoke flow"
|
||||
~~~
|
||||
|
||||
Expected: E2E and isolated GUI smoke pass.
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
# Alpha Workspace Identity 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:** Give every managed Дело a durable UUID identity and publish lifecycle events that let alpha features follow a case across rename, Trash, restore, and path reuse.
|
||||
|
||||
**Architecture:** The filesystem remains the source of truth for whether a case folder exists. A UUID marker inside each case is the relation identity; Desktop metadata indexes that marker and paths remain presentation caches.
|
||||
|
||||
**Tech Stack:** Go, Wails bindings, JSON metadata, Go tests.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- A new Дело is created only by an explicit user action.
|
||||
- workspaceId is UUID v4; a path is never relation identity.
|
||||
- The marker is .verstak/workspace.json inside the case.
|
||||
- Existing writable cases migrate without data loss; non-writable cases are view-only.
|
||||
- Use TDD and make one commit per task.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Store and resolve durable workspace identities
|
||||
|
||||
**Files:**
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go
|
||||
|
||||
**Interfaces:**
|
||||
- Produces Workspace.ID string and Metadata.WorkspaceID string.
|
||||
- Produces func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error).
|
||||
- Produces func (m *Manager) ListWorkspaceIdentities() ([]WorkspaceIdentity, error).
|
||||
|
||||
- [ ] **Step 1: Write the failing identity tests**
|
||||
|
||||
Add tests proving creation writes a UUID marker, rename retains it, and a newly
|
||||
created folder after external removal gets another UUID:
|
||||
|
||||
~~~go
|
||||
ws, err := m.CreateWorkspace("Clients", "default")
|
||||
if err != nil { t.Fatal(err) }
|
||||
first, err := m.EnsureWorkspaceIdentity(ws.Name)
|
||||
if err != nil || first.WorkspaceID == "" { t.Fatalf("identity = %+v, %v", first, err) }
|
||||
if err := m.RenameWorkspace("Clients", "Clients-2026"); err != nil { t.Fatal(err) }
|
||||
renamed, _ := m.EnsureWorkspaceIdentity("Clients-2026")
|
||||
if renamed.WorkspaceID != first.WorkspaceID { t.Fatal("rename changed workspace ID") }
|
||||
~~~
|
||||
|
||||
- [ ] **Step 2: Run the focused test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -run 'Test.*Workspace.*Identity' -count=1
|
||||
~~~
|
||||
|
||||
Expected: FAIL because WorkspaceIdentity and EnsureWorkspaceIdentity do not exist.
|
||||
|
||||
- [ ] **Step 3: Write the minimal marker implementation**
|
||||
|
||||
Add these types and use them in CreateWorkspace, GetWorkspaceMetadata, and
|
||||
workspace listing:
|
||||
|
||||
~~~go
|
||||
type WorkspaceIdentity struct {
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
RootPath string `json:"rootPath"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
const workspaceIdentityRelativePath = ".verstak/workspace.json"
|
||||
|
||||
func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error) {
|
||||
// Validate the folder, read the in-folder marker, and atomically create
|
||||
// a UUID-v4 marker only when absent and writable.
|
||||
}
|
||||
~~~
|
||||
|
||||
Write the same ID to central metadata only as an index. Never recover an ID from
|
||||
stale path-keyed metadata when the in-folder marker is absent.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -count=1
|
||||
git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go
|
||||
git commit -m "feat: add durable workspace identities"
|
||||
~~~
|
||||
|
||||
Expected: tests pass and the commit contains only workspace identity changes.
|
||||
|
||||
### Task 2: Expose identity and lifecycle events through Desktop
|
||||
|
||||
**Files:**
|
||||
- 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/App.svelte
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/api/App.d.ts
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/models.ts
|
||||
|
||||
**Interfaces:**
|
||||
- Produces ListWorkspaceIdentities() ([]workspace.WorkspaceIdentity, string).
|
||||
- Lifecycle payloads include workspaceId and workspaceRootPath; Trash also includes trashId.
|
||||
- Produces workspace.restored and workspace.purged events.
|
||||
|
||||
- [ ] **Step 1: Add failing API tests**
|
||||
|
||||
Extend TestWorkspaceAPIPublishesLifecycleEvents:
|
||||
|
||||
~~~go
|
||||
if got := received["workspace.created"]["workspaceId"]; got == "" {
|
||||
t.Fatal("workspace.created must include workspaceId")
|
||||
}
|
||||
if got := received["workspace.renamed"]["workspaceId"]; got != createdID {
|
||||
t.Fatalf("rename ID = %v, want %s", got, createdID)
|
||||
}
|
||||
~~~
|
||||
|
||||
Add restore and purge tests around RestoreVaultTrash and DeleteVaultTrash that
|
||||
assert the original UUID and trash ID are emitted.
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'TestWorkspaceAPI.*Lifecycle' -count=1
|
||||
~~~
|
||||
|
||||
Expected: FAIL because lifecycle payloads only identify a path.
|
||||
|
||||
- [ ] **Step 3: Publish complete identity lifecycle**
|
||||
|
||||
Resolve the marker before publishing create, rename, selected, and trashed
|
||||
events. Detect workspace Trash metadata in RestoreVaultTrash and
|
||||
DeleteVaultTrash, then publish:
|
||||
|
||||
~~~go
|
||||
map[string]interface{}{
|
||||
"operation": "restore",
|
||||
"workspaceId": identity.WorkspaceID,
|
||||
"workspaceRootPath": restoredRoot,
|
||||
"trashId": trashID,
|
||||
}
|
||||
~~~
|
||||
|
||||
Change App.svelte workspace-node conversion to preserve workspace.id while using
|
||||
rootPath only for selection and display.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/api ./internal/core/workspace -count=1
|
||||
git add internal/api/app.go internal/api/app_test.go frontend/src/App.svelte frontend/wailsjs/go/api/App.d.ts frontend/wailsjs/go/models.ts
|
||||
git commit -m "feat: publish workspace identity lifecycle"
|
||||
~~~
|
||||
|
||||
Expected: focused API and workspace tests pass.
|
||||
|
||||
### Task 3: Migrate legacy references and repair duplicate IDs
|
||||
|
||||
**Files:**
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
||||
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
||||
|
||||
**Interfaces:**
|
||||
- Produces RepairWorkspaceIdentity(keepRootPath, regenerateRootPath string) string.
|
||||
- Identity states are active, unavailable, or duplicate.
|
||||
- Later Inbox and Activity plans consume workspaceId through ListWorkspaceIdentities.
|
||||
|
||||
- [ ] **Step 1: Write failing duplicate and legacy tests**
|
||||
|
||||
Create two folders with copied marker data and assert neither is an active
|
||||
relation target. Create a legacy path-keyed capture fixture and assert a
|
||||
resolved path gets an ID while a missing path becomes unavailable:
|
||||
|
||||
~~~go
|
||||
if errStr := app.RepairWorkspaceIdentity("Original", "Copied"); errStr != "" {
|
||||
t.Fatal(errStr)
|
||||
}
|
||||
if originalID == copiedID { t.Fatal("repair must issue a new ID") }
|
||||
~~~
|
||||
|
||||
- [ ] **Step 2: Run red**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -run 'Test.*(Duplicate|Legacy).*Workspace' -count=1
|
||||
~~~
|
||||
|
||||
Expected: FAIL because copied markers are accepted as ordinary folders.
|
||||
|
||||
- [ ] **Step 3: Implement deterministic repair**
|
||||
|
||||
Group active roots by marker UUID during identity listing. Mark a group with
|
||||
more than one root duplicate and exclude it from assignment targets.
|
||||
RepairWorkspaceIdentity verifies the duplicate, keeps the first marker, creates
|
||||
a new marker for the second root, and moves no relation data.
|
||||
|
||||
- [ ] **Step 4: Run green and commit**
|
||||
|
||||
Run:
|
||||
|
||||
~~~bash
|
||||
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -count=1
|
||||
git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go internal/api/app.go internal/api/app_test.go
|
||||
git commit -m "feat: repair duplicate workspace identities"
|
||||
~~~
|
||||
|
||||
Expected: legacy, path-reuse, and duplicate-ID tests pass.
|
||||
Loading…
Reference in New Issue