Skip to content

fix(cmux-next): Home's dock takes tabs once the tree reports its ids, not at the next connect (FC-1 follow-up) - #30826

Merged
lawrencecchen merged 2 commits into
feat-cmux-nextfrom
fix/fc1-home-dock-allow-tabs-ids
Nov 1, 2026
Merged

lawrencecchen merged 2 commits into
feat-cmux-nextfrom
fix/fc1-home-dock-allow-tabs-ids

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Home's dock now takes tabs as soon as the tree reports its ids. Before, it waited for the next connection, so the dock could take no tab for a whole launch.

FC-1 follow-up (fuzz catalog). HomeDock pins Home's dock in its dock move, then lets other tabs in with a separate column.update {allow_tabs: true} (HomeDock.allowTabs). That request names the dock's screen and pane by their resource ids. When the tree had no ids for them yet, the step returned "home dock allow_tabs waits for resource ids", and only the next connection tried again. Until then the dock took no tab at all:

Fix (Packages/macOS/CmuxNext/Sources/CmuxNextHomeEngine/HomeDock.swift): when the ids are missing, the step starts one wait on the store's records (ObservationStream, no timer, no retry loop). When the page's screen and pane first have their ids, the step runs again. For the store Home (HomeService.dock), a later step (a reconnect's) replaces the wait. A window Home makes a new HomeDock per connect (WindowHomes.ensureDock), so each connect with no ids leaves its own wait; each sends one idempotent allow_tabs: true when the ids come, then ends. The wait ends when Home's page is gone. The request is now passed in (AllowTabs), so a test can see it; the app still sends setColumnAllowsTabs(true, ...).

Related: #30503 (landed: the real refusal text), #30676 (a new terminal aimed at a dock that takes no tabs opens beside it).

Changelog

  • Fixed: Home's dock takes dragged tabs right after launch, instead of only after cmux reconnected to its daemon.

Testing

Regression test: HomeDockAllowTabsTests (CmuxNextHomeEngineTests, no daemon). It applies a tree whose Home page is in a pinned dock that takes no tabs and has no screen or pane ids, then runs the step. It expects "waits for resource ids" and no request. Then it applies the same tree with the ids, and expects exactly one column.update for the dock's screen and pane, and the "dock takes tabs" setting recorded.

  • Red at 9874b69 (test plus the request seam, no behavior change), display worker (gui-session-v1), CMUX_TEST_REQUIRE_GUI=1: step 5e7e52fc0bb67363c299a114 failed with 3 issues: sent.requests.map(\.screen) → [] == [scr_home], the same for the pane, and the setting not recorded.
  • Green at ddcf71c, display worker cmux13s (gui-session-v1), CMUX_TEST_REQUIRE_GUI=1: step c42815e311f01f1104ed86c8 passed HomeDockAllowTabsTests (1 of 1) and the existing HomeDockOpensTests (10 of 10).
  • Compile: cmux-ci compile-check --app --package CmuxNext --recipe-version 2 job 1342e802aaafd68c920b1d2b passed at ddcf71c (CmuxNext, app, cmux-tui).

Observed / Not run

  • Observed: the red and green display steps and the compile-check above, and prepush-gate.py with no new violations.
  • Follow-ups from the review (not in this PR): a comment on why the wait holds HomeDock strongly across its await; removePersistentDomain for the test's UserDefaults suite; a continuation signal instead of the Task.yield loop; tests for the wait's replacement and its end when the page goes.
  • Not run: a first-5s bench launch that shows the ids arriving late and allow_tabs then applied (FC-1's diagnostic log had no HomeDock step lines); the full CmuxNext suite; a dogfood build.

No capture: no visible change beyond Home's dock accepting tabs as it already should.

perf claim: none

… ids (FC-1 follow-up)

HomeDock's allow-tabs step needs the dock's screen and pane resource ids;
with none in the tree yet it returned 'waits for resource ids' and nothing
ran it again until the next connection, so Home's pinned dock took no tab
for the whole launch. The test applies a tree without the ids, runs the
step, then applies the ids and expects one column.update. A seam passes the
request in (no behavior change). Red on the base.
… not at the next connect (FC-1 follow-up)

HomeDock's allow-tabs step (column.update {allow_tabs: true}) needs the
dock's screen and pane resource ids. When the tree had none yet, it
returned and only the next connection tried again, so Home's pinned dock
could take no tab for the whole launch: every new tab aimed at it was
refused (dock-column-pinned) or had to open beside it. The step now arms
one wait on the store's records (ObservationStream, no timer) and runs
again when the ids first show; a later step replaces the wait, and it
ends when Home's page is gone.

Regression test: HomeDockAllowTabsTests (red at the commit before).
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review ddcf71c: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr30826 -f ref=fix/fc1-home-dock-allow-tabs-ids -f platform=macos -f backend_mode=local

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (subagent, correctness first): SHIP at ddcf71c

Read the diff, HomeDock.swift at the head, ObservationStream, HomeService+Workspace (connect path), WindowHomes and DaemonService (connection lifetime). I did not run a local build or local tests.

Correctness (no defects found)

  • Wait and replacement: everything is on the main actor. HomeDock is @MainActor, and the Task {} inherits that isolation, which the compile proves because the closure writes allowTabsWait. No suspension happens between !Task.isCancelled, allowTabsWait = nil and the call into allowTabsLogged, so a later step cannot cancel the task it runs in. The task also cannot miss a cancel. allowTabs cancels the wait before the fresh || !dockTakesTabsKey guard, so every connect that reaches the step replaces the wait, including a connect that then returns nil.
  • shown: identified returns false when the page is gone, nil while the ids are missing, and true when both are present. ObservationStream drops the repeated nil values (an Equatable element) and returns nil when the task is cancelled (withTaskCancellationHandler). It does not poll and uses no timer.
  • Two sends: a connect step can overlap a step the wait already started (the key is set only after allow returns). The result is two allow_tabs:true requests. This is idempotent, and overlapping connects could already do it at the base.
  • Old connection: the daemon connection reconnects by itself (DaemonService.start), so a wait that outlives a reconnect sends on the same live object. A new DaemonConnection object only comes at a version handoff. In that case the send can fail, the error is logged, and the next connect tries again, as at the base.
  • When the ids are present, the behavior is the same as before: allowTabsWait is nil, so the cancel does nothing. The @escaping change compiles at all three call sites (job 1342e802aaafd68c920b1d2b).

Non-blocking notes

  1. WindowHomes.ensureDock (WindowHomes.swift:138) makes a new HomeDock() for each run. For window Homes, "a later step replaces the wait" is therefore false: each connect that has no ids yet leaves its own wait. When the ids come, each wait sends one idempotent allow_tabs:true. The waits are bounded, because each ends when the ids come or the page goes. The store Home (HomeService.dock, a single instance) works as the PR describes.
  2. [weak self] does not keep the wait from holding HomeDock: await self?.shown(...) holds self strongly across the await. Window Homes depend on this, because their transient HomeDock would otherwise be released and the wait would do nothing. Please add a comment, so that nobody later "fixes" it and breaks window Homes without notice.
  3. Test: UserDefaults(suiteName: "home-dock-allow-tabs-<UUID>") is never removed, so each run leaves one plist on the test host. Add defer { defaults.removePersistentDomain(forName: name) }.
  4. Test: REVIEW.md section 6 flags the loop of 2000 Task.yield calls. I do not see a timing flake here. The store write, the continuation resume, the wait task and allow (nonisolated(nonsending)) all run on the main actor, and the main queue is FIFO, so a few yields are always enough. Awaiting a signal from Sent (a continuation or AsyncStream that the first request resumes) would match the repo rule better.
  5. The test does not cover replacement or the end of the wait (page gone). These parts are simple, and the green run covers the main path.

Evidence

  • Red at 9874b69 (the test and the seam, no change in behavior): step 5e7e52fc0bb67363c299a114. The test failed with 3 issues at HomeDockAllowTabsTests.swift:55-57, which is the stated cause. The expectations before line 55 passed.
  • Green at ddcf71c: step c42815e311f01f1104ed86c8. theDockTakesTabsOnceTheTreeReportsItsIDs passed, and HomeDockOpensTests passed 10 of 10.
  • Compile: job 1342e802aaafd68c920b1d2b finished at the head (CmuxNext and the app).

Repo rules: the // task-owner: comment is present and accurate. No timer, no poll, no main-thread block. The logger records the result of the step.

perf claim: none

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 1, 2026

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 2 files

You’re at about 90% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Sources/CmuxNextHomeEngine/HomeDock.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextHomeEngine/HomeDock.swift:206">
P2: This cancels the pending wait before the guard decides whether this step does anything. A connect that returns nil here, such as one with `fresh` false after the key is set, still drops a wait that was created with `fresh` true and would have sent the update. Move the cancellation after the guard, or only cancel when this call proceeds to the ids check.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextHomeEngine/HomeDock.swift:235">
P2: `WindowHomes.ensureDock` creates `HomeDock()` temporarily, so this weak capture can find `self == nil` after `ensure` returns and abandon the ID wait. Keep the dock alive until this task finishes.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Tests/CmuxNextHomeEngineTests/HomeDockAllowTabsTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextHomeEngineTests/HomeDockAllowTabsTests.swift:41">
P3: This named `UserDefaults` suite is never cleaned up, so every test run leaves a persistent domain behind. Keep the suite name in a variable and add `defer { defaults.removePersistentDomain(forName: suite) }`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

workspace: @MainActor @Sendable () -> WorkspaceModel?, fresh: Bool) async throws -> String? {
private func allowTabs(store: DaemonStore, workspace: @escaping @MainActor @Sendable () -> WorkspaceModel?, fresh: Bool,
allow: @escaping AllowTabs) async throws -> String? {
// This step (a connect's, or the wait's own) supersedes a wait for the ids.

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: This cancels the pending wait before the guard decides whether this step does anything. A connect that returns nil here, such as one with fresh false after the key is set, still drops a wait that was created with fresh true and would have sent the update. Move the cancellation after the guard, or only cancel when this call proceeds to the ids check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextHomeEngine/HomeDock.swift, line 206:

<comment>This cancels the pending wait before the guard decides whether this step does anything. A connect that returns nil here, such as one with `fresh` false after the key is set, still drops a wait that was created with `fresh` true and would have sent the update. Move the cancellation after the guard, or only cancel when this call proceeds to the ids check.</comment>

<file context>
@@ -179,17 +181,31 @@ public final class HomeDock {
-                           workspace: @MainActor @Sendable () -> WorkspaceModel?, fresh: Bool) async throws -> String? {
+    private func allowTabs(store: DaemonStore, workspace: @escaping @MainActor @Sendable () -> WorkspaceModel?, fresh: Bool,
+                           allow: @escaping AllowTabs) async throws -> String? {
+        // This step (a connect's, or the wait's own) supersedes a wait for the ids.
+        allowTabsWait?.cancel()
+        allowTabsWait = nil
</file context>

Comment on lines +235 to +240
allowTabsWait = Task { [weak self] in
let identified: @MainActor @Sendable () -> Bool? = {
guard let placed = workspace().flatMap(Self.pageTab(in:)) else { return false }
return placed.screen.resourceID != nil && placed.pane.resourceID != nil ? true : nil
}
guard await self?.shown(identified) == true, !Task.isCancelled, let self else { return }

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: WindowHomes.ensureDock creates HomeDock() temporarily, so this weak capture can find self == nil after ensure returns and abandon the ID wait. Keep the dock alive until this task finishes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextHomeEngine/HomeDock.swift, line 235:

<comment>`WindowHomes.ensureDock` creates `HomeDock()` temporarily, so this weak capture can find `self == nil` after `ensure` returns and abandon the ID wait. Keep the dock alive until this task finishes.</comment>

<file context>
@@ -199,15 +215,35 @@ public final class HomeDock {
+        allowTabsWait?.cancel()
+        // task-owner: `allowTabsWait`, replaced by the next step and ended when the ids come or the page goes;
+        // event-driven (Observation of the store's records), no timer.
+        allowTabsWait = Task { [weak self] in
+            let identified: @MainActor @Sendable () -> Bool? = {
+                guard let placed = workspace().flatMap(Self.pageTab(in:)) else { return false }
</file context>
Suggested change
allowTabsWait = Task { [weak self] in
let identified: @MainActor @Sendable () -> Bool? = {
guard let placed = workspace().flatMap(Self.pageTab(in:)) else { return false }
return placed.screen.resourceID != nil && placed.pane.resourceID != nil ? true : nil
}
guard await self?.shown(identified) == true, !Task.isCancelled, let self else { return }
allowTabsWait = Task { [self] in
let identified: @MainActor @Sendable () -> Bool? = {
guard let placed = workspace().flatMap(Self.pageTab(in:)) else { return false }
return placed.screen.resourceID != nil && placed.pane.resourceID != nil ? true : nil
}
guard await shown(identified) == true, !Task.isCancelled else { return }

let store = DaemonStore()
store.noteHandshake(DaemonIdentity(capabilities: [DaemonCapability.pinnedDock.rawValue], generation: "home-dock-allow-tabs"))
store.apply(snapshot: Self.tree(identified: false))
let defaults = try #require(UserDefaults(suiteName: "home-dock-allow-tabs-\(UUID().uuidString)"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This named UserDefaults suite is never cleaned up, so every test run leaves a persistent domain behind. Keep the suite name in a variable and add defer { defaults.removePersistentDomain(forName: suite) }.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextHomeEngineTests/HomeDockAllowTabsTests.swift, line 41:

<comment>This named `UserDefaults` suite is never cleaned up, so every test run leaves a persistent domain behind. Keep the suite name in a variable and add `defer { defaults.removePersistentDomain(forName: suite) }`.</comment>

<file context>
@@ -0,0 +1,59 @@
+        let store = DaemonStore()
+        store.noteHandshake(DaemonIdentity(capabilities: [DaemonCapability.pinnedDock.rawValue], generation: "home-dock-allow-tabs"))
+        store.apply(snapshot: Self.tree(identified: false))
+        let defaults = try #require(UserDefaults(suiteName: "home-dock-allow-tabs-\(UUID().uuidString)"))
+        let dock = HomeDock(defaults: defaults)
+        let sent = Sent()
</file context>
Suggested change
let defaults = try #require(UserDefaults(suiteName: "home-dock-allow-tabs-\(UUID().uuidString)"))
let suite = "home-dock-allow-tabs-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defer { defaults.removePersistentDomain(forName: suite) }

@lawrencecchen
lawrencecchen merged commit cba3257 into feat-cmux-next Nov 1, 2026
217 of 240 checks passed
@lawrencecchen
lawrencecchen deleted the fix/fc1-home-dock-allow-tabs-ids branch November 1, 2026 11:55
cmux-bot Bot added a commit that referenced this pull request Nov 1, 2026
…'s page goes, no polling (#30826 review follow-ups) (#30888)

* test(cmux-next): Home's dock must take tabs once the tree reports its ids (FC-1 follow-up)

HomeDock's allow-tabs step needs the dock's screen and pane resource ids;
with none in the tree yet it returned 'waits for resource ids' and nothing
ran it again until the next connection, so Home's pinned dock took no tab
for the whole launch. The test applies a tree without the ids, runs the
step, then applies the ids and expects one column.update. A seam passes the
request in (no behavior change). Red on the base.

* fix(cmux-next): Home's dock takes tabs once the tree reports its ids, not at the next connect (FC-1 follow-up)

HomeDock's allow-tabs step (column.update {allow_tabs: true}) needs the
dock's screen and pane resource ids. When the tree had none yet, it
returned and only the next connection tried again, so Home's pinned dock
could take no tab for the whole launch: every new tab aimed at it was
refused (dock-column-pinned) or had to open beside it. The step now arms
one wait on the store's records (ObservationStream, no timer) and runs
again when the ids first show; a later step replaces the wait, and it
ends when Home's page is gone.

Regression test: HomeDockAllowTabsTests (red at the commit before).

* test(cmux-next): HomeDock's allow_tabs wait: replacement, end when Home's page goes, no polling (FC-1 follow-up review notes)

Review notes on #30826: the test's Task.yield loop becomes a continuation
the recorded request resumes; the test's UserDefaults suite is removed;
two tests cover a later step replacing the wait (one request when the ids
come) and the wait ending when Home's page is gone. HomeDock: a comment on
why the wait holds the dock across its await (window Homes keep no
HomeDock), and allowTabsWait is readable for the tests. No behavior change.

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>

This branch was previously deployed

1 inactive deployment
ci-routing-agent — ddcf71c2 Deployed Nov 1, 2026 by lawrencecchen via namespace autofix #11187
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant