feat(workspaces): Group repositories and scope the board per workspace#374
Draft
eyelock wants to merge 1 commit into
Draft
feat(workspaces): Group repositories and scope the board per workspace#374eyelock wants to merge 1 commit into
eyelock wants to merge 1 commit into
Conversation
Workspaces let a repo-heavy machine be narrowed to one context at a time. A workspace is a filter, mirroring the existing repos.json pattern — not a separate store or board file. Repositories - Workspace model + WorkspaceStore (workspaces.json holds the definitions and the active selection in one file). Pure WorkspaceFilter for the visible-repo logic. - Sidebar filters the repo list to the active workspace; switcher, Manage sheet, per-repo "Add to Workspace" menu, and a dedicated empty state. - "All Repositories" = every repo (zero-membership repos appear only here). - New user-facing strings localized across all 40 locales. Board (single board.json, per-card workspace tag) - Each card carries an optional workspaceId (TerminalCard + the Sendable Card DTO); additive and back-compatible — pre-existing cards decode as unassigned. - One board.json. "All" shows every card; a workspace shows only its cards (unassigned cards show only in "All"). Switching workspace is a pure display re-filter — terminal sessions are never torn down. - Agents are pinned via TERMQ_WORKSPACE_ID (injected at terminal creation): the MCP server and termq-cli stamp new cards with it and filter their reads to it, all against the one board.json. Testing - Pure filter (Board.cardsInWorkspace), card round-trips, create-stamping, the MCP handler-level filter, and the app's displayedCards/stamp via an injected active-workspace seam (no WorkspaceStore singleton coupling in tests). Supersedes an earlier separate-board-file-per-workspace approach: "All" must remain a fully-functional working board, which a per-file model can only do via a read-only merge. The single-board filter dissolves that and mirrors the repo list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Workspaces — named groupings that let a repo-heavy machine be narrowed to one context at a time. A workspace is a filter, mirroring the existing
repos.jsonpattern; it is not a separate store or board file.board.json; each card carries aworkspaceId. "All" shows every card; a workspace shows only its cards. Switching workspace is a pure display re-filter — sessions are never torn down.TERMQ_WORKSPACE_ID(injected at terminal creation) — the MCP server andtermq-clistamp new cards with it and filter their reads to it.Status — DRAFT
Feature-complete and verified (
make checkgreen: build + lint + format + 2083 tests, 0 failures, 0 warnings; the one lint note isContentViewtype_body_length, inherited from develop, not this branch). Pending live in-app validation. Not for merge yet.Design decisions (and the history a new contributor needs)
This supersedes an earlier separate-board-file-per-workspace approach (
board-<id>.json+ a file swap on switch). Why it was abandoned: "All" must remain a fully-functional working board (it is the default board, and the only board before any workspace exists). A per-file model can only aggregate all workspaces into "All" via a read-only merge — unacceptable. The single-board + per-card-tag + filter model dissolves that and reuses the repo-filter pattern already in the codebase.Locked rules:
activeWorkspaceId == nil("All") → every card / every repo. A workspace → only items tagged with it. Unassigned items (noworkspaceId) appear only in "All" — same rule as zero-membership repos.nilin "All").Architecture / file map
Sources/TermQ/Models/Workspace.swift,WorkspaceFilter.swift;Sources/TermQ/Services/WorkspaceStore.swift(workspaces.json= definitions + active selection).Views/Sidebar/WorkspaceSwitcher.swift,ManageWorkspacesSheet.swift,WorktreeSidebarView(+Workspace).swift;ViewModels/WorktreeSidebarViewModel.swift(displayedRepositories). Strings inUtilities/Strings+Sidebar.swiftlocalized to all 40 locales.Sources/TermQShared/Card.swift+Sources/TermQCore/TerminalCard.swift—workspaceId: UUID?(additive optional,decodeIfPresent).Board.cardsInWorkspace(_:workspaceId:)inTermQShared/Board.swift(MCP/CLI path);BoardViewModel.cardsInWorkspace(_:active:)(app path).BoardViewModel.displayedCards(for:)filters byactiveWorkspaceProvider()— an injectable seam (defaults toWorkspaceStore.shared; tests inject a fixed value, no singleton mutation).KanbanBoardViewrenders it.newCardWorkspaceIdinaddTerminal/newTerminal/quickNewTerminal,duplicateTerminal(inherits source), andHarnessLaunchCoordinator.BoardViewModel.observeWorkspaceSwitch()emitsobjectWillChangeonWorkspaceStore.$activeWorkspaceId(no file swap, no session teardown).ViewModels/TerminalSessionManager.swiftinjectsTERMQ_WORKSPACE_IDat terminal creation.MCPServerLib/Server.swiftholdsworkspaceId(fromMCPServer-CLI/main.swiftreading the env);ToolHandlersfilterhandleList/handlePending/handleFind;HeadlessWriter/BoardWriter.createCard(workspaceId:)stamp. CLI:TermQCLICore/CLI.swiftresolveWorkspaceId()+ filter inCLI+Find/CLI+LLM/CLIlist, stamp inCLINew/Create.BoardWriteredits raw JSON dicts and preserves unknown keys, so only the create path needed touching for writes.Tests
TermQSharedTests/WorkspaceCardFilterTests— pure filter edge cases,Cardround-trip,BoardWriter.createCardstamping.MCPServerLibTests/ToolHandlersWorkspaceFilterTests— server pinned vs unpinned actually filtershandleList/handlePending.MCPServerLibTests/HeadlessWriterWorkspaceTests— create-stamp + persistence.TermQTests/BoardViewModelWorkspaceFilterTests—displayedCardshonours injected workspace;addTerminalstamps.TermQTests/TerminalCardWorkspaceTests—TerminalCardround-trip (present + legacy-absent →nil).Pick-up points for the next agent
termq find/listinvocation tests would close it.workspaceIdis additive-optional, so existingboard.jsoncards decode as unassigned (visible in "All"). The obsoleteboard-<id>.jsonfiles from the earlier approach are never read/written by this code and were discarded from the author's debug data dir.Verify
🤖 Generated with Claude Code