Skip to content

Commit 5ef284d

Browse files
fix: workspace identity on changing for same repo.
1 parent e95b5d3 commit 5ef284d

30 files changed

Lines changed: 3508 additions & 479 deletions

docs/idb-workspace-state.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Each workspace record may include:
1717
- `id`
1818
- `createdAt`
1919
- `lastModified`
20+
- `workspaceScope` (`local` | `repository`)
2021
- Repository and PR context:
2122
- `repo`
2223
- `base`
@@ -44,3 +45,9 @@ IDB supports that by storing:
4445
If a value is required to accurately restore PR/workspace behavior after reload, it must live in IDB records.
4546

4647
`localStorage` should only mirror user preferences and lightweight bootstrap values.
48+
49+
## Behavioral Spec
50+
51+
For action-level drawer semantics and state machine behavior, see:
52+
53+
- `docs/workspaces-behavior-algorithm.md`

docs/localstorage-state.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ Examples that must stay out of `localStorage`:
3232
If data is needed to restore workspace or pull request workflow state, it belongs in IndexedDB workspace records.
3333

3434
Repository selection is derived from in-memory BYOT controls and IndexedDB-backed workspace records, not from a dedicated localStorage key.
35+
36+
For the Workspaces drawer action/state algorithm, see:
37+
38+
- `docs/workspaces-behavior-algorithm.md`
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Workspaces Drawer Behavior Algorithm
2+
3+
This document locks in the intended behavior for Workspaces drawer actions.
4+
5+
## Goals
6+
7+
- Keep action semantics explicit and predictable.
8+
- Keep button visibility state-driven and mutually exclusive.
9+
- Preserve workspace restore behavior by persisting state in IndexedDB.
10+
11+
## Core Terms
12+
13+
- `Local` scope: Workspaces whose `workspaceScope` is `local`.
14+
- `Repository` scope: Workspaces whose `workspaceScope` is `repository` and whose `repo` matches the selected repository filter.
15+
- `workspaceKey`: Derived identity key from repository + head branch. Used for matching/preference logic, not for UI policy by itself.
16+
17+
## Required Invariants
18+
19+
1. `Initialize` and `New workspace` must never be visible at the same time.
20+
2. `Local` scope never shows `Initialize`.
21+
3. `Initialize` for non-Local empty scope updates the active workspace in place (no fork).
22+
4. `New workspace` always forks from current editor/runtime state into a new record id.
23+
5. Fork creation must generate a fresh head branch suffix so `workspaceKey` and visible labels are distinct.
24+
6. Any workspace created via `New workspace` must persist with `prContextState = "inactive"`.
25+
7. For `New workspace`, `workspaceScope` is target-dependent:
26+
- `local` when repository filter is `__local__`
27+
- `repository` when repository filter is a non-local `owner/repo`
28+
29+
## UI State Machine
30+
31+
State is derived from:
32+
33+
- Selected repository filter (`__local__` vs non-local `owner/repo`)
34+
- Presence of stored workspaces in the selected scope
35+
36+
States:
37+
38+
1. `local-empty`
39+
- Show: `New workspace`
40+
- Hide: `Initialize`, workspace select, `Open`, `Remove`
41+
2. `local-with-workspaces`
42+
- Show: `New workspace`, workspace select, `Open`, `Remove`
43+
- Hide: `Initialize`
44+
3. `repository-empty`
45+
- Show: `Initialize`
46+
- Hide: `New workspace`, workspace select, `Open`, `Remove`
47+
4. `repository-with-workspaces`
48+
- Show: `New workspace`, workspace select, `Open`, `Remove`
49+
- Hide: `Initialize`
50+
51+
## Action Semantics
52+
53+
### A) Local + New workspace
54+
55+
- Action: fork current workspace into a new record.
56+
- Persisted updates:
57+
- `workspaceScope = "local"`
58+
- `prContextState = "inactive"`
59+
- `repo = ""`
60+
- fresh `id`
61+
- fresh local `head` (suffix-appended)
62+
- `workspaceKey = local::<fresh-head>`
63+
64+
### B) Non-Local + Initialize (no stored workspaces in selected repository)
65+
66+
- Action: update active workspace in place to selected repository scope.
67+
- Persisted updates on current record:
68+
- `workspaceScope = "repository"`
69+
- `repo = <selected owner/repo>`
70+
- `workspaceKey = <selected owner/repo>::<current head>`
71+
- Must preserve current record id.
72+
73+
### C) Non-Local + New workspace (stored workspaces exist)
74+
75+
- Action: fork current workspace into a new repository-scoped record.
76+
- Persisted updates:
77+
- `workspaceScope = "repository"`
78+
- `prContextState = "inactive"`
79+
- `repo = <selected owner/repo>`
80+
- fresh `id`
81+
- fresh repository `head` (suffix-appended from current head)
82+
- `workspaceKey = <selected owner/repo>::<head>`
83+
84+
## Storage Notes
85+
86+
- Canonical workflow state lives in IndexedDB (`prWorkspaces` records).
87+
- `localStorage` must not own repository/workspace workflow state.
88+
89+
## Regression Coverage Expectations
90+
91+
At minimum, tests should verify:
92+
93+
1. Local `New workspace` creates a new record and distinct local label/key.
94+
2. Non-local empty scope shows only `Initialize` and updates active record in place.
95+
3. Non-local scope with records shows only `New workspace` and forks new record.
96+
4. `Initialize` and `New workspace` never coexist.

playwright/github-byot-ai.spec.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -768,17 +768,16 @@ test('BYOT remembers selected repository across reloads', async ({ page }) => {
768768

769769
await page.getByRole('button', { name: 'Workspaces' }).click()
770770
const workspaceRepositoryFilter = page.getByLabel('Workspace repository filter')
771-
const newWorkspaceButton = page.getByRole('button', {
772-
name: 'New workspace',
771+
const initializeButton = page.getByRole('button', {
772+
name: 'Initialize',
773773
exact: true,
774774
})
775775
await expect(workspaceRepositoryFilter).toBeVisible()
776776
await workspaceRepositoryFilter.selectOption('knightedcodemonkey/develop')
777777
await expect(workspaceRepositoryFilter).toHaveValue('knightedcodemonkey/develop')
778778

779-
await expect(newWorkspaceButton).toBeVisible()
780-
await newWorkspaceButton.click()
781-
await page.getByRole('button', { name: 'Close workspaces drawer' }).click()
779+
await expect(initializeButton).toBeVisible()
780+
await initializeButton.click()
782781

783782
await ensureOpenPrDrawerOpen(page)
784783
await expect(repoSelect).toHaveValue('knightedcodemonkey/develop')

0 commit comments

Comments
 (0)