diff --git a/CONTEXT.md b/CONTEXT.md index a237c266..93103f07 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -4,26 +4,25 @@ Glossary for shared terms across the project. Implementation details belong in c ## Flagged ambiguities -- **A Workspace is defined by its configuration, not by the cloud.** Don't conflate "is this a Workspace?" (does the folder have a `.hubble/` configuration) with "is it synced?" ([[Cloud Sync]] enabled). A desktop Workspace can be local-only and gain Cloud Sync later. *(Note: not yet true in code — `init()` requires a Convex backend to mint a `workspaceId`. This is the subject of an active spec; see the deferred-cloud-sync handoff.)* -- **"Open folder" (desktop runtime) vs "Workspace."** The desktop editor operates on any open folder path and reads/writes the filesystem directly; that folder may be a [[Workspace Folder]] or a [[Plain Folder]]. Say "open folder" for the runtime notion, "Workspace" for the configured logical entity. +- **"Open folder" (desktop runtime) vs "Workspace."** The desktop editor operates on any open folder path and reads/writes the filesystem directly. Say "open folder" for the runtime notion and "Workspace" for the folder-first product notion. ## Glossary ### Workspace -A logical container of Markdown Files and Assets, defined by a **workspace configuration** (`.hubble/config.json`) — *not* by the cloud. A Workspace has a stable identity from the moment it is created. +A logical container of Markdown Files and Assets. On desktop, a Workspace is a folder the user has added to Hubble. Adding a Workspace does not require `.hubble/config.json`; optional capabilities create configuration only when needed. -[[Cloud Sync]] is an optional capability layered on top. A Workspace may be **local-only** (configured, no cloud backend) or **synced** (bound to a Convex deployment, with a row in the `workspaces` table). On the **web** a Workspace is always synced — there is no local filesystem to fall back to. On **desktop** a Workspace can be created local-only and have Cloud Sync enabled later. +[[Cloud Sync]] is an optional capability layered on top. A Workspace may be **local-only** (no cloud backend) or **synced** (bound to a Convex deployment, with a row in the `workspaces` table). On the **web** a Workspace is always synced — there is no local filesystem to fall back to. On **desktop** a Workspace starts local-only and can have Cloud Sync enabled later. The Workspace is the unit; the **access path differs by surface**. The web app reads/writes through the Convex backend. The desktop app reads/writes the [[Workspace Folder]] directly on disk (the working source of truth), with the background sync engine reconciling to Convex when Cloud Sync is on. Features that must run on both surfaces — notably [[Embed]]s — target the Workspace as the unit and resolve against whichever backend the current surface provides; they never assume Convex. ### Workspace Folder -The on-disk realization of a [[Workspace]]: a folder containing the workspace configuration at `.hubble/config.json`. **Configuration presence — not cloud binding — is what makes a folder a Workspace Folder.** When [[Cloud Sync]] is enabled the config also carries the Convex linkage (today: `workspaceId`, `workspaceName`); a local-only Workspace Folder has configuration without it. Multiple Workspace Folders across devices can map to the same synced Workspace. +The on-disk realization of a [[Workspace]]: a folder the desktop app has added to Hubble. The folder may have no `.hubble/` directory. `.hubble/config.json` is created only when an optional capability needs Workspace Configuration, such as [[Cloud Sync]], agent instructions, or embeds. Multiple Workspace Folders across devices can map to the same synced Workspace when they share Cloud Sync linkage. ### Plain Folder -A folder open in the desktop app with **no** workspace configuration (no `.hubble/`). It is not a [[Workspace]]: the desktop app reads and edits it as a general markdown viewer, nothing syncs, and Workspace-scoped features (e.g. [[Embed]]s) do not resolve. Adding a configuration promotes it to a [[Workspace Folder]]. +A folder opened directly in the desktop app without being added as a Workspace. The desktop app may read and edit it as a general markdown viewer, nothing syncs, and Workspace-scoped features may be unavailable until the folder is added as a Workspace. ### Folder @@ -37,7 +36,23 @@ _Avoid_: Compacted directory name ### Cloud Sync -The optional capability that binds a [[Workspace]] to a Convex deployment, enabling multi-device sync and web access. Required for web Workspaces; opt-in on desktop, where it can be enabled after a Workspace is created by supplying a Convex deployment URL. Whether Cloud Sync is on is orthogonal to whether a folder is a Workspace. +The optional capability that binds a [[Workspace]] to a Convex deployment, enabling multi-device sync and web access. Required for web Workspaces; opt-in on desktop, where it can be enabled after a Workspace is created by supplying a Convex deployment URL. + +Cloud Sync configuration lives under `cloudSync` in `.hubble/config.json`: + +```json +{ + "cloudSync": { + "provider": "convex", + "deploymentUrl": "https://...", + "workspaceId": "convex workspace id", + "deviceId": "local sync device uuid", + "backgroundSync": true + } +} +``` + +`cloudSync.backgroundSync` controls automatic daemon sync only. Manual sync can still run when background sync is off. Turning off background sync preserves `cloudSync` and `.hubble/state.json`; disconnecting Cloud Sync removes the `cloudSync` configuration. ### Markdown File diff --git a/docs/adr/0009-folder-first-workspaces-and-deferred-cloud-sync.md b/docs/adr/0009-folder-first-workspaces-and-deferred-cloud-sync.md new file mode 100644 index 00000000..c29ebaf4 --- /dev/null +++ b/docs/adr/0009-folder-first-workspaces-and-deferred-cloud-sync.md @@ -0,0 +1,19 @@ +# Folder-first Workspaces and deferred Cloud Sync + +Desktop Workspaces are folders added to Hubble. Adding a Workspace is non-invasive by default: Hubble does not create `.hubble/` files just to remember or edit the folder. + +Workspace Configuration is deferred until a capability needs it. Capabilities such as Cloud Sync, agent instructions, or embeds may create `.hubble/config.json`; plain editing and sidebar navigation do not. + +Cloud Sync is a nested Workspace capability, not the definition of a Workspace. When connected, `.hubble/config.json` stores `cloudSync.provider`, `cloudSync.deploymentUrl`, `cloudSync.workspaceId`, `cloudSync.deviceId`, and `cloudSync.backgroundSync`. The Convex `workspaceId` is the only remote Workspace id in this model, and `deviceId` is sync-scoped. + +Turning off background sync preserves Cloud Sync configuration and `.hubble/state.json`, so manual sync can continue. Disconnecting Cloud Sync removes the `cloudSync` configuration while leaving local files and sync state intact. + +Initial Cloud Sync setup uses the existing reconciliation rules: local-only files push, remote-only files pull, same-path same-hash files remain unchanged, same-path different-content files use conflict handling, and assets follow current asset sync rules. + +## Consequences + +- A desktop Workspace can exist without `.hubble/config.json`. +- `.hubble/config.json` presence means at least one optional Workspace capability is configured; it does not define Workspace existence. +- Web Workspaces remain synced-only because the web app has no local filesystem source of truth. +- UI copy should ask users to enable or connect Cloud Sync, not create a Workspace in the cloud. +- Relinking Cloud Sync is a distinct flow because it may merge a different remote Workspace into the same local folder. diff --git a/specs/gh-34/PRODUCT.md b/specs/gh-34/PRODUCT.md new file mode 100644 index 00000000..99a8707b --- /dev/null +++ b/specs/gh-34/PRODUCT.md @@ -0,0 +1,64 @@ +# Folder-first Workspaces and deferred Cloud Sync + +## Summary + +Desktop Workspaces are folders users add to Hubble, not cloud records and not `.hubble/` directories. Cloud Sync is an optional capability users can connect later without changing the local-first editing model. + +## Problem + +Today product language and flows can imply that a Workspace is created by Cloud Sync or by `.hubble/config.json`. That makes local-only desktop use feel provisional and makes later sync setup harder to reason about. + +## Goals + +- Adding a desktop Workspace is non-invasive by default. +- Cloud Sync setup is explicit, reversible, and independent from whether a folder is a Workspace. +- Users can choose an existing remote Workspace or create a new one when connecting Cloud Sync. +- Initial sync uses existing reconciliation behavior and does not imply local files are overwritten. +- Background sync is a preference, not the same thing as Cloud Sync linkage. + +## Non-goals + +- No multi-user account model. +- No token/auth flow changes. +- No new sync conflict policy. +- No support for preserving empty folders through Cloud Sync. +- No automatic relink to a different Convex deployment without confirmation. + +## Behavior + +1. When a desktop user chooses `Add Workspace`, Hubble asks for a folder. +2. Adding a folder succeeds without creating `.hubble/config.json`. +3. The added folder appears as a Workspace in desktop navigation immediately after selection. +4. Local-only Workspaces can create, edit, rename, and delete Markdown Files and Assets without Cloud Sync. +5. Workspace-scoped optional capabilities may create `.hubble/config.json` only when the user enables that capability. +6. The Add Workspace dialog offers optional setup for agent instructions. +7. The Add Workspace dialog offers optional Cloud Sync setup, hidden until the user chooses to connect it. +8. Cloud Sync setup asks for a Convex deployment URL. +9. On blur or submit, Hubble validates the Convex deployment URL and reports invalid, unreachable, or incompatible deployments in place. +10. After a valid URL, Hubble shows available remote Workspaces from that deployment. +11. The user can select an existing remote Workspace. +12. The user can choose `Create new` and enter a remote Workspace name, defaulting to the folder name. +13. Cloud Sync setup includes an `Auto sync` checkbox that defaults on. +14. The Auto sync explanation makes clear that manual sync remains available when auto sync is off. +15. Connecting Cloud Sync writes the linkage and runs initial sync. +16. Initial sync pushes local-only files, pulls remote-only files, leaves same-path same-hash files unchanged, and preserves same-path different-content files through conflict handling. +17. Assets follow the existing asset sync rules during initial sync. +18. If background sync registration succeeds, the Workspace syncs automatically after setup. +19. If background sync registration fails, Cloud Sync remains connected, the user sees the registration error, and a retry action is available. +20. A synced desktop Workspace shows its Convex deployment URL and linked remote Workspace in settings. +21. Settings show whether background sync is registered, based on live service state. +22. Turning off Auto sync preserves Cloud Sync linkage and sync state. +23. Manual sync remains available while Auto sync is off. +24. Disconnecting Cloud Sync removes the Cloud Sync linkage but keeps local files. +25. Editing the deployment URL or linked remote Workspace starts a relink flow with confirmation because it may merge different remote content into the same local folder. +26. Web Workspaces are always synced because web has no local filesystem Workspace source of truth. +27. A user can understand from labels and messages that `Workspace`, `Cloud Sync`, and `Auto sync` are separate states. + +## UX validation + +1. In desktop, add a folder as a Workspace and confirm no `.hubble/config.json` is created. +2. Enable agent instructions or another config-backed capability and confirm configuration is created only then. +3. Connect Cloud Sync with a valid Convex URL, create a new remote Workspace, and confirm initial files appear on web. +4. Connect Cloud Sync with an existing remote Workspace and confirm remote-only files pull locally. +5. Turn Auto sync off, run manual sync, and confirm the Workspace remains linked. +6. Disconnect Cloud Sync and confirm local files remain editable while web access stops for that local folder. diff --git a/specs/gh-34/TECH.md b/specs/gh-34/TECH.md new file mode 100644 index 00000000..0af858f0 --- /dev/null +++ b/specs/gh-34/TECH.md @@ -0,0 +1,120 @@ +# Folder-first Workspaces and deferred Cloud Sync + +## Context + +Issue #34 defines desktop Workspaces as local folders first, with Cloud Sync as an optional nested capability. See `PRODUCT.md` for behavior. + +Current commit: `4247f68c6fa5e63196dc3eefadb1192878720b69`. + +Current architecture already has part of the model: + +- `packages/sync/src/types.ts` defines optional `WorkspaceConfig.cloudSync`. +- `packages/sync/src/config.ts` reads missing config as `{}` and writes `cloudSync` only through `writeCloudSyncConfig`. +- `packages/sync/src/sync.ts` requires `cloudSync` only when running sync. +- `packages/cli/src/index.ts` has `hubble cloud create`, `connect`, `sync`, `watch`, and `disconnect`. +- `apps/www` stores a Convex URL and Workspace id because web Workspaces are synced-only. + +The main mismatch is desktop configuration ownership. `apps/desktop/electron/main.ts` uses `.hubble/config.json` for desktop pinned notes with a `{ version, pinnedNotes }` shape, while `packages/sync` expects the same path to hold optional `cloudSync`. Before shipping the full flow, those shapes need one shared Workspace Configuration schema or separate paths. + +## Affected apps and packages + +- `apps/desktop`: Add Workspace dialog, settings UI, background sync registration state, and Workspace Configuration reads/writes. +- `apps/www`: No local-first changes; web remains synced-only. May need copy updates if shared labels change. +- `packages/sync`: Shared config schema, Cloud Sync connect/disconnect helpers, initial sync behavior, and tests. +- `packages/convex-client`: Remote Workspace listing and creation are already available through backend APIs; expose typed helpers if UI should avoid raw Convex calls. +- `packages/cli`: Keep manual Cloud Sync commands aligned with the desktop config schema and background sync semantics. +- `packages/sync-backend`: No expected schema change for this slice unless remote Workspace list/create APIs need additions. + +## Module architecture + +Use one shared Workspace Configuration contract. + +- `packages/sync/src/types.ts` + - Own `WorkspaceConfigSchema`. + - Include optional `cloudSync`. + - Include any desktop config-backed capability fields that must share `.hubble/config.json`, or explicitly move desktop-only fields to a different file. +- `packages/sync/src/config.ts` + - Own all `.hubble/config.json` reads/writes used by sync-aware code. + - Preserve unknown or unrelated optional sections only if the shared schema intentionally supports them. +- `packages/sync/src/sync.ts` + - Keep `sync()` requiring `cloudSync`. + - Extract connect helpers that can create or link a remote Workspace and initialize `.hubble/state.json`. +- `apps/desktop/electron/main.ts` + - Stop owning an incompatible private schema for `.hubble/config.json`. + - Expose IPC for reading/writing Workspace Configuration through shared package code or an equivalent schema. +- `apps/desktop/src` + - Add Add Workspace dialog state for optional Cloud Sync fields. + - Add Workspace settings state for connected, disconnected, background sync registered, registration failed, and retry. +- `packages/cli/src/index.ts` + - Continue using shared config helpers for `cloud create`, `connect`, `sync`, `watch`, and `disconnect`. + +## Detailed plan + +1. Reconcile Workspace Configuration. + - Decide whether pinned notes belong in shared `.hubble/config.json` or a desktop-specific file. + - If shared, add `pinnedNotes` and config versioning to `WorkspaceConfigSchema`. + - If separate, move desktop pins away from `.hubble/config.json`. + - Add tests for reading old/missing config and writing config without enabling Cloud Sync. +2. Make desktop Add Workspace non-invasive. + - Ensure folder selection persists as a recent/current Workspace without writing `.hubble/config.json`. + - Keep file listing and editing independent from config presence. + - Verify Behavior 1-5. +3. Add Cloud Sync setup to Add Workspace. + - Validate deployment URL before showing remote Workspace choices. + - Fetch remote Workspaces after validation. + - Support existing remote Workspace selection and create-new name input. + - Default create-new name to the folder basename. + - Default Auto sync on. +4. Implement connect semantics. + - Write `cloudSync` config only after the user confirms setup. + - Create `.hubble/state.json` when missing. + - Run initial sync through existing reconciliation rules. + - Register background sync only after Cloud Sync linkage exists. + - Preserve linkage if registration fails, and surface retry state. +5. Add Workspace settings. + - Show deployment URL, linked remote Workspace, background sync registration state, and read-only ids with copy actions. + - Toggle Auto sync by changing background registration, not by deleting `cloudSync`. + - Disconnect Cloud Sync by removing only `cloudSync`. + - Gate URL/Workspace edits behind a relink confirmation. +6. Keep CLI aligned. + - Ensure `hubble cloud disconnect` removes `cloudSync` without deleting local files or `.hubble/state.json`. + - Add a background-sync option only if the daemon has a CLI-facing registration path. + +## Testing and validation + +- Behavior 1-5: desktop unit/integration coverage for adding a folder and confirming no config write occurs. +- Behavior 8-14: component tests for Cloud Sync setup validation, remote Workspace list, create-new state, and Auto sync default. +- Behavior 15-17: `packages/sync` tests for connect plus initial sync using existing file and asset reconciliation. +- Behavior 18-19: desktop tests around registration success/failure and retry state. +- Behavior 20-25: settings tests for display, Auto sync toggle, disconnect, and relink confirmation. +- Behavior 26-27: web smoke test for unchanged synced-only connection flow and copy review. + +Run: + +- `pnpm check` for quick iteration. +- `pnpm build:desktop` before final review. +- Desktop manual validation with a temporary folder, then a second synced device or web app to verify initial sync. +- Web validation through `apps/www` with `?test=1` when checking web visibility. + +## Parallelization + +Useful after the config decision is made: + +- Agent 1: shared config/schema and sync tests. +- Agent 2: desktop Add Workspace and settings UI. +- Agent 3: CLI alignment and manual validation docs. + +Do not parallelize before the config path decision; otherwise agents may encode incompatible `.hubble/config.json` assumptions. + +## Risks and mitigations + +- Data loss during first sync: reuse existing conflict handling and add explicit tests for same-path different-content files. +- Config schema collision: resolve desktop pins vs sync config before UI work. +- Partial setup failure: persist Cloud Sync linkage before background registration and expose retry. +- Relink surprises: require confirmation before changing deployment URL or remote Workspace. + +## Follow-ups + +- Pairing or token auth for protected Convex deployments. +- Empty folder preservation. +- More detailed background sync service observability.