Repository navigation
Conversation
Introduce the kanban feature's foundation: a KanbanColumn config value type (id/title/order/colorHex/isArchive/isCollapsed) seeded with To Do / In Progress / Done / Archive, exposed as the global kanban.columns JSON setting via a new KanbanCatalogSection. Card membership is per-tab state: kanbanColumnId/kanbanOrder on Workspace, persisted as optional fields on SessionWorkspaceSnapshot (no schema-version bump; old manifests decode). Pure column-list transforms (add/rename/recolor/collapse/delete + guard rules) live in KanbanColumnMutations so they're unit-testable without a TabManager. Adds model/back-compat tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a .board view mode to SidebarSelection, cross-faded in ContentView.terminalContent like the existing NotificationsPage layer, and make it the default landing view (persisted/restored via the session snapshot). The board reads kanban.columns through @LiveSetting and buckets tabManager.tabs into columns; clicking a card selects that workspace and switches to the terminal view. All mutations route through a single shared path (TabManager+Kanban): per-tab moves set the Workspace fields (sending objectWillChange so the board re-renders), column CRUD writes kanban.columns through the shared SettingsRuntime JSON store. Cards drag between columns reusing the existing com.cmux.sidebar-tab-reorder payload; card/column context menus cover move/archive/rename/recolor/collapse/new/delete. Board views keep the snapshot-boundary rule (immutable value snapshots + closure bundles below the ForEach). Wires new sources into the cmux target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Toggle Board View" shortcut (default Cmd-Shift-B) that flips between the board and terminal views, wired through the shortcut action enums, dispatch handler, and cmux.json schema per the shortcut policy. Localize all kanban UI strings (en + ja) in Localizable.xcstrings, and resolve the four seeded column titles to localized display names while leaving user-renamed columns untouched. Document the shortcut and the kanban.columns config key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Someone is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds a configurable Kanban board with persisted columns and workspace placement, card and column management, board-focused keyboard navigation, new board shortcuts and titlebar controls, session integration, localization, schema documentation, and regression tests. ChangesKanban board feature
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ContentView
participant KanbanBoardView
participant TabManager
participant SettingsRuntime
User->>ContentView: Select board or invoke board shortcut
ContentView->>KanbanBoardView: Show board and focused card
KanbanBoardView->>TabManager: Move, rename, archive, or edit column
TabManager->>SettingsRuntime: Persist column configuration
SettingsRuntime-->>KanbanBoardView: Updated board settings
Possibly related PRs
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (6 errors, 1 warning)
✅ Passed checks (18 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Under Swift 6 strict concurrency, the board views referenced main-actor state from non-isolated contexts. Mark KanbanCardSnapshot's Workspace init @mainactor, isolate KanbanBoardView and KanbanColumnView to the main actor (their helpers read tabManager / run NSAlert), and type the drop delegate's onCardDropped closure @mainactor @sendable so it can be carried into loadDataRepresentation's @sendable completion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kanbanColumnId/kanbanOrder are declared last on SessionWorkspaceSnapshot, so the memberwise-init call in Workspace.sessionSnapshot() must pass them after environment, not after groupId. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Ghostty terminal is a portal view drawn above SwiftUI, so the terminal layer's `.opacity(0)` didn't hide it on the board page — it bled through. Gate each workspace's visibility, input-active, and portal priority on `selection == .tabs` so the portal is torn down when the board (or notifications) owns the screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Land on the board on every cold launch: force SidebarSelection to .board in both fresh-window and session-restore setup, and suppress the initial terminal auto-focus (which otherwise flips to .tabs) via a one-shot isInitialBoardLanding flag cleared on the first explicit selection. Add board keyboard navigation: a per-window KanbanFocusState tracks the focused card (highlighted via a value passed down to the card, preserving the snapshot boundary). Arrow keys move focus (via .onKeyPress, since bare arrows can't be bound as cmux shortcuts). Return opens the focused card, Ctrl-Opt-Left/Right move it between columns (including Archive), and Opt-Delete archives it — all real cmux shortcuts scoped to a new .board shortcut context so they only fire while the board is shown. Shortcuts are registered, localized (en+ja), schema-supported, and documented. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MainWindowContext.init and registerMainWindow default the new kanbanFocusState param to KanbanFocusState(), evaluated in a nonisolated context — but the @mainactor class's implicit init was main-actor isolated. Add an explicit nonisolated init (the nil default needs no isolation) so the default argument compiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Right-clicking a card now offers "Rename…", which renames the card's underlying workspace via a prompt (reusing the app's existing Rename Workspace strings). Routes through a new TabManager.renameCard that calls setCustomTitle and fires objectWillChange so the board re-renders the card title. New label localized en+ja. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a board-toggle button to the titlebar controls, immediately left of the notifications (bell) button. It calls the same shared toggleBoardViewInActiveMainWindow path as the Cmd-Shift-B shortcut (per shared-behavior), with a localized accessibility label + tooltip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The board toggle button already renders in minimal mode (shared TitlebarControlsView), but minimal mode routes clicks through slot-indexed proxies. Add a .toggleBoard slot to MinimalModeSidebarControlActionSlot (between toggleSidebar and showNotifications), its hit-lane geometry in TitlebarControlsHitRegions, and dispatch in both the SwiftUI proxy and the WindowDecorationsController click target — all calling the shared toggleBoardViewInActiveMainWindow. Add a matching shortcut-hint slot so the button shows its ⌘⇧B badge, and update the exhaustive-switch tests. Refresh the file-length budget for the four files this feature grew (AppDelegate, UpdateTitlebarAccessory, WindowDragHandleView, ShortcutAction). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bundled preset sidebar views (Project Worktrees, Attention Queue, Dev Servers, etc.) render rows via CmuxExtensionSidebarWorkspaceRowView, which had no context menu — so workspaces there couldn't be renamed. Add a right-click "Rename Workspace…" item that renames the row's workspace, reusing the existing setCustomTitle path, NSAlert prompt pattern, and localization keys. The rename closure is supplied by VerticalTabsSidebar (which holds tabManager), keeping the row a value+closure view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@codex review |
@ivan-kyu I can't start this review because your workspace has reached its free monthly review limit. cubic has reviewed 241,631 of the 240,000 allowed lines of code this month. Reviews resume on 1 August 2026 (in 18 days). Paid plans include much higher monthly review limits. Upgrade now to resume reviews. To help optimise your usage, you can tune cubic to get the most out of your usage limits:
|
|
✅ Action performedReview finished.
|
Greptile SummaryThis PR adds a kanban board as the macOS app's main workspace view. The main changes are:
Confidence Score: 4/5The app build and column persistence paths need fixes before merging.
Sources/Kanban/KanbanCardView.swift, Sources/Kanban/KanbanColumnView.swift, and Sources/TabManager+Kanban.swift Important Files Changed
Reviews (1): Last reviewed commit: "Add Rename to preset sidebar (Project Wo..." | Re-trigger Greptile |
|
|
||
| var body: some View { | ||
| HStack(spacing: 8) { | ||
| if let colorHex = card.colorHex, let color = Color(hex: colorHex) { |
There was a problem hiding this comment.
Color(hex:) is not provided by SwiftUI or the imported modules; the existing helper initializes NSColor instead. This call and the matching calls in KanbanColumnView prevent the app target from compiling.
Context Used: Apply cmux's custom review rules from .github/revi... (source)
| private func writeKanbanColumns(_ columns: [KanbanColumn]) { | ||
| guard let runtime = AppDelegate.shared?.settingsRuntime else { return } | ||
| Task { | ||
| try? await runtime.jsonStore.set(columns, for: runtime.catalog.kanban.columns) | ||
| } | ||
| objectWillChange.send() | ||
| } |
There was a problem hiding this comment.
Column Writes Race and Disappear
Each column edit starts an independent task and discards the write error. Rapid edits from two windows can finish out of order and restore an older column list, while an unwritable or externally replaced config makes a successful rename, collapse, recolor, or delete silently disappear.
Rule Used: Flag new legacy async patterns in cmux-owned Swift... (source)
| .onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: KanbanColumnDropDelegate( | ||
| targetColumnId: column.id, | ||
| nextOrder: (cards.map(\.order).max() ?? -1) + 1, | ||
| isTargeted: $isDropTargeted, | ||
| onCardDropped: actions.onCardDropped | ||
| )) |
There was a problem hiding this comment.
Drop Order Uses Stale Snapshot
The drop closure captures nextOrder from the last rendered card snapshot instead of computing it when the mutation runs. Two drops accepted before SwiftUI rebuilds the column can receive the same order, leaving their relative position unstable on later renders or restores.
Context Used: Apply cmux's custom review rules from .github/revi... (source)
Greptile SummaryThis PR adds a kanban workspace view as the macOS app's default landing screen. The main changes are:
Confidence Score: 4/5Column persistence can lose rapid updates or diverge from workspace assignments, and the new UI lacks translations for supported locales.
Sources/TabManager+Kanban.swift; Resources/Localizable.xcstrings Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
UI[Board action] --> TM[TabManager mutation]
TM --> WS[Workspace assignment]
TM --> SNAP[Read column snapshot]
SNAP --> TASK[Start asynchronous write]
TASK --> STORE[Global settings store]
STORE --> LIVE[Live setting reload]
LIVE --> BOARD[Board view]
TASK -. stale or failed write .-> BAD[Column and workspace state diverge]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart LR
UI[Board action] --> TM[TabManager mutation]
TM --> WS[Workspace assignment]
TM --> SNAP[Read column snapshot]
SNAP --> TASK[Start asynchronous write]
TASK --> STORE[Global settings store]
STORE --> LIVE[Live setting reload]
LIVE --> BOARD[Board view]
TASK -. stale or failed write .-> BAD[Column and workspace state diverge]
Reviews (2): Last reviewed commit: "Add Rename to preset sidebar (Project Wo..." | Re-trigger Greptile |
| /// failure is swallowed here the same way that helper does, rather than | ||
| /// surfaced to the caller. | ||
| private func writeKanbanColumns(_ columns: [KanbanColumn]) { | ||
| guard let runtime = AppDelegate.shared?.settingsRuntime else { return } | ||
| Task { | ||
| try? await runtime.jsonStore.set(columns, for: runtime.catalog.kanban.columns) |
There was a problem hiding this comment.
Each column action reads the current snapshot, then this helper starts an unstructured write. If a user adds a column and immediately renames or recolors it, the second action can read the pre-add snapshot and enqueue a later write that removes the new column. A failed delete write can also leave reassigned workspaces out of sync with the persisted column list because the error is discarded.
| } | ||
| } | ||
| }, | ||
| "titlebar.board.accessibilityLabel": { |
There was a problem hiding this comment.
Board Translations Are Incomplete
The new kanban.*, shortcut.*, and titlebar.board.* entries contain only English and Japanese, although this catalog supports additional locales. Users of those locales receive English fallback throughout the new production UI instead of translated board controls and dialogs.
File Used: .github/review-bot-rules/full-internationalization.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Greptile SummaryThis PR adds a kanban board as the macOS app's main workspace view. The main changes are:
Confidence Score: 4/5Card ordering, rename propagation, and failed column persistence need fixes before merging.
Sources/ContentView.swift, Sources/Kanban/KanbanColumnView.swift, Sources/TabManager+Kanban.swift, and Resources/Localizable.xcstrings Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
Config[kanban.columns config] --> Board[Kanban board]
Snapshot[Session snapshot] --> Workspace[Workspace assignments]
Workspace --> Board
Board --> Actions[TabManager kanban actions]
Shortcuts[Board shortcuts] --> Actions
Menus[Card and column menus] --> Actions
Actions --> Workspace
Actions --> Config
Board --> Terminal[Selected workspace terminal]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart LR
Config[kanban.columns config] --> Board[Kanban board]
Snapshot[Session snapshot] --> Workspace[Workspace assignments]
Workspace --> Board
Board --> Actions[TabManager kanban actions]
Shortcuts[Board shortcuts] --> Actions
Menus[Card and column menus] --> Actions
Actions --> Workspace
Actions --> Config
Board --> Terminal[Selected workspace terminal]
Reviews (3): Last reviewed commit: "Add Rename to preset sidebar (Project Wo..." | Re-trigger Greptile |
| guard response == .alertFirstButtonReturn else { return } | ||
| let trimmed = input.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard !trimmed.isEmpty else { return } | ||
| tabManager.setCustomTitle(tabId: tab.id, title: trimmed) |
There was a problem hiding this comment.
This new sidebar action calls setCustomTitle directly instead of the shared renameCard path, which sends the TabManager update observed by the board. Renaming from the extension sidebar can therefore leave the card showing its old title until an unrelated board update occurs.
Context Used: CLAUDE.md (source)
| .cornerRadius(10) | ||
| .onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: KanbanColumnDropDelegate( | ||
| targetColumnId: column.id, | ||
| nextOrder: (cards.map(\.order).max() ?? -1) + 1, |
There was a problem hiding this comment.
Drop Order Uses Stale Snapshot
nextOrder is captured when the drop delegate is created, before the provider finishes loading its data. If another card enters the same column during that interval, both drops can receive the same order, leaving their displayed order unstable because cards are sorted only by kanbanOrder.
| private func writeKanbanColumns(_ columns: [KanbanColumn]) { | ||
| guard let runtime = AppDelegate.shared?.settingsRuntime else { return } | ||
| Task { | ||
| try? await runtime.jsonStore.set(columns, for: runtime.catalog.kanban.columns) | ||
| } | ||
| objectWillChange.send() | ||
| } |
There was a problem hiding this comment.
Failed Writes Leave Split State
Column deletion reassigns workspace cards before this asynchronous config write succeeds, and this method discards every write error. If the store is unavailable or the disk is full, the session can persist the new card assignments while cmux.json retains the deleted column, so the column returns after restart without its former cards.
Context Used: CLAUDE.md (source)
| @@ -234025,6 +234059,737 @@ | |||
| } | |||
There was a problem hiding this comment.
Board Strings Omit Existing Locales
The new board menus, dialogs, titlebar labels, and shortcut names include only English and Japanese even though this catalog supports additional locales. Users of those locales receive English fallback text across the new feature; every added key needs entries for all locales already present in the catalog.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 333028e0ce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| public var isArchive: Bool | ||
| /// Collapsed columns hide their cards, showing only the header. | ||
| public var isCollapsed: Bool |
There was a problem hiding this comment.
Decode omitted kanban flags as defaults
The schema/docs make only id/title/order required and show examples without isArchive/isCollapsed, but synthesized Decodable requires these non-optional Bool keys. A user who configures kanban.columns with the documented minimal objects will fail to decode the whole array and silently fall back to defaults, so custom columns disappear; provide a custom decoder/defaults or make these optional on decode.
Useful? React with 👍 / 👎.
| let newOrder = (columns.map(\.order).max() ?? -1) + 1 | ||
| return columns + [KanbanColumn(id: id, title: title, order: newOrder)] |
There was a problem hiding this comment.
Keep Archive after newly added columns
When adding a column to the default list, max(order) + 1 assigns the new column order 4 while Archive remains order 3, so the new column renders to the right of Archive even though Archive is treated as the pinned final column and keyboard movement uses this order. Users who add any column get Archive in the middle and moving right from Done archives before reaching the new column; insert before Archive or bump Archive's order instead.
Useful? React with 👍 / 👎.
| private func focusFirstCard(sortedColumns: [KanbanColumn], cardsByColumnId: [String: [KanbanCardSnapshot]]) { | ||
| for column in sortedColumns { | ||
| if let firstCard = cardsByColumnId[column.id]?.first { | ||
| kanbanFocusState.focusedCardId = firstCard.id |
There was a problem hiding this comment.
Seed board focus only from visible columns
When the board appears with cards only in a collapsed column (the default Archive is collapsed, or a user collapses an earlier column), focusFirstCard still selects that hidden card. Because the Return shortcut opens focusedCardId without checking visibility, users can open an archived/collapsed workspace that is not rendered; filter out collapsed columns when seeding/validating focus, as the arrow-key paths already do.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/ContentView.swift`:
- Around line 2616-2630: Update the .ghosttyDidFocusTab handler around
sidebarSelectionState.isInitialBoardLanding so the first focus event preserves
both .board and .notifications selections. Clear isInitialBoardLanding and
return without assigning .tabs when the current selection is either of those
destinations; retain the existing .tabs behavior for all other selections.
In `@Sources/Kanban/KanbanBoardView.swift`:
- Around line 247-252: Extract the shared “open board card” sequence from
selectWorkspace(_:) into a reusable helper, preserving workspace selection,
selection = .tabs, and isInitialBoardLanding = false. Update
Sources/Kanban/KanbanBoardView.swift lines 247-252 to call it, and update
Sources/AppDelegate.swift lines 6630-6643 in
openFocusedBoardCardInActiveMainWindow to call the same helper instead of
duplicating the logic.
- Around line 150-170: Update ensureValidFocus to seed focus only from
navigable, non-collapsed columns, matching moveFocusToAdjacentColumn and
moveFocusWithinColumn. Pass the filtered navigableColumns to focusFirstCard
while preserving validation of an existing focused card and the nil fallback
when no visible cards exist.
In `@Sources/Kanban/KanbanColumnView.swift`:
- Around line 333-337: Replace String(format:) with
String.localizedStringWithFormat in showInvalidColumnColorAlert for the
invalid-color message and in promptDeleteColumn for the delete title, preserving
the existing localized format strings and arguments.
In `@Sources/Kanban/KanbanFocusState.swift`:
- Around line 13-24: Convert KanbanFocusState to the `@Observable` pattern by
removing ObservableObject and `@Published` while preserving its plain-reference
storage in Sources/AppDelegate.swift:548-552 with no signature change. In
Sources/Kanban/KanbanBoardView.swift:25 and Sources/ContentView.swift:813,
replace `@EnvironmentObject` with `@Environment`(KanbanFocusState.self); in
Sources/ContentView.swift, also replace every
.environmentObject(kanbanFocusState) injection with
.environment(kanbanFocusState).
In `@Sources/TabManager`+Kanban.swift:
- Around line 102-105: Update the batch reassignment loop in the tab
reassignment flow to calculate the starting order for
deletion.reassignedToColumnId once before iterating, then assign that value to
each matching tab while incrementing it after each assignment. Remove the
per-tab calls to nextOrder(inColumn:) and preserve the existing column
reassignment behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bfcade61-ce69-4298-ad39-e6cb219ba857
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (36)
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/KanbanCatalogSection.swiftPackages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swiftPackages/macOS/CmuxSettings/Sources/CmuxSettings/Values/KanbanColumn.swiftPackages/macOS/CmuxSettings/Sources/CmuxSettings/Values/KanbanColumnMutations.swiftPackages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swiftPackages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swiftPackages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutContextKnownKey.swiftPackages/macOS/CmuxSettings/Tests/CmuxSettingsTests/KanbanColumnMutationsTests.swiftResources/Localizable.xcstringsSources/AppDelegate.swiftSources/ContentView.swiftSources/ExtensionSidebarWorkspaceRowView.swiftSources/Kanban/KanbanBoardView.swiftSources/Kanban/KanbanCardView.swiftSources/Kanban/KanbanColumnView.swiftSources/Kanban/KanbanFocusState.swiftSources/KeyboardShortcutActionContext.swiftSources/KeyboardShortcutContext.swiftSources/KeyboardShortcutSettings.swiftSources/SessionPersistence.swiftSources/SidebarSelectionState.swiftSources/TabManager+Kanban.swiftSources/Update/MinimalModeSidebarControls.swiftSources/Update/UpdateTitlebarAccessory.swiftSources/WindowDecorationsController.swiftSources/WindowDragHandleView.swiftSources/Workspace.swiftcmux.xcodeproj/project.pbxprojcmuxTests/KanbanColumnAssignmentTests.swiftcmuxTests/ShortcutAndCommandPaletteTests.swiftcmuxTests/UpdatePillReleaseVisibilityTests.swiftcmuxTests/WindowAndDragTests.swiftskills/cmux-settings/references/shortcut-actions.mdweb/app/[locale]/(landing)/docs/configuration/page.tsxweb/data/cmux-shortcuts.tsweb/data/cmux.schema.json
| view = AnyView(view.onReceive(NotificationCenter.default.publisher(for: .ghosttyDidFocusTab)) { _ in | ||
| // The initial workspace auto-focuses at launch, which would otherwise | ||
| // immediately steal the cold-launch board landing (#board-landing). | ||
| // Consume that first post-launch focus without leaving the board; | ||
| // every later focus event behaves as before. | ||
| if sidebarSelectionState.isInitialBoardLanding { | ||
| sidebarSelectionState.isInitialBoardLanding = false | ||
| if sidebarSelectionState.selection == .board { | ||
| scheduleTitlebarTextRefresh() | ||
| return | ||
| } | ||
| } | ||
| sidebarSelectionState.selection = .tabs | ||
| scheduleTitlebarTextRefresh() | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant file region and related state definitions.
git ls-files 'Sources/ContentView.swift' 'Sources/**/SidebarSelectionState*' 'Sources/**/SidebarSelection*.swift' 'Sources/**/Kanban*' | sed -n '1,200p'
echo '--- ContentView.swift around the reported lines ---'
sed -n '2588,2645p' Sources/ContentView.swift
echo '--- Search for SidebarSelectionState and related selection writes ---'
rg -n "struct SidebarSelectionState|class SidebarSelectionState|enum SidebarSelection|isInitialBoardLanding|selection = \\.tabs|selection = \\.board|selection = \\.notifications|ghosttyDidFocusTab" Sources/ContentView.swift SourcesRepository: manaflow-ai/cmux
Length of output: 8476
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Broaden the inspection to the selection state and the notifications view.
echo '--- Files of interest ---'
git ls-files 'Sources/**/SidebarSelection*.swift' 'Sources/**/ContentView.swift' 'Sources/**/Notifications*.swift' 'Sources/**/Kanban*.swift' | sed -n '1,200p'
echo '--- SidebarSelection definitions ---'
rg -n "enum SidebarSelection|struct SidebarSelectionState|class SidebarSelectionState|isInitialBoardLanding" Sources
echo '--- Notifications / board selection handling ---'
rg -n "selection = \\.notifications|selection = \\.board|selection = \\.tabs|isInitialBoardLanding|ghosttyDidFocusTab|Notifications" Sources/ContentView.swift SourcesRepository: manaflow-ai/cmux
Length of output: 50372
Initial focus can still override Notifications — Sources/ContentView.swift:2616-2630
When isInitialBoardLanding is still true, this handler only preserves .board. If the user has already switched to .notifications before the first .ghosttyDidFocusTab, it still falls through and resets selection to .tabs, discarding that navigation.
🔧 Suggested fix
view = AnyView(view.onReceive(NotificationCenter.default.publisher(for: .ghosttyDidFocusTab)) { _ in
// The initial workspace auto-focuses at launch, which would otherwise
// immediately steal the cold-launch board landing (`#board-landing`).
// Consume that first post-launch focus without leaving the board;
// every later focus event behaves as before.
if sidebarSelectionState.isInitialBoardLanding {
sidebarSelectionState.isInitialBoardLanding = false
- if sidebarSelectionState.selection == .board {
+ if sidebarSelectionState.selection != .tabs {
scheduleTitlebarTextRefresh()
return
}
}
sidebarSelectionState.selection = .tabs
scheduleTitlebarTextRefresh()
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| view = AnyView(view.onReceive(NotificationCenter.default.publisher(for: .ghosttyDidFocusTab)) { _ in | |
| // The initial workspace auto-focuses at launch, which would otherwise | |
| // immediately steal the cold-launch board landing (#board-landing). | |
| // Consume that first post-launch focus without leaving the board; | |
| // every later focus event behaves as before. | |
| if sidebarSelectionState.isInitialBoardLanding { | |
| sidebarSelectionState.isInitialBoardLanding = false | |
| if sidebarSelectionState.selection == .board { | |
| scheduleTitlebarTextRefresh() | |
| return | |
| } | |
| } | |
| sidebarSelectionState.selection = .tabs | |
| scheduleTitlebarTextRefresh() | |
| }) | |
| view = AnyView(view.onReceive(NotificationCenter.default.publisher(for: .ghosttyDidFocusTab)) { _ in | |
| // The initial workspace auto-focuses at launch, which would otherwise | |
| // immediately steal the cold-launch board landing (`#board-landing`). | |
| // Consume that first post-launch focus without leaving the board; | |
| // every later focus event behaves as before. | |
| if sidebarSelectionState.isInitialBoardLanding { | |
| sidebarSelectionState.isInitialBoardLanding = false | |
| if sidebarSelectionState.selection != .tabs { | |
| scheduleTitlebarTextRefresh() | |
| return | |
| } | |
| } | |
| sidebarSelectionState.selection = .tabs | |
| scheduleTitlebarTextRefresh() | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/ContentView.swift` around lines 2616 - 2630, Update the
.ghosttyDidFocusTab handler around sidebarSelectionState.isInitialBoardLanding
so the first focus event preserves both .board and .notifications selections.
Clear isInitialBoardLanding and return without assigning .tabs when the current
selection is either of those destinations; retain the existing .tabs behavior
for all other selections.
| private func focusFirstCard(sortedColumns: [KanbanColumn], cardsByColumnId: [String: [KanbanCardSnapshot]]) { | ||
| for column in sortedColumns { | ||
| if let firstCard = cardsByColumnId[column.id]?.first { | ||
| kanbanFocusState.focusedCardId = firstCard.id | ||
| return | ||
| } | ||
| } | ||
| kanbanFocusState.focusedCardId = nil | ||
| } | ||
|
|
||
| /// Called once from `.onAppear` (the board stays mounted and only | ||
| /// cross-fades opacity, so this fires on first launch, not every visit). | ||
| /// Seeds focus to the first card of the first column when nothing is | ||
| /// focused yet, or when the previously focused card no longer exists. | ||
| private func ensureValidFocus(sortedColumns: [KanbanColumn], cardsByColumnId: [String: [KanbanCardSnapshot]]) { | ||
| if let focusedCardId = kanbanFocusState.focusedCardId, | ||
| cardsByColumnId.values.contains(where: { $0.contains(where: { $0.id == focusedCardId }) }) { | ||
| return | ||
| } | ||
| focusFirstCard(sortedColumns: sortedColumns, cardsByColumnId: cardsByColumnId) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
ensureValidFocus can seed focus onto a card in a collapsed (unrendered) column.
focusFirstCard is called from ensureValidFocus with the full sortedColumns (line 169), but moveFocusToAdjacentColumn/moveFocusWithinColumn both filter to navigableColumns (!$0.isCollapsed) before calling the same helper (lines 119, 143). If the first column containing cards is collapsed, .onAppear seeds focusedCardId to a card that isn't rendered (no visible focus ring) until the user presses an arrow key, at which point the mismatch self-heals via the fallback in moveFocusToAdjacentColumn/moveFocusWithinColumn.
🔧 Suggested fix
private func ensureValidFocus(sortedColumns: [KanbanColumn], cardsByColumnId: [String: [KanbanCardSnapshot]]) {
if let focusedCardId = kanbanFocusState.focusedCardId,
cardsByColumnId.values.contains(where: { $0.contains(where: { $0.id == focusedCardId }) }) {
return
}
- focusFirstCard(sortedColumns: sortedColumns, cardsByColumnId: cardsByColumnId)
+ focusFirstCard(sortedColumns: sortedColumns.filter { !$0.isCollapsed }, cardsByColumnId: cardsByColumnId)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private func focusFirstCard(sortedColumns: [KanbanColumn], cardsByColumnId: [String: [KanbanCardSnapshot]]) { | |
| for column in sortedColumns { | |
| if let firstCard = cardsByColumnId[column.id]?.first { | |
| kanbanFocusState.focusedCardId = firstCard.id | |
| return | |
| } | |
| } | |
| kanbanFocusState.focusedCardId = nil | |
| } | |
| /// Called once from `.onAppear` (the board stays mounted and only | |
| /// cross-fades opacity, so this fires on first launch, not every visit). | |
| /// Seeds focus to the first card of the first column when nothing is | |
| /// focused yet, or when the previously focused card no longer exists. | |
| private func ensureValidFocus(sortedColumns: [KanbanColumn], cardsByColumnId: [String: [KanbanCardSnapshot]]) { | |
| if let focusedCardId = kanbanFocusState.focusedCardId, | |
| cardsByColumnId.values.contains(where: { $0.contains(where: { $0.id == focusedCardId }) }) { | |
| return | |
| } | |
| focusFirstCard(sortedColumns: sortedColumns, cardsByColumnId: cardsByColumnId) | |
| } | |
| private func focusFirstCard(sortedColumns: [KanbanColumn], cardsByColumnId: [String: [KanbanCardSnapshot]]) { | |
| for column in sortedColumns { | |
| if let firstCard = cardsByColumnId[column.id]?.first { | |
| kanbanFocusState.focusedCardId = firstCard.id | |
| return | |
| } | |
| } | |
| kanbanFocusState.focusedCardId = nil | |
| } | |
| /// Called once from `.onAppear` (the board stays mounted and only | |
| /// cross-fades opacity, so this fires on first launch, not every visit). | |
| /// Seeds focus to the first card of the first column when nothing is | |
| /// focused yet, or when the previously focused card no longer exists. | |
| private func ensureValidFocus(sortedColumns: [KanbanColumn], cardsByColumnId: [String: [KanbanCardSnapshot]]) { | |
| if let focusedCardId = kanbanFocusState.focusedCardId, | |
| cardsByColumnId.values.contains(where: { $0.contains(where: { $0.id == focusedCardId }) }) { | |
| return | |
| } | |
| focusFirstCard(sortedColumns: sortedColumns.filter { !$0.isCollapsed }, cardsByColumnId: cardsByColumnId) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/Kanban/KanbanBoardView.swift` around lines 150 - 170, Update
ensureValidFocus to seed focus only from navigable, non-collapsed columns,
matching moveFocusToAdjacentColumn and moveFocusWithinColumn. Pass the filtered
navigableColumns to focusFirstCard while preserving validation of an existing
focused card and the nil fallback when no visible cards exist.
| private func selectWorkspace(_ workspaceId: UUID) { | ||
| guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else { return } | ||
| tabManager.selectWorkspace(workspace) | ||
| selection = .tabs | ||
| isInitialBoardLanding = false | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift
"Open board card" behavior is wired separately through two entrypoints. Both the SwiftUI card-tap path and the AppKit keyboard-shortcut path independently implement "select workspace → set selection = .tabs → clear isInitialBoardLanding", risking future drift (e.g. a future change to the landing-flag semantics that only gets applied to one path).
Sources/Kanban/KanbanBoardView.swift#L247-L252: extract the "open card" sequence (select workspace,selection = .tabs,isInitialBoardLanding = false) into one shared helper (e.g. onSidebarSelectionStateor a smallKanbanBoardActionstype) that both this closure andAppDelegatecall.Sources/AppDelegate.swift#L6630-L6643: call the same shared helper instead of re-implementing the select/selection/flag-clear sequence inline inopenFocusedBoardCardInActiveMainWindow.
📍 Affects 2 files
Sources/Kanban/KanbanBoardView.swift#L247-L252(this comment)Sources/AppDelegate.swift#L6630-L6643
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/Kanban/KanbanBoardView.swift` around lines 247 - 252, Extract the
shared “open board card” sequence from selectWorkspace(_:) into a reusable
helper, preserving workspace selection, selection = .tabs, and
isInitialBoardLanding = false. Update Sources/Kanban/KanbanBoardView.swift lines
247-252 to call it, and update Sources/AppDelegate.swift lines 6630-6643 in
openFocusedBoardCardInActiveMainWindow to call the same helper instead of
duplicating the logic.
Source: Coding guidelines
| alert.informativeText = String( | ||
| format: String(localized: "kanban.column.customColor.invalid.message", defaultValue: "\"%@\" is not a valid hex color. Use #RRGGBB."), | ||
| value | ||
| ) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use String.localizedStringWithFormat instead of String(format:) for these localized %@ messages.
Both showInvalidColumnColorAlert's message and promptDeleteColumn's title build the string via String(format: String(localized: "...", defaultValue: "...%@..."), value). Based on a retrieved learning, this repo's convention requires String.localizedStringWithFormat for exactly this pattern (the older String(localized:) overload where format args are passed separately), e.g. for clipboard.sshError.single.
🔧 Suggested fix
- alert.informativeText = String(
- format: String(localized: "kanban.column.customColor.invalid.message", defaultValue: "\"%@\" is not a valid hex color. Use `#RRGGBB`."),
- value
- )
+ alert.informativeText = String.localizedStringWithFormat(
+ String(localized: "kanban.column.customColor.invalid.message", defaultValue: "\"%@\" is not a valid hex color. Use `#RRGGBB`."),
+ value
+ )- alert.messageText = String(
- format: String(localized: "kanban.column.delete.title", defaultValue: "Delete \"%@\"?"),
- column.title
- )
+ alert.messageText = String.localizedStringWithFormat(
+ String(localized: "kanban.column.delete.title", defaultValue: "Delete \"%@\"?"),
+ column.title
+ )Based on learnings, "Only require String.localizedStringWithFormat when using the older String(localized:) overload that takes a plain String (i.e., where format arguments must be passed separately), such as for keys like clipboard.sshError.single."
Also applies to: 345-348
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/Kanban/KanbanColumnView.swift` around lines 333 - 337, Replace
String(format:) with String.localizedStringWithFormat in
showInvalidColumnColorAlert for the invalid-color message and in
promptDeleteColumn for the delete title, preserving the existing localized
format strings and arguments.
Source: Learnings
| @MainActor | ||
| final class KanbanFocusState: ObservableObject { | ||
| /// The workspace id of the currently keyboard-focused card, or `nil` when | ||
| /// nothing is focused (e.g. the board has no cards yet). | ||
| @Published var focusedCardId: UUID? | ||
|
|
||
| /// `nonisolated` so it can be used as a default argument value | ||
| /// (`kanbanFocusState: KanbanFocusState = KanbanFocusState()`) in | ||
| /// `MainWindowContext.init` / `registerMainWindow`, which are called from | ||
| /// nonisolated contexts. Initializing the `nil` default needs no isolation. | ||
| nonisolated init() {} | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift
New KanbanFocusState uses ObservableObject/@Published/@EnvironmentObject instead of @Observable. Coding guidelines call for @Observable + @State/@Environment for new cmux-owned SwiftUI state; mirroring the older SidebarSelectionState convention doesn't exempt this brand-new type.
Sources/Kanban/KanbanFocusState.swift#L13-L24: convert the class to@Observable(dropObservableObject/@Published).Sources/Kanban/KanbanBoardView.swift#L25: replace@EnvironmentObject var kanbanFocusState: KanbanFocusStatewith@Environment(KanbanFocusState.self) var kanbanFocusState.Sources/AppDelegate.swift#L548-L552: no signature change needed once the class is@Observable, sinceMainWindowContextholds it as a plain reference; just confirm the stored type still compiles under@Observable.Sources/ContentView.swift#L813: replace@EnvironmentObject var kanbanFocusState: KanbanFocusStatewith@Environment(KanbanFocusState.self) var kanbanFocusState, and change the injection sites (.environmentObject(kanbanFocusState)) to.environment(kanbanFocusState).
🧰 Tools
🪛 SwiftLint (0.65.0)
[Warning] 14-14: Classes should have an explicit deinit method
(required_deinit)
📍 Affects 4 files
Sources/Kanban/KanbanFocusState.swift#L13-L24(this comment)Sources/Kanban/KanbanBoardView.swift#L25-L25Sources/AppDelegate.swift#L548-L552Sources/ContentView.swift#L813-L813
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/Kanban/KanbanFocusState.swift` around lines 13 - 24, Convert
KanbanFocusState to the `@Observable` pattern by removing ObservableObject and
`@Published` while preserving its plain-reference storage in
Sources/AppDelegate.swift:548-552 with no signature change. In
Sources/Kanban/KanbanBoardView.swift:25 and Sources/ContentView.swift:813,
replace `@EnvironmentObject` with `@Environment`(KanbanFocusState.self); in
Sources/ContentView.swift, also replace every
.environmentObject(kanbanFocusState) injection with
.environment(kanbanFocusState).
Source: Coding guidelines
| for tab in tabs where tab.kanbanColumnId == id { | ||
| tab.kanbanColumnId = deletion.reassignedToColumnId | ||
| tab.kanbanOrder = nextOrder(inColumn: deletion.reassignedToColumnId) | ||
| } |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
Fix nested full-collection scans during batch reassignment.
Calling nextOrder(inColumn:) inside the loop performs a full scan of the tabs array (via .filter) for every reassigned workspace. As per path instructions, this results in an O(N²) per-target rescan during a batch action, which scales poorly for 1000+ workspaces.
Calculate the starting order once outside the loop and increment it for each reassigned workspace.
⚡ Proposed fix for batch reassignment
- for tab in tabs where tab.kanbanColumnId == id {
- tab.kanbanColumnId = deletion.reassignedToColumnId
- tab.kanbanOrder = nextOrder(inColumn: deletion.reassignedToColumnId)
- }
+ var targetOrder = nextOrder(inColumn: deletion.reassignedToColumnId)
+ for tab in tabs where tab.kanbanColumnId == id {
+ tab.kanbanColumnId = deletion.reassignedToColumnId
+ tab.kanbanOrder = targetOrder
+ targetOrder += 1
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for tab in tabs where tab.kanbanColumnId == id { | |
| tab.kanbanColumnId = deletion.reassignedToColumnId | |
| tab.kanbanOrder = nextOrder(inColumn: deletion.reassignedToColumnId) | |
| } | |
| var targetOrder = nextOrder(inColumn: deletion.reassignedToColumnId) | |
| for tab in tabs where tab.kanbanColumnId == id { | |
| tab.kanbanColumnId = deletion.reassignedToColumnId | |
| tab.kanbanOrder = targetOrder | |
| targetOrder += 1 | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/TabManager`+Kanban.swift around lines 102 - 105, Update the batch
reassignment loop in the tab reassignment flow to calculate the starting order
for deletion.reassignedToColumnId once before iterating, then assign that value
to each matching tab while incrementing it after each assignment. Remove the
per-tab calls to nextOrder(inColumn:) and preserve the existing column
reassignment behavior.
Source: Path instructions
|
Kanban is a product and UI call, so I’m leaving this open for team review. |
Summary
com.cmux.sidebar-tab-reorderpayload); card/column context menus cover move / archive / rename / recolor / collapse / new / delete..boardshortcut context: arrow keys move card focus, Return opens the focused card, ⌃⌥←/→ move it between columns, ⌥⌫ archives it, ⌘⇧B (and a new titlebar button, left of the bell) toggles the board.Demo Video
Screenshots below.
Design
WorkspaceGroup(whose anchor-dissolve semantics can't keep fixed/empty columns).kanban.columnsincmux.jsonvia a newCmuxSettingsKanbanCatalogSection, read on the board through@LiveSetting(hot-reloads on external edits). Per-tab column assignment (kanbanColumnId/ fractionalkanbanOrder) is per-workspace state persisted as optional fields onSessionWorkspaceSnapshot(no schema-version bump; old manifests decode cleanly).TabManager+Kanban): drag/drop, context menus, keyboard actions, and the titlebar button all route through it — per the repo's shared-behavior rule. Pure list transforms + guard rules (Archive non-deletable, keep ≥1 real column) live inKanbanColumnMutations(CmuxSettings package), unit-tested without aTabManager.ForEach(issue Nightly freezes: sidebar LazyVStack layout loop pegs main thread at 100% CPU, deadlocks CLI #2586 spin-loop class). Every board view is@MainActorand strict-concurrency clean.AppDelegate.toggleBoardViewInActiveMainWindow.Tests
KanbanColumnMutationsTests(CmuxSettings package): 14 tests over add / rename / recolor / collapse / delete, card reassignment, and the guard rules.KanbanColumnAssignmentTests:SessionWorkspaceSnapshotround-trip, back-compat decode of pre-feature manifests (fields absent → nil → default column), andKanbanColumn.defaultsinvariants.Validation
./scripts/lint-pbxproj-test-wiring.sh— ok (453 test files)python3 scripts/check-package-resolved-policy.py— OKpython3 scripts/check-workspace-package-groups.py --check— OKpython3 scripts/swift_file_length_budget.py— no touched file over budget (largest new file 400 lines)git diff --check— clean./scripts/reload.sh --tag kanban-boardon macOS (Xcode, Swift 6.x strict concurrency): board-on-launch, drag/drop across columns + Archive, collapse, card open, rename, keyboard nav, and the titlebar toggle all verified interactively. App-target compilation + XCUITests otherwise delegated to CI.Localization audit
All new user-facing strings use
String(localized:defaultValue:)and were added toResources/Localizable.xcstringswith en + jatranslatedentries: thekanban.*card/column menu + dialog + column-title keys, the 4 board-shortcut display names, andtitlebar.board.accessibilityLabel/titlebar.board.tooltip. Seeded column titles resolve to localized display names while user-renamed columns keep their text; the card Rename prompt reuses the existingalert.renameWorkspace.*strings. New shortcuts are also added toweb/data/cmux.schema.jsonand documented (en+ja) inweb/data/cmux-shortcuts.ts. Known gap: theboardVisiblekey is not yet listed in the free-formwhen-clause vocabulary string duplicated across ~20 locale files (the key works regardless); en+ja is the stated supported-locale scope, so the catalog's other ~17 locales fall back to English for the new keys.Issues
New feature; no linked bug. Board-as-default-landing could be gated behind a setting if maintainers prefer — happy to adjust.
Planned next stage (not in this PR)
A follow-up would add a second board mode, "Tickets", backed by markdown files in a configurable folder (default
.tickets/) with YAML frontmatter (id, status, priority, branch, …): status ↔ column mapping, a ticket create/edit flow, file watching, and the payoff — spawn a workspace on a ticket's branch from its card. Kept separate because it's a distinct, larger subsystem (persistent per-repo task tracking), and a generic configurable folder keeps it upstream-friendly.Notes
CHANGELOG.mdentry yet (existing entries link a merged PR number); happy to add at merge time.Checklist
reload.sh --tag kanban-boardon macOS; interactively verified board, drag/drop, keyboard nav, toggle, rename)KanbanColumnMutationsTests,KanbanColumnAssignmentTests)Summary by CodeRabbit