Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Keep the mental model high-level:
- `src/nodes/` — feature modules for each node type (schema, fields, generator, helpers)
- `src/store/` — Zustand stores and workflow-generation state
- `src/lib/` — cross-cutting utilities: persistence, generation, registries, validation, OpenCode client, marketplace helpers
- `src/lib/storage/` — pluggable storage provider abstraction (interface, local filesystem impl, factory)
- `src/hooks/` — reusable editor and data hooks
- `src/types/` — shared type definitions
- `docs/tasks/` — task-specific plans and notes
Expand Down Expand Up @@ -88,6 +89,13 @@ Keep the mental model high-level:
- Export targets currently include `OpenCode`, `PI`, and `Claude Code`.
- Generated output names are sanitized from workflow or node names; preserve existing sanitization helpers rather than duplicating naming logic.

### Storage abstraction
- All server-side file I/O flows through a pluggable `StorageProvider` interface defined in `src/lib/storage/`.
- The default provider is `LocalFilesystemProvider` (local filesystem). Selection is driven by the `NEXUS_STORAGE_PROVIDER` env var (default: `"local"`), with `NEXUS_STORAGE_ROOT` controlling the base path.
- Use `getStorageProvider()` from `@/lib/storage` to obtain the active provider. Do not import `node:fs/promises` directly for storage operations.
- To add a new storage backend (S3, Azure Blob, etc.), implement the `StorageProvider` interface and register it via `registerStorageProvider()`.
- `workspace/server.ts`, `workspace/snapshots.ts`, `brain/server.ts`, and `collaboration/object-store.ts` all use the storage abstraction.

### OpenCode integration
- OpenCode support is optional.
- Keep offline/editor-only flows working even when OpenCode is disconnected.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# E2E Test Specification: Generic File Storage

## User Story

As a user, I can create workspaces, save workflows, and perform all storage operations without noticing any change — the generic storage layer is transparent.

## Preconditions

- Application is running at `http://localhost:3000`
- No `NEXUS_STORAGE_PROVIDER` env var is set (defaults to `"local"`)
- The `.nexus-brain` data directory is clean or in a known state

## Test Steps

1. **Navigate to the app** at `http://localhost:3000`
2. **Create a new workspace** via the workspace picker
- Screenshot: after workspace creation
3. **Add a workflow** to the workspace
4. **Add a Start node** and an **Agent node** to the canvas
5. **Save the workflow** (Ctrl+S)
- Screenshot: after workflow save
6. **Refresh the page** to verify the workflow persists
- Screenshot: after page refresh showing persisted data
7. **Delete the workflow**
8. **Verify it no longer appears** in the workspace
- Screenshot: after deletion

## Success Criteria

- All workspace and workflow CRUD operations work identically to before the refactor
- No regressions in save, load, delete, or snapshot behavior
- The storage abstraction is completely transparent to the user — no UI changes, no new error states
- Existing data created before the refactor (if any) continues to load correctly

## Screenshot Capture Points

1. After workspace creation — shows the new workspace in the picker
2. After workflow save — shows the workflow with nodes on canvas
3. After page refresh — confirms data was persisted and reloaded
4. After deletion — confirms the workflow is removed from the workspace

## Additional Verification

- Check that `.nexus-brain/workspaces/` directory structure matches the expected layout (manifest.json, workflows/*.json, snapshots/)
- Verify that Brain documents (if the Knowledge Brain feature is in use) continue to save and load correctly
- Confirm that the collaboration server (if running) still persists room state via the storage provider

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# E2E Test Specification: Workspace Recent Changes Panel

## User Story
Validate that a returning user sees a changes panel on the workspace dashboard showing node-level changes made by other users since their last visit.

## Preconditions
- The application is running at `http://localhost:3000`.
- No pre-existing workspace data (clean slate or known workspace ID).

## Test Steps

### Setup via API

1. **Create a workspace** via `POST /api/workspaces` with body `{ "name": "E2E Changes Test" }`. Capture `workspace.id`.
2. **Create a workflow** via `POST /api/workspaces/{id}/workflows` with body `{ "name": "My Workflow" }`. Capture `workflow.id`.
3. **Save workflow with initial nodes** via `PUT /api/workspaces/{id}/workflows/{wid}` with body:
```json
{
"lastModifiedBy": "Alice",
"data": {
"name": "My Workflow",
"nodes": [
{ "id": "n1", "type": "start", "position": { "x": 0, "y": 0 }, "data": { "type": "start", "label": "Start", "name": "Start" } },
{ "id": "n2", "type": "prompt", "position": { "x": 200, "y": 0 }, "data": { "type": "prompt", "label": "Ask Question", "name": "Ask Question", "promptText": "", "detectedVariables": [], "brainDocId": null } }
],
"edges": [],
"ui": { "sidebarOpen": true, "minimapVisible": false, "viewport": { "x": 0, "y": 0, "zoom": 1 } }
}
}
```
4. **Wait briefly** (500ms), then **save again** with an added node and `lastModifiedBy: "Bob"`:
```json
{
"lastModifiedBy": "Bob",
"data": {
"name": "My Workflow",
"nodes": [
{ "id": "n1", "type": "start", "position": { "x": 0, "y": 0 }, "data": { "type": "start", "label": "Start", "name": "Start" } },
{ "id": "n2", "type": "prompt", "position": { "x": 200, "y": 0 }, "data": { "type": "prompt", "label": "Ask Question", "name": "Ask Question", "promptText": "", "detectedVariables": [], "brainDocId": null } },
{ "id": "n3", "type": "script", "position": { "x": 400, "y": 0 }, "data": { "type": "script", "label": "Process Data", "name": "Process Data", "promptText": "", "detectedVariables": [] } }
],
"edges": [],
"ui": { "sidebarOpen": true, "minimapVisible": false, "viewport": { "x": 0, "y": 0, "zoom": 1 } }
}
}
```

### Browser Test Steps

5. **Set localStorage** key `nexus:workspace-last-seen:{workspaceId}` to a timestamp **before** both saves (e.g., 1 hour ago).
6. **Navigate** to `/workspace/{workspaceId}`.
7. **Assert** the changes panel slides in from the right side of the viewport.
8. **Assert** the panel header shows a change count and "since {formatted date}".
9. **Assert** the workflow name "My Workflow" appears as a group header in the panel.
10. **Assert** individual change events show correct user names ("Alice", "Bob") and node names ("Start", "Ask Question", "Process Data").
11. **Assert** colored initial badges are visible (round circles with first letter of user name).
12. **Click "Dismiss"** (the X button) — assert the panel slides out and is no longer visible.
13. **Reload the page** — assert the panel re-appears (last-seen was updated on the prior load, but the saves still happened after the original `since` time set in step 5; however, the new `since` from the markSeen call means only changes after the previous page load would show — depending on timing, panel may or may not appear. To guarantee it appears, reset localStorage again before reload).
14. **Screenshot capture** at: panel visible state, after dismiss.

### No-Changes Scenario

15. **Set localStorage** `nexus:workspace-last-seen:{workspaceId}` to the **current** time.
16. **Reload** the page.
17. **Assert** no changes panel appears.

## Success Criteria
- Panel appears with correct change data grouped by workflow.
- Dismiss works — panel slides out and does not re-appear for the rest of the session.
- Colored initial badges use consistent color hashing (same name = same color).
- The `node_added` events for "Start", "Ask Question" (from Alice's save) and "Process Data" (from Bob's save) are all shown.
- No `node_moved` events appear when only position changes occur.
- Panel does not appear when `last-seen` is set to current time.

## Edge Cases to Verify
- Empty workspace (no workflows) — no panel shown.
- Workflow with no snapshots — no panel shown.
- Very long node names — panel content scrolls.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Patch: Differentiate Open Workspace and New Workspace actions

## Metadata
adw_id: `docs/tasks/feature-workspace-recent-changes-panel-857b7bc9/patches/patch-feature-workspace-recent-changes-panel-857b7bc9-1.md`
review_change_request: `The workspace management needs to be improved. We need to have a way to edit and select different workspaces. Right now, there's just a recent history dropdown, but that's confusing. "Open" should open a list of the workspaces you currently have, and "New" should create a new one. Right now they both do the same function.`

## Issue Summary
**Original Plan:** docs/tasks/feature-workspace-recent-changes-panel-857b7bc9/plan-feature-workspace-recent-changes-panel-857b7bc9.md
**Issue:** In `src/components/workspace/landing-page.tsx`, both the "Open Workspace" card button and the "New workspace" button call the same `handleNewWorkspace()` handler, which always creates a new workspace via `POST /api/workspaces`. There is no way to browse and select an existing workspace — the only path to existing workspaces is through the "Recent workspaces" list below, which is not intuitive.
**Solution:**
1. Add a `GET` handler to the `/api/workspaces` route that lists all workspace directories from disk.
2. Add a `listWorkspaces()` function to `server.ts`.
3. Change the "Open Workspace" button to open a dialog/sheet that fetches and displays all existing workspaces for selection.
4. Keep the "New workspace" button as-is (creates a new workspace).

## Files to Modify

- **`src/lib/workspace/server.ts`** — Add `listWorkspaces()` function to scan the data directory for workspace manifests.
- **`src/app/api/workspaces/route.ts`** — Add `GET` handler that calls `listWorkspaces()`.
- **`src/components/workspace/landing-page.tsx`** — Change "Open Workspace" button to open a workspace picker dialog instead of creating a new workspace. Add workspace picker dialog with loading state, empty state, and clickable workspace entries.

## Implementation Steps
IMPORTANT: Execute every step in order, top to bottom.

### Step 1: Add `listWorkspaces()` to server.ts
- In `src/lib/workspace/server.ts`, add a new exported function `listWorkspaces()` that:
1. Reads the workspace data directory (`getWorkspaceConfig().dataDir`).
2. Lists subdirectories using `fs.readdir` with `withFileTypes: true`.
3. For each subdirectory, attempts to read its `manifest.json` via `readJsonFile`.
4. Returns an array of `WorkspaceRecord` objects (id, name, createdAt, updatedAt) sorted by `updatedAt` descending.
5. Gracefully skips directories without a valid manifest.

### Step 2: Add GET handler to `/api/workspaces` route
- In `src/app/api/workspaces/route.ts`, add a `GET` handler:
- Calls `listWorkspaces()` from `server.ts`.
- Returns `{ workspaces: WorkspaceRecord[] }` as JSON.
- Wraps in try/catch with 500 error handling, matching existing POST handler pattern.

### Step 3: Update landing page with workspace picker
- In `src/components/workspace/landing-page.tsx`:
- Add `showPicker` state (boolean, default false).
- Change the "Open Workspace" button's `onClick` to set `showPicker(true)`.
- Add an inline workspace picker section (rendered conditionally when `showPicker` is true) that:
1. Fetches `GET /api/workspaces` on open via a `useEffect`.
2. Shows a loading spinner while fetching.
3. If no workspaces exist, shows "No workspaces yet" empty state with a prompt to create one.
4. Lists workspaces as clickable rows (name, last updated time) — clicking navigates to `/workspace/{id}`.
5. Has a "Cancel" or close button to hide the picker.
- Use existing theme tokens (`BG_SURFACE`, `BORDER_DEFAULT`, `TEXT_PRIMARY`, `TEXT_MUTED`) and patterns from `recent-workspaces.tsx` for consistent styling.
- Keep the "New workspace" button unchanged — it continues to call `handleNewWorkspace()`.

## Validation
Execute every command to validate the patch is complete with zero regressions.

```bash
bun run typecheck
bun run lint
bun run build
```

## Patch Scope
**Lines of code to change:** ~80-100
**Risk level:** low
**Testing required:** Manual verification that "Open Workspace" shows a picker of existing workspaces, "New workspace" creates a new workspace, and existing recent workspaces list still works.
Loading