Skip to content

Add Workspaces feature for clipboard isolation#1417

Open
naman-ajmera wants to merge 1 commit into
p0deje:masterfrom
naman-ajmera:feat/workspace
Open

Add Workspaces feature for clipboard isolation#1417
naman-ajmera wants to merge 1 commit into
p0deje:masterfrom
naman-ajmera:feat/workspace

Conversation

@naman-ajmera

@naman-ajmera naman-ajmera commented Jun 7, 2026

Copy link
Copy Markdown

Summary

Adds a Workspaces feature to Maccy that lets users organize clipboard history into separate, isolated workspaces. This prevents cross-project clipboard pollution — items copied in one workspace never appear in another.

What it does

  • Clipboard items are stored per-workspace; only the active workspace's items are shown
  • Workspace tabs appear at the top of the popup for quick switching
  • Full CRUD: create ("+"), rename (double-click tab or context menu), delete (context menu)
  • A "Default" workspace is automatically created on first launch
  • Existing clipboard items are migrated into the Default workspace seamlessly
  • Workspace management is also available in Settings > Workspaces
  • clearOnQuit only clears the active workspace, preserving other workspaces' data

What it doesn't do (current limitations)

  • No drag-and-drop reordering of workspace tabs (sortOrder exists in the model for future use)
  • No "All Items" global view — always scoped to active workspace
  • No moving items between workspaces after copy
  • Maximum of 10 workspaces (enforced at data layer and both UIs)

Reviewer Guide

New files (4)

File Purpose
Maccy/Models/Workspace.swift SwiftData @Model — id, name, createdAt, sortOrder, items relationship
Maccy/Observables/WorkspaceManager.swift Singleton managing workspace CRUD, active workspace persistence, and switching
Maccy/Views/WorkspaceTabsView.swift Horizontal tab bar in the popup — tap to switch, double-click to rename, context menu for delete, "+" to add
Maccy/Settings/WorkspaceSettingsPane.swift Settings pane for workspace management (list, add, rename, delete)

Key changes to existing files

File What changed Why
Maccy/Models/HistoryItem.swift Added workspace: Workspace? relationship with inverse: \Workspace.items Links each clipboard item to exactly one workspace
Maccy/Storage.swift Registered Workspace.self in ModelContainer; added ensureDefaultWorkspace() Creates Default workspace on first launch and migrates orphaned items
Maccy/Observables/History.swift load(), findSimilarItem(), clear(), clearAll() scoped to active workspace; new clearEverything() All history operations respect workspace boundaries
Maccy/Observables/AppState.swift Added workspaceManager, isPopoverEditing; registered Workspaces settings pane Central state for workspace access and rename-popover coordination
Maccy/Observables/Popup.swift Added workspaceTabsHeight; updated preferredHeight() and resize() Popup height calculation accounts for workspace tabs
Maccy/Views/KeyHandlingView.swift selectCurrentItem returns .ignored when isPopoverEditing Prevents Enter key from selecting a clipboard item while user is renaming a workspace
Maccy/Views/ContentView.swift Inserted WorkspaceTabsView() between header and history list Renders workspace tabs in the popup
Maccy/AppDelegate.swift Calls ensureDefaultWorkspace() + WorkspaceManager.shared.load() at launch; clearOnQuit uses clearEverything() Bootstrap workspaces before clipboard starts; scoped quit-time clearing

Design decisions worth understanding

  1. Workspace identity via stable UUID — not persistentModelID.hashValue. The active workspace ID is persisted in Defaults as a UUID string and restored across launches.

  2. SwiftData inverse: on one side onlyHistoryItem.workspace declares inverse: \Workspace.items; Workspace.items has @Relationship(deleteRule: .nullify) without inverse. This follows SwiftData documentation to avoid conflicts.

  3. #Predicate and static properties#Predicate macros cannot reference static properties directly. Storage.ensureDefaultWorkspace() captures Workspace.defaultName into a local variable before use in the predicate.

  4. clearEverything() fetches from DB directly — At quit time, History.all may not be loaded. The method fetches items matching the active workspace ID from storage rather than relying on in-memory state.

  5. findSimilarItem is workspace-scoped — Prevents a duplicate in Workspace A from removing an identical item in Workspace B. This is critical for workspace isolation.

  6. Per-method @MainActorWorkspaceManager uses @MainActor on individual methods (not the class) to match the codebase pattern seen in other observables.

  7. Rename popover coordination — A single .onChange(of: showRenamePopover) drives appState.isPopoverEditing, which KeyHandlingView checks to avoid swallowing Enter during rename.

  8. Localization pattern — Popup views use bare keys in Localizable.strings; settings panes use tableName:-scoped .strings files (WorkspaceSettings.strings). This matches the existing codebase convention.

  9. clear() / clearAll() refactored — Replaced context.delete(model:) bulk deletes with per-item deletion loops. The bulk API deleted across all workspaces; per-item deletion respects workspace scope since History.all only contains active workspace items.

Screenshot 2026-06-07 at 10 49 30 PM Screenshot 2026-06-07 at 10 50 11 PM

@naman-ajmera naman-ajmera changed the title workspace feature init workspace feature Jun 7, 2026
@naman-ajmera naman-ajmera changed the title workspace feature Add Workspaces feature for clipboard isolation Jun 7, 2026
@naman-ajmera
naman-ajmera marked this pull request as ready for review June 7, 2026 17:25
@naman-ajmera

Copy link
Copy Markdown
Author

#1408

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant