Skip to content

Add kanban board main view (macOS): workspaces as cards - #7981

Open
ivan-kyu wants to merge 12 commits into
manaflow-ai:mainfrom
ivan-kyu:feat/kanban-board
Open

ivan-kyu wants to merge 12 commits into
manaflow-ai:mainfrom
ivan-kyu:feat/kanban-board

Conversation

@ivan-kyu

@ivan-kyu ivan-kyu commented Jul 13, 2026 •

Copy link
Copy Markdown

Summary

  • Add a kanban board main view for the macOS app: each workspace (cmux's "tab") is a draggable card; columns are To Do / In Progress / Done (add / rename / recolor / delete) plus a collapsible Archive.
  • The board is the default landing view on cold launch; opening a card jumps into that workspace's terminal, and the terminal portal is torn down while the board is shown so it doesn't bleed through.
  • Drag cards between columns (reusing the existing com.cmux.sidebar-tab-reorder payload); card/column context menus cover move / archive / rename / recolor / collapse / new / delete.
  • Full keyboard support scoped to a new .board shortcut 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

  • Columns are a new orthogonal axis, not WorkspaceGroup (whose anchor-dissolve semantics can't keep fixed/empty columns).
  • Column definitions are global config — kanban.columns in cmux.json via a new CmuxSettings KanbanCatalogSection, read on the board through @LiveSetting (hot-reloads on external edits). Per-tab column assignment (kanbanColumnId / fractional kanbanOrder) is per-workspace state persisted as optional fields on SessionWorkspaceSnapshot (no schema-version bump; old manifests decode cleanly).
  • One shared mutation path (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 in KanbanColumnMutations (CmuxSettings package), unit-tested without a TabManager.
  • Snapshot-boundary rule respected: board rows/cards receive immutable value snapshots + closure bundles only — no store refs below the column/card ForEach (issue Nightly freezes: sidebar LazyVStack layout loop pegs main thread at 100% CPU, deadlocks CLI #2586 spin-loop class). Every board view is @MainActor and strict-concurrency clean.
  • The titlebar toggle and the ⌘⇧B shortcut both call the same AppDelegate.toggleBoardViewInActiveMainWindow.

Tests

  • KanbanColumnMutationsTests (CmuxSettings package): 14 tests over add / rename / recolor / collapse / delete, card reassignment, and the guard rules.
  • KanbanColumnAssignmentTests: SessionWorkspaceSnapshot round-trip, back-compat decode of pre-feature manifests (fields absent → nil → default column), and KanbanColumn.defaults invariants.

Validation

  • ./scripts/lint-pbxproj-test-wiring.sh — ok (453 test files)
  • python3 scripts/check-package-resolved-policy.py — OK
  • python3 scripts/check-workspace-package-groups.py --check — OK
  • python3 scripts/swift_file_length_budget.py — no touched file over budget (largest new file 400 lines)
  • git diff --check — clean
  • Built and ran locally via ./scripts/reload.sh --tag kanban-board on 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 to Resources/Localizable.xcstrings with en + ja translated entries: the kanban.* card/column menu + dialog + column-title keys, the 4 board-shortcut display names, and titlebar.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 existing alert.renameWorkspace.* strings. New shortcuts are also added to web/data/cmux.schema.json and documented (en+ja) in web/data/cmux-shortcuts.ts. Known gap: the boardVisible key is not yet listed in the free-form when-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

  • No CHANGELOG.md entry yet (existing entries link a merged PR number); happy to add at merge time.

Checklist

  • I tested the change locally (built + ran via reload.sh --tag kanban-board on macOS; interactively verified board, drag/drop, keyboard nav, toggle, rename)
  • I added or updated tests for behavior changes (KanbanColumnMutationsTests, KanbanColumnAssignmentTests)
  • I updated docs (config + shortcut docs); no changelog entry yet (see Notes)
  • I requested bot reviews after my latest commit (trigger comment below)
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by CodeRabbit

  • New Features
    • Added a Kanban board view with customizable columns, cards, collapsing, colors, drag-and-drop, archive, move, rename, and delete actions.
    • Added board-focused keyboard shortcuts and a title bar control for quick board access.
    • Added workspace renaming from the sidebar context menu.
  • Improvements
    • Board layout and workspace placement persist across sessions.
    • Added localized labels, tooltips, accessibility text, and configuration support.
  • Bug Fixes
    • Improved terminal focus handling when switching between the board and tabs.

Ivan Kyu and others added 3 commits July 13, 2026 18:38
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>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d1b858c0-5bd6-4475-925f-7b662fb81393

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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.

Changes

Kanban board feature

Layer / File(s) Summary
Settings, models, and mutations
Packages/macOS/CmuxSettings/...
Adds JSON-backed Kanban columns, defaults, safe encoding, pure mutation helpers, and board shortcut action definitions.
Persistence and TabManager integration
Sources/Workspace.swift, Sources/SessionPersistence.swift, Sources/TabManager+Kanban.swift, cmuxTests/*
Persists column assignments and fractional ordering, supports backward-compatible session decoding, and centralizes card and column mutations.
Board UI and focus navigation
Sources/Kanban/*
Adds board, column, and card views with drag/drop, context menus, modal editing flows, column management, and keyboard focus navigation.
Window and content integration
Sources/AppDelegate.swift, Sources/ContentView.swift, Sources/SidebarSelectionState.swift
Adds board selection, initial board landing, per-window focus state, environment wiring, terminal-layer gating, and board action dispatch.
Shortcuts and controls
Sources/KeyboardShortcut*.swift, Sources/Update/*, Sources/Window*.swift
Adds board shortcut contexts and defaults, delete-key handling, titlebar controls, minimal-mode layout support, and action routing.
Schema, localization, and tests
web/data/*, web/app/..., Resources/Localizable.xcstrings, skills/*, cmuxTests/*
Documents Kanban settings and shortcuts, adds English/Japanese strings, and updates affected UI and persistence expectations.

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
Loading

Possibly related PRs

  • manaflow-ai/cmux#7414: Extends the same shortcut action infrastructure with new action cases and default shortcut mappings.

Suggested reviewers: lawrencecchen, austinywang, azooz2003-bit


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (6 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error deleteKanbanColumn rescans tabs for each reassigned card via nextOrder(inColumn:), creating O(N²) work for large columns. Compute the target order once before the loop and increment it per reassigned tab, so deletion stays O(N).
Cmux Swift Concurrency ❌ Error PR adds new Combine app state via KanbanFocusState: ObservableObject/@Published, and TabManager+Kanban.writeKanbanColumns launches a fire-and-forget Task for settings writes. Move the focus model to @Observable/@Environment, and replace the unscoped write Task with an awaited async path or a retained/cancelable task boundary.
Cmux Full Internationalization ❌ Error New user-facing strings in Resources/Localizable.xcstrings and web/data/cmux-shortcuts.ts are only localized for en/ja, but the touched catalogs/surfaces support 20 locales. Add translated entries for every supported locale in Localizable.xcstrings, and localize the new web shortcut/data text across all locales in web/messages/routing.ts.
Cmux Swiftui State Layout ❌ Error PR adds new SwiftUI-owned KanbanFocusState as ObservableObject/@published; the rule requires @Observable for new cmux state. Convert KanbanFocusState to @Observable and inject via @Environment(KanbanFocusState.self); keep only AppKit bridge state as ObservableObject.
Cmux Architecture Rethink ❌ Error FAIL: selectWorkspace/.tabs/isInitialBoardLanding=false are implemented separately in KanbanBoardView.selectWorkspace and AppDelegate.openFocusedBoardCardInActiveMainWindow, violating the... Extract one board-open helper/action on a single owner (e.g. SidebarSelectionState or a Kanban action helper) and have both the card-tap and shortcut paths call it.
Cmux No Ambient Global State ❌ Error KanbanColumnMutations.swift adds a public enum used only as a static-function namespace, which the rule forbids as ambient global API. Move the mutation logic onto an injectable owning type (e.g. TabManager+Kanban/service instance) and keep any helpers private/file-local.
Docstring Coverage ⚠️ Warning Docstring coverage is 41.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (18 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: a new macOS kanban board view with workspaces shown as cards.
Description check ✅ Passed The description covers summary, design, tests, validation, localization, and checklist details; the template is mostly complete.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New board UI/state types are explicitly @MainActor, and the settings write path uses the existing JSONConfigStore actor; I found no new off-main UI-store access.
Cmux Swift Blocking Runtime ✅ Passed Diff scan found no added sync/wait/sleep/lock/asyncAfter calls in the touched Swift files; new board code uses only async UI dispatch.
Cmux Browser Automation Off-Main ✅ Passed HEAD changes only ContentView and ExtensionSidebarWorkspaceRowView; no browser.* commands or socket policy files were touched.
Cmux Expensive Synchronous Load ✅ Passed No added or moved agent-history loader appears in the diff; the touched files only add Kanban/UI code, and existing RestorableAgentSessionIndex.load() sites are unchanged.
Cmux Cache Substitution Correctness ✅ Passed PASS: snapshotValue(for:) re-reads disk (not a stale cache), and kanban snapshot fields are persisted from live workspace state; the only fallback is documented early-startup defaults.
Cmux No Hacky Sleeps ✅ Passed No changed non-Swift runtime files introduce sleeps/polling; the TS/TSX edits are static data/docs only and contain no timers or waits.
Cmux Swift @Concurrent ✅ Passed PASS: the PR adds no new nonisolated async helpers or invalid @concurrent usage; the only added async write hops to JSONConfigStore.actor via await.
Cmux Swift File And Package Boundaries ✅ Passed Kanban domain logic was extracted into CmuxSettings; app-target additions are UI/AppKit glue and shared TabManager wiring, with no new oversized or mixed-responsibility production file.
Cmux Swiftpm Lockfiles ✅ Passed PASS: The PR only adds sources/tests to cmux.xcodeproj; no Package.swift, Package.resolved, .gitignore, or workflow/dependency-pin changes, so the lockfile rule isn't triggered.
Cmux Swift Logging ✅ Passed PASS: No added print/debugPrint/dump/NSLog or new Logger constants appeared in the touched diffs; diff-wide scan found no new logging lines.
Cmux User-Facing Error Privacy ✅ Passed Added alerts/copy are generic kanban UI text; no vendor, internal provider, snapshot, or secret details appear in user-visible messages.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds Kanban UI inside the main workspace window; the only NSWindowController touched is an existing inspector window already using cmux.extensionSidebarInspector.
Cmux Source Artifacts ✅ Passed Only two modified paths are normal source files; no logs, caches, build outputs, screenshots, or scratch dirs appear in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test-only/debug seam APIs appeared in production diff; the lone added #if DEBUG just wraps a log in a titlebar action.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Ivan Kyu and others added 7 commits July 13, 2026 22:21
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>
@ivan-kyu ivan-kyu changed the title Kanban board main view (macOS): workspaces as cards Add kanban board main view (macOS): workspaces as cards Jul 14, 2026
Ivan Kyu and others added 2 commits July 14, 2026 14:34
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>
@ivan-kyu
ivan-kyu marked this pull request as ready for review July 14, 2026 07:10
@ivan-kyu

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 14, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai 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:

Learn more →

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

@ivan-kyu I'll review the PR now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a kanban board as the macOS app's main workspace view. The main changes are:

  • Configurable, persistent kanban columns and workspace assignments.
  • Card drag-and-drop, column management, archive behavior, and keyboard navigation.
  • Board landing, terminal portal switching, and titlebar controls.
  • Localized app copy, shortcut documentation, schema updates, and tests.

Confidence Score: 4/5

The app build and column persistence paths need fixes before merging.

  • The new card and column views reference an unavailable Color initializer.
  • Column writes can complete out of order or fail without notifying the caller.
  • Closely timed drops can assign duplicate card order values.

Sources/Kanban/KanbanCardView.swift, Sources/Kanban/KanbanColumnView.swift, and Sources/TabManager+Kanban.swift

Important Files Changed

Filename Overview
Sources/Kanban/KanbanCardView.swift Adds draggable card rendering but calls an unavailable hexadecimal Color initializer.
Sources/Kanban/KanbanColumnView.swift Adds column UI, menus, dialogs, and drop handling, with compile and stale-order issues.
Sources/TabManager+Kanban.swift Centralizes kanban mutations but persists column edits through unordered tasks that suppress failures.
Sources/Kanban/KanbanBoardView.swift Composes snapshot-based columns and cards with focus navigation and live settings.
Sources/SessionPersistence.swift Adds optional workspace column and order fields while preserving older snapshot decoding.
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/KanbanColumnMutations.swift Adds pure column transforms and deletion guards with focused unit coverage.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing Hex Color Initializer

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)

Comment on lines +135 to +141
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()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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)

Comment on lines +123 to +128
.onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: KanbanColumnDropDelegate(
targetColumnId: column.id,
nextOrder: (cards.map(\.order).max() ?? -1) + 1,
isTargeted: $isDropTargeted,
onCardDropped: actions.onCardDropped
))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a kanban workspace view as the macOS app's default landing screen. The main changes are:

  • Configurable columns with add, rename, color, collapse, and delete actions.
  • Persisted workspace column assignments and card ordering.
  • Drag-and-drop, context menus, keyboard navigation, and card renaming.
  • Board toggles in the titlebar and shortcut system.
  • Settings schema, documentation, localization, and focused tests.

Confidence Score: 4/5

Column persistence can lose rapid updates or diverge from workspace assignments, and the new UI lacks translations for supported locales.

  • Column actions use snapshot-based, fire-and-forget writes with no ordering or failure handling.
  • Column deletion mutates workspaces before confirming that the global column write succeeded.
  • New localization keys cover only English and Japanese.
  • Optional snapshot fields preserve compatibility with older sessions.

Sources/TabManager+Kanban.swift; Resources/Localizable.xcstrings

Important Files Changed

Filename Overview
Sources/TabManager+Kanban.swift Adds shared card and column mutations, but column writes are not serialized and discard persistence failures.
Sources/Kanban/KanbanBoardView.swift Adds board projection, card grouping, keyboard focus movement, and snapshot-based row data.
Sources/AppDelegate.swift Adds per-window board focus, cold-launch selection, toggle handling, and shortcut dispatch.
Sources/SessionPersistence.swift Adds optional workspace column and order fields while preserving old manifest decoding.
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/KanbanColumnMutations.swift Adds pure column transformations and guards for Archive and the last working column.
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift Registers board actions, labels, window requirements, and board-visible preconditions.
Resources/Localizable.xcstrings Adds board strings with English and Japanese values but omits other supported catalog locales.
web/data/cmux.schema.json Extends the shortcut configuration schema for board actions.

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]
Loading
%%{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]
Loading

Reviews (2): Last reviewed commit: "Add Rename to preset sidebar (Project Wo..." | Re-trigger Greptile

Comment on lines +133 to +138
/// 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Column Writes Lose Updates

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": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a kanban board as the macOS app's main workspace view. The main changes are:

  • Configurable columns with card movement, archive, rename, color, collapse, and deletion actions.
  • Persisted per-workspace column assignments and ordering.
  • Board-focused keyboard navigation and titlebar controls.
  • New localization, configuration documentation, schema entries, and tests.

Confidence Score: 4/5

Card ordering, rename propagation, and failed column persistence need fixes before merging.

  • Sidebar renames can leave board cards stale.
  • Concurrent drops can assign duplicate card orders.
  • A failed config write can leave persisted columns and workspace assignments inconsistent.
  • New board text is incomplete across existing catalog locales.

Sources/ContentView.swift, Sources/Kanban/KanbanColumnView.swift, Sources/TabManager+Kanban.swift, and Resources/Localizable.xcstrings

Important Files Changed

Filename Overview
Sources/Kanban/KanbanBoardView.swift Builds board snapshots, keyboard focus navigation, and workspace selection while preserving the list snapshot boundary.
Sources/Kanban/KanbanColumnView.swift Adds column rendering, menus, and drag-and-drop, with destination ordering captured too early.
Sources/TabManager+Kanban.swift Centralizes board mutations but can leave workspace and column configuration state inconsistent after a failed write.
Sources/ContentView.swift Integrates the board and extension-sidebar rename action, which bypasses the board-aware rename path.
Sources/SessionPersistence.swift Adds optional column and order fields while retaining compatibility with older snapshots.
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/KanbanColumnMutations.swift Adds pure column transforms and guards against deleting Archive or the final working column.
Resources/Localizable.xcstrings Adds board UI text but omits translations for existing supported catalog locales.

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]
Loading
%%{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]
Loading

Reviews (3): Last reviewed commit: "Add Rename to preset sidebar (Project Wo..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift
guard response == .alertFirstButtonReturn else { return }
let trimmed = input.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
tabManager.setCustomTitle(tabId: tab.id, title: trimmed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Rename Bypasses Board Updates

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +135 to +141
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()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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 @@
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +20 to +22
public var isArchive: Bool
/// Collapsed columns hide their cards, showing only the header.
public var isCollapsed: Bool

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +18 to +19
let newOrder = (columns.map(\.order).max() ?? -1) + 1
return columns + [KanbanColumn(id: id, title: title, order: newOrder)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +150 to +153
private func focusFirstCard(sortedColumns: [KanbanColumn], cardsByColumnId: [String: [KanbanCardSnapshot]]) {
for column in sortedColumns {
if let firstCard = cardsByColumnId[column.id]?.first {
kanbanFocusState.focusedCardId = firstCard.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0cac703 and 333028e.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (36)
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/KanbanCatalogSection.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SettingCatalog.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/KanbanColumn.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/KanbanColumnMutations.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction+Defaults.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutAction.swift
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutContextKnownKey.swift
  • Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/KanbanColumnMutationsTests.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/ExtensionSidebarWorkspaceRowView.swift
  • Sources/Kanban/KanbanBoardView.swift
  • Sources/Kanban/KanbanCardView.swift
  • Sources/Kanban/KanbanColumnView.swift
  • Sources/Kanban/KanbanFocusState.swift
  • Sources/KeyboardShortcutActionContext.swift
  • Sources/KeyboardShortcutContext.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/SessionPersistence.swift
  • Sources/SidebarSelectionState.swift
  • Sources/TabManager+Kanban.swift
  • Sources/Update/MinimalModeSidebarControls.swift
  • Sources/Update/UpdateTitlebarAccessory.swift
  • Sources/WindowDecorationsController.swift
  • Sources/WindowDragHandleView.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/KanbanColumnAssignmentTests.swift
  • cmuxTests/ShortcutAndCommandPaletteTests.swift
  • cmuxTests/UpdatePillReleaseVisibilityTests.swift
  • cmuxTests/WindowAndDragTests.swift
  • skills/cmux-settings/references/shortcut-actions.md
  • web/app/[locale]/(landing)/docs/configuration/page.tsx
  • web/data/cmux-shortcuts.ts
  • web/data/cmux.schema.json

Comment thread Sources/ContentView.swift
Comment on lines 2616 to 2630
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()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 Sources

Repository: 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 Sources

Repository: 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.

Suggested change
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.

Comment on lines +150 to +170
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)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +247 to +252
private func selectWorkspace(_ workspaceId: UUID) {
guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else { return }
tabManager.selectWorkspace(workspace)
selection = .tabs
isInitialBoardLanding = false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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. on SidebarSelectionState or a small KanbanBoardActions type) that both this closure and AppDelegate call.
  • Sources/AppDelegate.swift#L6630-L6643: call the same shared helper instead of re-implementing the select/selection/flag-clear sequence inline in openFocusedBoardCardInActiveMainWindow.
📍 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

Comment on lines +333 to +337
alert.informativeText = String(
format: String(localized: "kanban.column.customColor.invalid.message", defaultValue: "\"%@\" is not a valid hex color. Use #RRGGBB."),
value
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

Comment on lines +13 to +24
@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() {}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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 (drop ObservableObject/@Published).
  • Sources/Kanban/KanbanBoardView.swift#L25: replace @EnvironmentObject var kanbanFocusState: KanbanFocusState with @Environment(KanbanFocusState.self) var kanbanFocusState.
  • Sources/AppDelegate.swift#L548-L552: no signature change needed once the class is @Observable, since MainWindowContext holds it as a plain reference; just confirm the stored type still compiles under @Observable.
  • Sources/ContentView.swift#L813: replace @EnvironmentObject var kanbanFocusState: KanbanFocusState with @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-L25
  • Sources/AppDelegate.swift#L548-L552
  • Sources/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

Comment on lines +102 to +105
for tab in tabs where tab.kanbanColumnId == id {
tab.kanbanColumnId = deletion.reassignedToColumnId
tab.kanbanOrder = nextOrder(inColumn: deletion.reassignedToColumnId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 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.

Suggested change
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

@teamleaderleo teamleaderleo added area: layout Splits, panes, tabs, windows, resizing, full screen area: workspaces Workspaces, sessions, restore after relaunch, worktrees labels Sep 30, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Kanban is a product and UI call, so I’m leaving this open for team review.

This branch has not been deployed

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

Labels

area: layout Splits, panes, tabs, windows, resizing, full screen area: workspaces Workspaces, sessions, restore after relaunch, worktrees

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants