Compare commits

...
Author SHA1 Message Date
Lawrence Chen 3fad5abf1d Merge pull request #10424 from manaflow-ai/feat/coderouter-status-session-counts
Expose per-account session counts and cooldowns in account listing
2026-08-19 04:13:14 -07:00
Lawrence Chen 34f0e1eeca Expose per-account session counts and cooldowns in account listing
The status endpoint now reports, per account, how many sessions are
bound with traffic inside the routing load window, plus the account's
cooldown deadline. The coderouter CLI uses both to make sticky routing
and rate-limit cooldowns visible. The session-count read is display
only and fails to zero so it can never take the status endpoint down.
2026-08-19 03:59:50 -07:00
Lawrence Chen 882ab10e75 Merge pull request #10402 from manaflow-ai/feat/coderouter-free-tier
Make hosted coderouter free for up to 3 accounts
2026-08-18 23:44:59 -07:00
Lawrence Chen 9ad361ed37 Make hosted coderouter free for up to 3 accounts
Pricing change: a team may connect and route up to 3 provider
accounts (subscriptions) for free. More than 3 requires an active
cmux Pro or Team subscription.

- New services/coderouter/entitlement.ts: free tier checked first
  (one indexed count read); the Stripe read runs only for teams over
  the limit. Both checks fail closed.
- Session issuance: replaces the flat Pro gate. Over-limit teams
  without a subscription get 402 with the count in the message.
- Account add: connecting an account beyond the limit without a
  subscription returns 402 before anything is stored. Re-importing
  an account the team already has is always allowed, so a broken
  account stays repairable on the free tier. The accounts POST
  handler is now factory-built for dependency-injected tests.
- Billing-lapse token revocation stays unchanged: after a lapse the
  CLI renews and the free tier re-qualifies teams with <= 3 accounts.
- New analytics: entitlement_basis on route_session_issued and a
  coderouter_account_limit_reached event, both schema-whitelisted.
- CODEROUTER_HOSTED_PRO_REQUIRED=0 still disables all gating.

Boundary: exactly 3 accounts is free; the 4th needs Pro/Team.
2026-08-18 23:21:33 -07:00
Lawrence Chen ae7d761e03 Merge pull request #10387 from manaflow-ai/fix/coderouter-codex-session-spread
Spread Codex placements and pin sessions to accounts
2026-08-18 21:57:20 -07:00
Austin Wang c15d665f65 Merge pull request #10363 from manaflow-ai/issue-10103-copy-mode-selection-bleed
Fix terminal Copy Mode selection scope across split panes
2026-08-18 21:46:09 -07:00
Lawrence Chen 425f2d7efe Keep sticky sessions through in-flight credential refreshes
A bound account in state 'refreshing' is healthy; its refresh lease
resolves in seconds. Treat it as usable for sticky reuse, and give a
sticky session a short bounded wait (4 x 500ms) when the refresh lease
is busy, instead of instantly moving the session and discarding its
prompt cache. Non-sticky requests keep the fail-fast move.
2026-08-18 21:43:42 -07:00
Lawrence Chen 7d79a858f6 Degrade to legacy routing while the session table migration is pending
If the code deploys before the additive migration is applied, the
session-stickiness statements hit undefined_table (42P01). Treat that
as: no binding found, claim without the session-load ordering term, and
skip the pin. Routing behaves exactly like the pre-change code until
the migration lands, instead of erroring every Codex request. Covered
by a DB behavior test that renames the table away and back.
2026-08-18 21:38:20 -07:00
Lawrence Chen 18ed976a1b Spread Codex placements and pin sessions to accounts
Port of subrouter PR #228 to the coderouter TypeScript data plane.

Problems in the current routing:
- selectAccountForRequest picks an account per request with three
  independent statements (sweep, select, update). Nothing spans the
  read-pick-write sequence, so parallel session starts read the same
  snapshot and herd onto one account. That account burns quota first,
  then every session reroutes to the next account at once.
- No session stickiness exists. Each request rotates to the
  least-recently-used account, so the provider prompt cache is
  discarded on nearly every turn and the whole prefix is re-billed
  as uncached input across all accounts.

Fix:
- New coderouter_session_accounts table pins one agent session (the
  Codex CLI session_id header) to one account.
- selectAccountForSession honors a usable binding first (sticky), and
  moves a session only when its account is broken, cooling down,
  removed, or already attempted in this request.
- New placements claim an account atomically in one statement with
  FOR UPDATE SKIP LOCKED, ordered by fewest recently-active bound
  sessions, then least-recently-used. Concurrent claims take
  different accounts instead of the same snapshot argmax. When every
  candidate row is locked, a blocking fallback claim accepts a
  collision instead of failing the request.
- selectAccountForRequest (models, opencode) now uses the same atomic
  claim, which closes its read-pick-write race too.

Tests: unit tests for the selector and the responses proxy, and
CMUX_DB_TEST-gated behavior tests that prove sequential spread,
concurrent non-herding, stickiness, move-on-cooldown, exclusion,
and last-write-wins binding against real Postgres.
2026-08-18 21:34:14 -07:00
Abdulaziz Albahar 9d815638c7 Merge pull request #10381 from manaflow-ai/fix-legacy-beta-namespace-fallback
fix(iroh): let pre-namespace legacy iOS bindings pair with default and nightly Macs
2026-08-18 20:59:38 -07:00
Lawrence Chen 34483ba969 Merge pull request #10382 from manaflow-ai/chore-testbox-skill-moved
chore: move the Testbox skill to cmuxterm-hq
2026-08-18 20:43:39 -07:00
Lawrence Chen bc0dcaa8b9 chore: move the Testbox skill to cmuxterm-hq
The lane is fleet infrastructure we operate, not contributor guidance for this
codebase, so the prose now lives in cmuxterm-hq at
skills/infra/blacksmith-testbox/ beside macfleet and cloud-vm-ops. See
manaflow-ai/cmuxterm-hq#306.

Everything the box actually executes stays here: both workflows, the five
scripts/blacksmith-*.sh helpers, and the two guards that validate them. Only
tests/test_testbox_doc_blocks.sh follows the skill, because it validates that
prose, and hq gained a workflow so it keeps gating rather than quietly stopping.

CLAUDE.md keeps a pointer rather than dropping the subject, because an agent
starting cmux-tui Rust work reads this file and must still learn not to compile
on the Mac. Worth stating plainly: all nine trial agents found this skill from a
local file in this repo, and a pointer to another checkout is weaker
discoverability, particularly from a plain cmux clone.
2026-08-18 20:40:08 -07:00
Abdulaziz Albahar 540bf2b9e4 Merge pull request #10310 from manaflow-ai/feat-conn-method-diagnostics
feat(ios): state connection method and live transport in diagnostics reports
2026-08-18 20:29:53 -07:00
Abdulaziz Albahar 2048eebfc9 Merge pull request #10294 from manaflow-ai/feat-tsonly-secondary-gate
fix(ios): Tailscale-only must gate secondary-Mac and discovery Iroh dials
2026-08-18 20:29:44 -07:00
Abdulaziz Albahar 65d4870c00 Merge pull request #10313 from manaflow-ai/fix-gallery-picker-simstream
fix(ios): pass selectSimulatorStream in the surface gallery preview
2026-08-18 20:29:39 -07:00
Abdulaziz AlbaharandClaude Fable 5 3f2b88f307 refactor(ios): map connection method to its diagnostics enum exhaustively
Review follow-up: replace the 0/1 ternaries with an exhaustive switch into
DiagnosticConnectionMethod so a future third method becomes a compile error
instead of silently reporting as Tailscale.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-18 20:29:20 -07:00
Abdulaziz AlbaharandClaude Fable 5 58416dcbd4 test(ios): pin the runtime clock in the Tailscale-only pool tests
Review follow-up: inject a fixed date instead of wall-clock Date() so
route-selection behavior in these tests is deterministic.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-18 20:26:47 -07:00
Lawrence Chen bee0e6c546 Merge pull request #10304 from manaflow-ai/feat-tab-identity-single-owner
Make the topology own tab identity
2026-08-18 20:25:19 -07:00
Lawrence Chen 76418e4592 Merge pull request #10299 from manaflow-ai/feat-startup-orphan-terminal-projection
Fix startup wedge when several terminal hosts are unadoptable
2026-08-18 20:25:01 -07:00
Abdulaziz AlbaharandClaude Fable 5 5bc3fee2c8 fix(iroh): let pre-namespace legacy iOS bindings pair with default and nightly Macs
The bundle-isolation rollout (#9183) records iOS builds without the
X-Cmux-App-Namespace header as legacy. Those are the shipped pre-namespace
Beta binaries on the default lane, and they lost the official-namespace
default->{default,nightly} exception: discovery still listed a Nightly Mac,
but issuePairGrant denied it with target_not_pairable, surfacing on the
phone as Authorization failed while relay policy and reachability passed.

Legacy callers on the default lane now get the same default+nightly Mac
reach as official namespaced apps, applied through the single
canIOSBindingUseMac choke point so pair grants and proofed discovery agree.
Non-default legacy lanes keep exact tag matching, and canIOSBindingForgetMac
is split off the alias so the destructive forget path is not broadened.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-18 20:15:24 -07:00
Abdulaziz AlbaharandClaude Fable 5 d856159ce8 test(iroh): legacy default-lane iOS bindings must reach default and nightly Macs
Old Beta builds predate the X-Cmux-App-Namespace header and binding request
proofs, so the broker records them as legacy/default. They currently lose the
official-namespace default->nightly exception and get target_not_pairable
against Nightly Macs. These tests pin the intended compatibility: pairing and
discovery gain the fallback, non-default legacy lanes keep exact matching, and
the destructive forget_mac path is not broadened.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-18 20:08:29 -07:00
Austin Wang 11684e5064 Merge pull request #10359 from manaflow-ai/issue-10102-drop-ghosted-regression
Fix terminal file drops being ghosted after pane teardown
2026-08-18 18:12:07 -07:00
austinpower1258 485d627042 fix: complete drop result and isolate fixtures 2026-08-18 16:43:22 -07:00
austinpower1258 d4a2021b1f test: cover multiple promised image items 2026-08-18 16:40:01 -07:00
austinpower1258 94f35f8e5a fix: read each promised drop item 2026-08-18 16:39:08 -07:00
austinpower1258 b7d1c230e2 fix: handle optional drag pasteboard types 2026-08-18 16:35:24 -07:00
austinpower1258 5129950f7e fix: make transient image drops durable 2026-08-18 16:34:02 -07:00
austinpower1258 b372e09f9f fix: avoid redundant pasteboard type fallbacks 2026-08-18 15:04:32 -07:00
austinpower1258 167eb677e6 fix: expose pasteboard temporary root to drop planner 2026-08-18 14:57:53 -07:00
austinpower1258 c8f202201b fix: preserve promised image drop files 2026-08-18 14:19:45 -07:00
austinpower1258 6e56e6ff14 fix: cancel copy mode when pane deactivates 2026-08-18 14:16:33 -07:00
austinpower1258 58a0400a6e fix: recognize unix temporary drop paths 2026-08-18 14:14:07 -07:00
austinpower1258 a1a5ef6c39 fix: restore terminal file drops and retain transient images 2026-08-18 14:12:16 -07:00
austinpower1258 b0948c0341 fix: cancel copy mode on responder loss 2026-08-18 14:11:59 -07:00
austinpower1258 d3e3abd367 fix: scope terminal copy mode to focused pane 2026-08-18 14:01:01 -07:00
austinpower1258 9f556f118e test: require copy mode to end on focus loss 2026-08-18 14:00:14 -07:00
Abdulaziz Albahar 786a35d099 Show model loading and model-specific effort controls (#10098)
* test: cover pending model catalogs in shared picker

* Show model catalog loading in shared picker

* test: expose buried model loading state

* Make pending model picker unmistakable

* test: require model-specific effort picker

* Use model-specific effort metadata

* test: require native model effort picker

* test: isolate native effort picker regression

* test: isolate iOS UI test plan

* feat: add native model effort picker

* test: verify native effort choices

* test: cover all native effort choices

* test: require shared readable picker scroller

* fix: keep task pickers readable in one scroller

* test: measure visible picker labels accurately

* test: cover default model effort picker

* fix: source efforts for the default model

* fix: keep default efforts through draft restore

* test: cover unpinned default model efforts

* test: define semantic OpenCode effort ordering

* fix: order OpenCode effort variants semantically

* test: cover task model discovery failures

* fix: surface task model discovery errors

* fix: keep model discovery errors readable

* fix: resolve provider loading review findings
2026-08-18 13:58:00 -07:00
austinpower1258 c7e3d94b66 test: prevent inactive terminal drop target shadowing 2026-08-18 13:50:10 -07:00
Abdulaziz AlbaharandClaude Fable 5 7589f52b81 Isolate every iOS build by bundle identity (#9183)
* test: require iOS build isolation

* fix: isolate iOS builds by bundle identity

* fix: compile namespaced Mac backup publisher

* chore: remove shared app group example

* test: require isolated iOS OAuth cookies

* fix: isolate iOS OAuth browser cookies

* fix: close autoreview namespace gaps

* fix: complete iOS namespace isolation

* refactor: satisfy namespace isolation policy

* fix: close namespace migration review gaps

* fix: preserve isolated Iroh and OAuth admission

* test: cover isolated iOS rollout paths

* fix: authenticate isolated iOS rollout paths

* test: cover authenticated management recovery

* fix: authorize isolated management operations

* test: cover cached proof and Mac forget

* fix: preserve proof across isolated lifecycle paths

* fix: satisfy Iroh API package policy

* test: cover autoreview isolation regressions

* fix: close isolated rollout review gaps

* test: cover target and trust boundary regressions

* fix: enforce isolated pairing boundaries

* test: cover legacy tombstone migration

* fix: preserve tombstones across backup migration

* test: cover migration precedence and discovery privacy

* fix: enforce migration and discovery precedence

* test: cover push targeting and migration bounds

* fix: close push and migration isolation gaps

* test: prevent entitlement dumps on signing failures

* fix: redact signed entitlements on upload failure

* fix: make backup scope provider sendable

* fix: return localized pairing target names

* fix: drain legacy namespaced revocations

* test: track push token policy limits

* test: cover post-revocation fallback paths

* fix: refresh authority after binding revocations

* test: cover paired-Mac migration boundaries

* fix: bound and scope legacy paired-Mac migration

* fix: capture migration account without async coalescing

* fix: keep legacy migration and keychain deletion safe

* chore: refresh pull request head

* fix: bound legacy backup reconciliation

* test: cover conditional paired Mac migration

* fix: make paired Mac migration conditional

* test: cover pairing migration recovery races

* fix: serialize identity persistence recovery

* test: cover migration team and pairing lanes

* fix: pin migration team and pairing channel

* test: await development identity storage

* test: cover pairing emission and keychain scope

* fix: fail closed on app and keychain scope

* test: cover bundle-derived Mac namespaces

* fix: namespace Mac bindings by bundle

* test: cover legacy sign-out binding namespaces

* fix: revoke bindings in their stored namespace

* test: expect APNs bundle in send outcome

* test: cover untagged debug pairing identity

* fix: preserve untagged debug pairing identity

* test: cover rate-limited pending revocations

* fix: drain revocations after rate-limited registration

* test: cover safe pending revocation reconciliation

* fix: preserve active binding during revocation recovery

* fix: preserve broker proof through client wrappers

* test: retain authorization during rate-limited recovery

* fix: preserve retained binding during rate-limit recovery

* test: cover stale binding cleanup authorization

* fix: authorize stale binding cleanup

* docs: document binding authorization helpers

* test: type stale cleanup response

* fix(macos): restore compilable cleanupSurfaceState call

https://github.com/manaflow-ai/cmux/pull/10072 changed this call site to
pass workspaceID: but never landed that overload, so the macOS target
has not compiled since it merged (CI is dispatch-only and did not catch
it). Restore the existing signature; the native-mobile-surface
preservation intent needs to re-land together with its implementation.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix: wire mobile surface artifact integration

* fix: require explicit trust broker namespace

* fix: require explicit management revocation routes

* fix: reject ambiguous iOS bundle namespaces

* fix: bound identity waits and scope legacy auth

* fix: cancel queued identity operations

* fix: modernize browser change handlers

* fix: scope legacy token deletion

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-18 13:37:30 -07:00
Abdulaziz Albahar 029d652972 docs: define shared fleet capacity (#10312)
* docs: define shared fleet and messaging policy

* docs: keep messaging policy global
2026-08-18 12:34:01 -07:00
Lawrence Chen c821be8dfc Merge pull request #10306 from manaflow-ai/fix-testbox-push-requirement
fix: require a pushed commit for Testbox benchmarks, and lead the skill with the workflow
2026-08-17 23:31:24 -07:00
Lawrence Chen 0c18281d4e fix: the demo script leaked the runner it told you to worry about
Revalidating the demo after the set-difference rewrite passed the build (2m13s
then 0.14s, box stopped, exit 0), but the org check afterwards showed its warmup
run still in_progress, holding a 32 vCPU runner. The skill calls cancelling a
required step and the script did not do it, so the one entry point most people
will run was the one leaking.

It records the run it approves and cancels it from the same EXIT trap that stops
the box, so a Ctrl-C cleans up both.
2026-08-17 23:24:28 -07:00
Lawrence Chen 8dca3436f8 fix: cargo test needs umask 022 on a Testbox, and I shipped it without
I added the cargo test line to the skill without running it. A concurrency
trial ran it and it fails on a fresh box: 104 failures, exit 101, all one root
cause. `blacksmith testbox run` gives a shell at umask 0002, so test directories
are created group-writable, and cmux-remote's secure-directory check correctly
rejects an ancestor writable by other users without the sticky bit.

Hosted CI runs at umask 022, so the suite passes there and fails here. The suite
is not umask-independent, which means this lane does not reproduce CI unless the
command says so. With `umask 022` prepended, 3504 tests pass in about 88s.
Builds and clippy are unaffected.
2026-08-17 23:17:11 -07:00
Lawrence Chen 39410cd265 fix: identify your run by set difference, not a time window
Two agents dispatching nine seconds apart deadlocked. The guard correctly
refused to guess between two identical waiting gates, then told the operator to
"approve yours in the UI". An agent has no UI, and nothing in the REST API binds
a run to a Testbox ID, so the recovery was unusable by the caller the lane is
built for. One trial burned 11 minutes and an extra box getting out of it.

Snapshot the waiting gates before dispatching and take the set difference after.
That is exact where a 120 second window is not, since another operator lands
inside any window you pick. On the rare true tie the instruction is now
deterministic and needs no browser: stop your box, re-snapshot, dispatch again.

Also record the trap the same trial nearly fell into. Correlating a box to a run
by timestamp looks right and is wrong: Blacksmith rewrites a box's CREATED value
as it hydrates, measured moving 05:54:43 to 05:55:03 to 05:58:48 on one box. The
authoritative binding is the RUN URL column, which appears only once the box is
ready.

SKILL.md, benchmark.md, and the demo script now share the one algorithm.
2026-08-17 23:08:35 -07:00
Lawrence Chen 2765225598 docs: cover tests and lints, and move the threat model out of the way
Every trial exercised the evidence path, so the skill only ever showed
cargo build. An agent's real task is usually cargo test or clippy, which use the
same warm target/ and need no stage helper; show them.

Move the trust-boundary reasoning to references/trust-boundary.md and keep the
operative summary inline. An agent about to build needs the rule and the
enforcement, not the full argument. SKILL.md is 11.7 KB, still above the 10 KB
guideline; the remaining bulk is the approval block, and the eighth trial proved
that moving an executable step away from the file that needs it strands a live
box, so it stays.
2026-08-17 22:42:28 -07:00
Lawrence Chen b6a6403506 docs: stop the exit trap crying wolf on a clean run
Ninth trial ran all eight documented blocks literally, in order, each exiting 0,
repairing nothing. Two cosmetic inaccuracies left, both mine.

The EXIT trap printed "Testbox <id> is still running; no stop was authorized"
at the end of a run whose ceremony had already stopped the box, because it
inferred liveness from CONFIRM_TESTBOX_STOP_SHA being unset rather than from
the inventory it had just captured. On a clean run that reads as a failure. It
checks list-at-exit.log now.

And I cited the changed-file overhead gap as "several times larger, 8.1s against
1.2s". It measured 1.48s this run with no change to the lane. The guidance not
to read that gap as sync overhead stands; the magnitude does not, so the text no
longer promises one.
2026-08-17 22:38:59 -07:00
Lawrence Chen 93cba89d64 fix: the poll guard I added never ran, and stranded a box
Eighth trial ran the documented blocks literally, as instructed, and the
warmup block stranded a live 32 vCPU box. `grep -c .` exits 1 when the count is
zero, and the plan sets `set -euo pipefail`, so the assignment
`waiting_count="$(... | grep -c .)"` ended the script on the first poll. Zero
waiting runs is the normal state right after warmup returns, so the 150s poll
loop was unreachable dead code, and the exact failure commit 8d3edf5b28 set out
to fix happened again, one round later, in the fix itself. There is no error
text; the script simply stops, leaving a warmed box, an un-approved run, and a
receipt the trap correctly refuses to act on.

Guarded every count with `|| true` in both files.

tests/test_testbox_doc_blocks.sh now treats the plan as executable
documentation: it parses all 21 fenced bash blocks under the shell the plan
mandates, then executes each counting construct against an empty result to prove
it survives its normal first state. Verified red against the unguarded line and
green after. Syntax checking could never have caught this, which is why two
rounds of review missed it.

Two more from the same trial. benchmark.md never cancelled the warmup run, so
following it alone leaves the keepalive holding a runner until the 120 minute
timeout; it now cancels and polls to terminal state. And the setup artifact is
required evidence whose command sat in prose rather than a fenced block, so a
literal run never captured it.
2026-08-17 22:21:26 -07:00
Lawrence Chen 9ebfec987c fix: put the approval in the code block that needs it
Seventh trial ran smooth end to end, zero unexpected failures, and still found
that benchmark.md's warmup block goes straight from the receipt to
`status --wait`. The approval existed only in the surrounding prose, so anyone
copying the block parks at the environment gate, burns the full 15 minute
timeout, and strands a warmed 32 vCPU box. Prose next to a code block does not
protect someone running the code block. The approval, with the polling guard and
its own attempt transcript, is now in the code, along with the DISPATCH_EPOCH
capture it binds to. Removed the unreachable second warmup_status check while
there.

Also qualified the overhead column. The changed-file gap measured 8.1s against
1.2s for the other stages while all three synced strategy=skip, because that
stage's backup, edit, restore, and re-verify work runs inside the CLI call but
outside wall_seconds. It is real, it is not sync and transport, and
operations.md now says to compare overhead only across first-clean and
incremental-noop.

Every bash block in the plan is now syntax-checked as a unit.
2026-08-17 21:58:45 -07:00
Lawrence Chen 8d3edf5b28 fix: stop the approval guard from aborting a warmup that was fine
Sixth trial reported the run as not smooth, and it was right. The guard I wrote
in SKILL.md Step 3 queries for waiting runs the instant warmup returns. GitHub
had not surfaced the run yet, so zero matched, and the guard printed "more than
one run waiting; approve yours in the UI" and exited. Zero runs is not
ambiguity, it is "not yet". Under benchmark.md's set -e that abort left a live,
un-approved box on the org inventory with its receipt already written, and the
plan's own trap is designed to refuse to stop it.

The guard now polls for 150s, distinguishes zero from two-or-more, and says
which happened. It also tells you that an abort means you already own a running
box, so stop it before retrying or the next dispatch leaves two boxes and one
receipt.

The trap also lied about why it gave up: it printed "warmup returned no owned
Testbox receipt" when the receipt existed and the real cause was
CONFIRM_TESTBOX_STOP_SHA being unset, which is the normal state. It now reports
the condition that actually held, and names the box you must stop yourself. Its
inventory file was called list-after-warmup-failure.log even on the success
path; it is list-at-exit.log now.

Two smaller ones. Record every approval attempt separately, because a retry that
overwrites the first hides a live unapproved box from the evidence pack. And
warm-before-reading has an exception for evidence runs, whose preflight and
receipt must exist before warmup; three agents in a row hit that ordering.
Finally, changed-file rebuilds cmux-remote as well as cmux-tui, so its ~8s is a
small-edit figure, not a single-crate floor.
2026-08-17 21:37:36 -07:00
Lawrence Chen f41a1ee183 Verify R2 nightly bytes before redirect (#10317) 2026-08-17 21:32:22 -07:00
Lawrence Chen 307a2acb28 docs: fix the last three defects, including a second clock that was never captured
Fifth trial ran the lane smoothly end to end: every documented command exited
as documented, no retries, no procedural guessing, and the only non-zero exits
were the two the docs predict. 132.25s clean, 0.17s no-op, 8.03s changed-file,
box and runner both proven gone. The artifact check added last round worked: it
recorded a 746 MB binary, 48 bytes larger in the changed-file stage.

Two defects were mine from the previous commit. The artifact prose said
cmux-tui-testbox-<run-id> while the command beside it correctly said
cmux-tui-testbox-setup-<run-id>, so following the prose downloads nothing. And
required field 3 asked for an "identity run ID" for a run the same document
forbids issuing; it now says there is no such run and names the real transcripts.

The third is older and worse. operations.md promised two clocks and described
the second as the local CLI transcript covering sync, transport, and queueing.
No pack ever contained it: stage `wall_seconds` is measured on the box around
cargo, which is why it sits milliseconds above `/usr/bin/time -p` real rather
than well above it, and three separate agents nearly cited it as CLI wall time.
The plan now records the real local clock per stage in <stage>.cli-wall.txt, and
operations.md says plainly that a pack without it cannot measure Testbox
overhead at all.
2026-08-17 21:17:43 -07:00
Lawrence Chen 0a0d41243d Support signed R2 nightly assets on web downloads (#10309) 2026-08-17 21:06:57 -07:00
Lawrence Chen ae6a0f143e fix: pin before the stages, and prove a binary came out
Fourth trial agent ran the plan with zero non-zero exits and no retries
(127.03s clean, 0.17s no-op, 8.02s changed-file, box and runner proven gone),
then found the ordering defect that matters most: benchmark.md printed the
three-stage loop BEFORE the pin instruction, so following that file top to
bottom benchmarks main rather than your branch. SKILL.md had the right order,
so the two documents disagreed. The pin now has its own section ahead of the
loop, and says plainly that running the loop first measures main.

The stage helper now verifies the build produced cmux-tui/target/debug/cmux-tui
and records its size in each stage JSON. A zero exit proves cargo was happy, not
that anything was produced, and the box is destroyed before anyone can check.

Also: name the setup artifact and give the gh run download command, instead of
"keep the artifact URL" with no artifact name; document that gh run cancel takes
about five minutes to land, so in_progress right after cancelling is not a
failure; and explain the two surfaces that look contradictory after a stop, that
status still prints a completed row while list shows nothing, and that
setup-identity.json says X64 where stage records say x86_64 for the same host.
2026-08-17 20:57:34 -07:00
Abdulaziz AlbaharandClaude Fable 5 bd8d313383 fix(ios): pass selectSimulatorStream in the surface gallery preview
MacSurfaceGalleryPreviewView (#if DEBUG) builds TerminalPickerMenuActions
without the required selectSimulatorStream closure, so every Debug iOS
build from main fails compiling CmuxMobileShellUI while Release archives
skip the file and stay green. Missed in the PR 10072 partial merge, same
family as #10287/#10290/#10295.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-17 20:40:47 -07:00
Lawrence Chen 4e54c420a2 docs: fix the evidence-pack defects a third trial agent found
Third agent ran the whole plan with no failure, no retry, and every exit code
the docs predict, including the intended 75 from PREVIEW: 129.97s clean, 0.18s
no-op, 9.28s changed-file, box and runner both proven gone. What it found was
that the evidence pack the plan produces cannot be fully trusted.

`blacksmith auth whoami` writes to stderr, so the plan's bare invocation put no
org identity in the pack while the field list implied one; it captures stderr
now. `timings.json` hardcoded schema 2 while stage records self-report schema 3,
and operations.md still described stage records as schema 2, so the wrapper now
carries stage_record_schema and the prose matches the code.

The guard naming a historical evidence directory pointed at a path that does not
exist and could not: `blacksmith-testbox-e40704611ac35f4ffa153` is the first 13
characters of that SHA glued to its last 8, and it lacks the mandatory suffix. A
guard you cannot resolve protects nothing, so both files now state the rule
itself: never write into an evidence directory you did not create this run. The
superseded 161.47s/8.28s/9.13s numbers go with it; three trials have since
measured this lane directly.

The collision-avoidance branch was dead code. It tested for
blacksmith-testbox-<sha> while every directory the plan creates is
blacksmith-testbox-<sha>-<suffix>, so the timestamp path never fired and two
runs of one SHA from one PID collided. It globs the suffix now.

Finally, Blacksmith's CREATED column is a last-transition time, not a creation
time; the same box reported three different values across hydrating, ready, and
stopped. Say so, and point at the status --wait transcript for elapsed time.
2026-08-17 20:38:01 -07:00
Lawrence Chen 9e841fc193 fix: repair the five defects a second trial agent hit
Second independent agent, given only "produce defensible benchmark evidence",
found the skill from CLAUDE.md and ran the full plan first try: 129.37s clean,
0.17s no-op, 9.69s changed-file, box and runner both confirmed gone. The
receipt-bound cleanup fixed in the previous commit worked for it. Five things
still failed it.

Two were code. The post-stop poll wrote to status-before-stop.log, so the
evidence pack's own pre-stop record was overwritten with post-stop state and an
auditor would misread when the box existed; it writes status-after-stop.log now.
And cleanup-preview.json paired warmup_ref with the benchmarked source_sha,
so the destruction record named a SHA that is not on the ref beside it; it
records both refs now, schema 2, and the regression test asserts it.

Three were documentation. benchmark.md bills itself as the full plan but never
mentioned the deployment gate, so its warmup block deadlocks for 15 minutes on
`status --wait`; it now points at the guarded approval and warns against
approving workflow_runs[0]. The PREVIEW cleanup exits 75 on success, which the
same plan's mandated `set -e` turns into an abort right before cleanup; both
call sites say so and show the `set +e` wrapper. And SKILL.md claimed stopping
the box ends the warmup run, which is false: the measured run sat in_progress
for four minutes afterwards holding a 32 vCPU runner, so cancelling is now a
required step rather than a hedge.
2026-08-17 20:20:13 -07:00
Abdulaziz AlbaharandClaude Fable 5 3a15a3e053 fix(ios): honor Tailscale-only for secondary Macs and Iroh discovery
makeSecondaryClient picked routes with a bare storedReconnectRoutes call,
so with Connection Method = Tailscale the multi-Mac aggregation pool still
pinned stored Iroh routes and dialed background-control sessions over
public paths and managed relays; broker-discovered secondaries took the
same path. Route through orderedReconnectRoutes, which applies the same
strict Tailscale requirement as the foreground dial: grant-authorized
Tailscale routes still aggregate, everything else fails closed and waits
for a new external edge.

Zero-touch Iroh discovery now returns no candidates while Tailscale is
selected, skipping broker round-trips whose iroh-only results could never
be dialed.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-17 19:56:09 -07:00
Abdulaziz AlbaharandClaude Fable 5 6e27c776ac test(ios): Tailscale-only must not dial Iroh for secondary Macs
With Connection Method = Tailscale, background multi-Mac aggregation still
pins a stored Iroh route and opens an Iroh background-control session over
public paths and managed relays. makeSecondaryClient builds its routes
without the Tailscale requirement, bypassing the strict filter every other
dial path applies. Failing test only; the fix lands in the next commit.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-17 19:56:09 -07:00
Abdulaziz AlbaharandClaude Fable 5 a0c9ba476c feat(ios): state connection method and live transport in diagnostics
A transport report today only implies the connection method and carrier
transport through whichever dial events survived the bounded ring; reading
one still needs a Settings screenshot. Record the configured method
(connectionMethodConfigured) at store construction and on every foreground
so any report window states it, decode the existing
connectionMethodPreferenceChanged value into the same readable method name,
and record foregroundTransportSelected with the active route's transport on
connect and on every route change, covering both state-then-route and
route-then-state connect flows plus mid-connection promotions.

Reports now carry lines like:
  App feature event (Operation: connectionMethodConfigured, Method: Tailscale Only)
  App feature event (Operation: foregroundTransportSelected, Transport: Iroh)

en+ja catalog entries for the new method and field names.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-17 19:54:11 -07:00
Lawrence Chen 1e4592dd90 fix: unbreak receipt-bound Testbox cleanup, and close the doc gaps a test agent hit
A subagent given only "compile cmux-tui on Linux" found the skill from
CLAUDE.md and followed it to real numbers, then reported what broke.

The receipt-bound cleanup could never succeed. #10303 pinned warmup to
--ref main, so the inventory REF column is always main, while the receipt
recorded source_ref, the branch being benchmarked. cleanup.sh compared the two
and exited 66 with "inventory ownership context differs from the warmup
receipt", for every legitimate user. That pushed operators toward a bare stop
and away from the ownership check the docs spend a page justifying. The receipt
now carries warmup_ref and source_ref separately and cleanup compares the
inventory against warmup_ref. tests/test_testbox_cleanup_receipt_ref.sh drives
the real script against a stubbed CLI; it reproduces the old exit 66 and passes
on the fix.

The approval snippet took workflow_runs[0]. Every run in this lane shares a
title and a main head branch, and the agent found a stranger's run waiting two
minutes ahead of its own, so the snippet would have approved someone else's
deployment gate. It now binds to a run created after your own dispatch and
refuses when more than one is waiting, matching the demo script's guard.

Also: `list --all` is not proof nothing is burning, because the keepalive holds
a 32 vCPU runner after the box is gone, so step 7 checks the run too and shows
`gh run cancel`; say which of the two cleanup paths is authoritative and when;
state that warmup does not block; put the --idle-timeout unit on the command;
and note that the aggregator is all-three-stages or nothing.
2026-08-17 19:27:31 -07:00
Lawrence Chen 8238cecf5c feat: drop the demo script's confirmation prompt
Running a script named "testbox demo" is the consent. The spend is bounded by
--idle-timeout and by the EXIT trap that stops the box, so a prompt whose answer
is always yes was noise. It also aborted confusingly with no TTY, because `read`
returns non-zero under `set -e`. Print what is about to happen instead.
2026-08-17 19:10:24 -07:00
Lawrence Chen f9aaa2c77d docs: point the skill at the tour script and explain the cancelled run state 2026-08-17 18:51:56 -07:00
Lawrence Chen 57984fa578 feat: add a runnable Testbox tour script
Reading a runbook does not tell you what a persistent build box feels like.
scripts/blacksmith-testbox-demo.sh warms one, pins it to your pushed HEAD,
builds cmux-tui twice, and prints both times so the second build makes the
point the prose cannot.

It prints every remote command before running it, so the tour doubles as the
documentation. It stops the box it created from an EXIT trap, including on
Ctrl-C, and it only ever stops that box. Deployment approval matches runs
dispatched within two minutes of its own warmup and refuses to guess when more
than one is waiting, so it cannot approve someone else's run. --stages swaps
the two plain builds for the three measured stages.
2026-08-17 18:43:46 -07:00
Lawrence Chen d0f14053a2 docs: make blacksmith-testbox a skill an agent acts on, not a manual
The skill opened with its threat model and buried the commands, so an agent
about to compile cmux-tui learned the lane existed only after it had already
waited. Lead with the operating rule instead: warm your own box in the first
minute, before reading code, because the four minutes of hydration are four
minutes you can spend reading.

Then a seven-step CLI walkthrough covering warmup, deployment approval, pinning
the box to your commit, building or benchmarking, download, and stop. One box
per worktree and per agent, since `testbox run` synchronizes with
`rsync --delete` and two agents on one ID overwrite each other.

SKILL.md drops from 19.7 KB to 8 KB. Stage orchestration, the cleanup ceremony,
and how to read the two clocks move to references/operations.md; the full
evidence plan stays in benchmark.md. Index the skill in CLAUDE.md, which #10303
missed, so agents can find it at all.
2026-08-17 18:39:51 -07:00
Lawrence Chen 3e377bba84 fix: require and enforce a pushed commit for Testbox benchmarks
Testing the broker's cross-commit path on a live box disproved a claim I wrote
into the skill. `blacksmith testbox run` synchronizes file contents, not
history: it makes one opportunistic `git fetch` of the benchmarked commit, falls
back to copying changed files, and skips even that once fingerprints match. A
local-only commit fails on the box with `upload-pack: not our ref`, leaving the
box on the `main` checkout the warmup job made, with candidate file contents
written over it. The stage helper then died on a raw `git rev-parse
<sha>^{tree}` error.

Require the pushed commit, and pin the box to it once per box with fetch plus
`git reset --hard` before the first stage. That makes the box an exact checkout
of the benchmarked revision, so a timing always names something anyone can
fetch, and an uncommitted edit can never be what got built. The helper now
verifies the commit is present and checked out, and prints the exact push and
pin commands instead of a git internals error.
2026-08-17 18:19:07 -07:00
Lawrence Chen c8d2cac0eb Merge pull request #10305 from manaflow-ai/feat-testbox-guard-workflow
ci: run the Testbox broker guard on every pull request
2026-08-17 18:12:07 -07:00
Lawrence Chen 86f14c0997 ci: name both shellcheck codes for the trap-invoked helpers
Ubuntu 24.04 ships shellcheck 0.9, which reports trap-only functions as
SC2317; 0.10 renamed that to SC2329, which is what the file disabled. The new
lint job runs on the runner's shellcheck, so it flagged all 15 lines of the two
trap handlers. Name both codes.
2026-08-17 18:10:36 -07:00
Lawrence Chen 8ebd307452 ci: run the Testbox broker guard on every pull request
The main CI suite is dispatch-only while CI is paused, so tests/test_ci_testbox_broker_guard.py
would not have run on a pull request that weakened the lane it guards. Give it
a small always-on workflow that also shellchecks the four helper scripts and
actionlints the warmup workflow with a checksum-pinned actionlint.

No path filter, deliberately. A change that moves or renames the guard is
exactly the change a path filter would let through.
2026-08-17 18:02:25 -07:00
Lawrence Chen 2562c5ecfe Merge pull request #10303 from manaflow-ai/feat-testbox-main-broker
ci: warm cmux-tui Testboxes from a main-controlled broker
2026-08-17 18:00:08 -07:00
Lawrence Chen d28e3199fe docs: describe the broker ref guard instead of the removed reviewed pins 2026-08-17 17:21:50 -07:00
Lawrence Chen 7105bc46e4 test: guard the Testbox broker trust boundary in CI
The property that keeps candidate code out of the token-bearing job is a
workflow shape, and no runtime check can observe it without spending a 32 vCPU
box. Assert it statically: manual dispatch only, no input that selects a
source revision, a refs/heads/main guard ahead of begin-testbox, no repository
script or local composite action before the token, every checkout pinned to
github.sha, SHA-pinned actions, and a keepalive that runs from the checked-in
main script.

Verified red against the previous candidate-controlled workflow (two failures:
the candidate-selecting inputs and the missing main guard) and green here.
2026-08-17 17:20:37 -07:00
Lawrence Chen f4222dec61 ci: warm cmux-tui Testboxes from a main-controlled broker
blacksmith testbox warmup resolves the workflow definition and the hydrated
source from one --ref, so a lane that warms a candidate branch runs that
branch's copy of the workflow before begin-testbox writes the Testbox auth
token into the job. A candidate could therefore delete its own guards.

Hydrate main only. The first step refuses any ref except refs/heads/main, and
no repository code runs before the token. A candidate revision reaches the box
afterwards through blacksmith testbox run, which syncs a maintainer's worktree
onto the warm VM and needs Blacksmith org credentials that already grant box
access, so it moves no trust boundary.

The hydrated commit and the benchmarked commit are now deliberately different.
The stage helper checks the setup marker for VM identity, runner class, and
toolchain completeness instead of source equality, records the hydrated ref and
SHA under a new "hydration" block, and still fails closed when the active Rust,
Cargo, or Zig differs from what warmed the caches.

The lane no longer needs BLACKSMITH_TESTBOX_REVIEWED_REF or
BLACKSMITH_TESTBOX_REVIEWED_SHA; the environment needs a deployment branch rule
of exactly main.
2026-08-17 17:20:31 -07:00
Lawrence Chen 70f2e14f44 Make the topology own tab identity
Tab identity was re-derived on every read from either the live surface or
the slot indexes, so each reader invented its own missing-data policy: the
projection failed hard, `rebuild_resource_indexes` silently dropped the tab,
and the browser branch required a live surface. A silent drop is the worst
of the three, because the next projection tombstones durable rows that are
still live.

`State::register_tab_identity` is now the writer every placement path uses,
including `insert_surface_checked` and the browser attach path, which
previously left identity to be harvested from the surface at the next index
rebuild. The reserved-placement install in `resource_project_terminal_selected`
still writes its own placement order, which `register_tab_identity` must not
reorder; it writes the same identity fields.

Every reader now takes identity from the topology: the index rebuild no
longer consults surfaces, the projection resolver reads the owner, and the
layout-undo token and active-tab lookup drop their surface fallbacks.
`State::ensure_tab_identity_coverage` runs at the projection boundary, so a
missing identity fails that mutation by name instead of erasing durable rows.

Browser tabs now project from their durable row when their runtime is gone,
matching terminals. That closes the same failure for a browser view whose
surface never materialized.
2026-08-17 17:15:07 -07:00
Austin Wang 0fc34d7067 Merge pull request #10198 from manaflow-ai/issue-10189-claude-teams-panel-path-title
Fix Claude Teams panel PATH and agent names
2026-08-17 16:39:22 -07:00
Austin Wang 33a43031e2 Merge pull request #10211 from manaflow-ai/issue-10204-reload-build-perf
Speed up Blacksmith reload builds with warm caches
2026-08-17 16:39:09 -07:00
Lawrence Chen aa046ab0c7 Project restored tabs from durable identity, not live surfaces
`ordered_terminal_tab_ids` demanded a live surface for every pane tab, while
the projection loop right below it already accepted a restored tab that had
no surface yet. Startup restores tabs before adoption, so an unadoptable host
leaves a tab with no surface, and the ordering pre-pass aborted the whole
projection with "pane references missing surface <slot>".

The abort happened inside the exit-detach commit that was supposed to prune
that terminal, so nothing was written and every later start repeated it. One
orphan host recovered, because removing it made the tree consistent; two or
more behind the same pane wedged the session forever.

Both call sites now resolve tab identity through one helper that prefers the
live surface and falls back to the durable indexes.

Also commit the durable exit before deleting the host record in the two
startup paths that proved a host dead. The record is the only evidence that
the host existed, so a failed commit must not erase it first.
2026-08-17 16:33:18 -07:00
Lawrence Chen e276c42ede Add failing test for two dead hosts behind one pane
A daemon restart that finds several unadoptable terminal hosts in the same
pane must still start. Today the first exit-detach projects the whole tree
while the other restored tab still has no surface, so startup aborts with
"pane references missing surface <slot>" and writes nothing, which makes the
session permanently unopenable.
2026-08-17 16:25:27 -07:00
austinpower1258 04576b88f5 Bound cached products and preserve cmuxd behavior 2026-08-17 16:17:49 -07:00
Austin Wang 11c4556b1d Merge pull request #10224 from manaflow-ai/issue-10210-light-chrome-white-controls
Fix browser chrome appearance authority across focus transitions
2026-08-17 15:58:00 -07:00
austinpower1258 3950e833ad Fix shim tilde expansion and OS cache isolation 2026-08-17 15:56:48 -07:00
Austin Wang af1788ca18 Merge pull request #10234 from manaflow-ai/fix-nightly-main-compile
Fix nightly macOS Release compile on main
2026-08-17 15:52:27 -07:00
Abdulaziz Albahar 12b646bf56 iOS: remove New Task Composer beta toggle (#10291)
* test(ios): cover task composer beta toggle removal

* fix(ios): remove task composer beta setting

* test(ios): use stable settings scroll anchor

* test(ios): retain retired toggle coverage

* test(ios): narrow retired beta toggle coverage
2026-08-17 15:47:00 -07:00
austinpower1258 0274b638d4 Harden Ghostty fallback and cache architecture keys 2026-08-17 15:44:30 -07:00
Abdulaziz AlbaharandClaude Fable 5 e27b734c53 fix(ios): implement selectedMacSurfaceID store selection, unhide effectiveConnectionStatus (#10295)
Fourth and fifth Release-archive breaks from 04ff18eea6: WorkspaceDetailView
and its extensions reference store.selectedMacSurfaceID and
store.selectMacSurface(_:), which were never implemented on
MobileShellComposite (MobileShellCompositePreviewTests already encodes the
contract: starts nil, selectMacSurface sets it without touching
selectedTerminalID, workspace switch clears it). effectiveConnectionStatus
was private in WorkspaceDetailView.swift but used from
WorkspaceDetailView+Surfaces.swift, a different file.

Adds the stored property plus the clearing hook in selectedWorkspaceID.didSet,
the selectMacSurface mutator in MobileShellComposite+SurfaceFocus, and drops
the private modifier.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-17 15:39:24 -07:00
austinpower1258 dd954ccfc1 Harden reload timing and Ghostty download budgets 2026-08-17 15:27:56 -07:00
Abdulaziz AlbaharandClaude Fable 5 240c978340 fix(ios): public import for MobileWorkspacePreview.ID in capabilities API (#10290)
supportsPanelArtifacts(in:) exposes MobileWorkspacePreview.ID publicly, but
the file imported CmuxMobileShellModel with a plain import, which is internal
under InternalImportsByDefault. The Release device archive rejects it (method
cannot be declared public because its parameter uses an internal type); Debug
simulator builds do not, which is why test-ios stayed green. Matches the 24
sibling files in this package that already use public import.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-17 15:15:10 -07:00
austinpower1258 aab8ceccb7 fix(macOS): forward workspace identity during surface cleanup 2026-08-17 14:57:53 -07:00
austinpower1258 4113149d01 fix(macOS): reconcile mobile surface APIs on main 2026-08-17 14:57:53 -07:00
Abdulaziz AlbaharandClaude Fable 5 21c48914dd fix(ios): make artifact failure and scope switches exhaustive (#10287)
* fix(ios): make artifact failure and scope switches exhaustive

04ff18eea6 added ChatArtifactError.unknown(code:) and the .panel loader
scope but missed two switches, so the cmux-ios scheme has not compiled
since Aug 14 and every iOS TestFlight upload since build 20260814211222
has failed. Adds the .unknown failure presentation (localized en/ja,
never blames connectivity) and routes .panel loader scope to the .panel
viewer scope.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): map unknown artifact errors in MarkdownSurfaceModel

The same 04ff18eea6 change left a third non-exhaustive switch in
MarkdownSurfaceModel.failure(for:), hidden behind the CmuxAgentChatUI
compile failure because CmuxMobileShellUI builds after it. Map
.unknown(code:) to .loadFailed(code:), whose doc contract is exactly
this case (the Mac answered with an unrecognized error), preserving the
code for display.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-17 14:44:11 -07:00
Abdulaziz Albahar e2fedffd0f Merge pull request #10112 from manaflow-ai/fix-ios-push-toggle-off
Fix iOS push alerts opt-out toggle
2026-08-17 11:49:17 -07:00
Abdulaziz Albahar 9c5f7d072f fix(ios): stop replay scroll drain watchdog loop (#10186)
* test(ios): bound verified replay scroll drain ownership

* fix(ios): bound verified replay scroll ownership
2026-08-17 11:27:31 -07:00
Austin Wang 7f9af0f900 Merge pull request #10223 from manaflow-ai/issue-10222-double-open-preferred-editor
Fix double-open: deliver each terminal open event to exactly one handler
2026-08-16 12:57:06 -07:00
austinpower1258andClaude Fable 5 aa025fa30a review: settle-window open-url assertion; drop debug payload field
CodeRabbit follow-ups: the UI test now also asserts the open-url capture
stays at exactly one entry through the settle window, and
debugSimulateCommandClick no longer exposes the open-url dispatch state
in its payload (nothing consumed it).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-15 20:13:39 -07:00
austinpower1258 f0da2a5262 fix: preserve inherited appearance for WebKit refresh 2026-08-15 19:21:17 -07:00
austinpower1258 f3a2eb940c fix: keep browser chrome on resolved surface appearance 2026-08-15 19:15:41 -07:00
austinpower1258andClaude Fable 5 e8ad112051 fix: deliver each open event to exactly one handler (#10222)
Root cause: one cmd-click could be handled by two independent open paths.
When the click landed on a link Ghostty recognizes (URL regex or OSC 8
hyperlink), Ghostty consumed the release and dispatched its open_url
action synchronously inside ghostty_surface_mouse_button; cmux routed it
through TerminalLinkOpenCoordinator. Back in handleCommandClickRelease,
the cmd-click word-path fallback ALSO ran: its guard deliberately lets
pointer-snapshot resolutions through when Ghostty consumed the release,
because consumption alone can also mean mouse reporting or prompt clicks.
Both paths then opened the same target, each with its own routing
decision, so with app.preferredEditor configured an image opened in both
Preview (coordinator, raw NSWorkspace) and the preferred editor
(fallback, PreferredEditorService) at once.

Fix, at the routing layer:

- GhosttyNSView now counts Ghostty open_url dispatches; the release path
  snapshots the counter around the ghostty_surface_mouse_button call.
  When it advanced, Ghostty owns the click's open and the word-path
  fallback stays out entirely. The snapshot exception still applies to
  consumed-but-not-link releases (mouse reporting, prompt clicks), so
  cmd-click on paths inside TUIs keeps working.
- TerminalLinkOpenCoordinator routes local-file external opens through
  the shared FileOpening seam (PreferredEditorService) instead of raw
  NSWorkspace, so the single chosen handler is the preferred editor when
  configured and the system default otherwise, matching every other file
  entrypoint. Non-file URLs still open through the system opener.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-15 19:08:07 -07:00
austinpower1258 d93e73b3d7 Scope reload caches to checkout paths 2026-08-15 19:03:44 -07:00
austinpower1258andClaude Fable 5 e1b1c00bea test: regression coverage for double-open with preferredEditor (#10222)
Two failing behavior tests reproducing issue #10222:

- TerminalLinkOpenCoordinatorTests: with app.preferredEditor configured, a
  local file that is not routed inside cmux must go through the
  preferred-editor seam, never the raw system opener. Fails today because
  TerminalLinkOpenCoordinator hands local files to NSWorkspace directly,
  ignoring the preferred editor.
- TerminalCmdClickUITests: cmd-clicking an OSC 8 file hyperlink must reach
  exactly one handler. Fails today because Ghostty consumes the click and
  dispatches its open-url action AND the cmd-click word-path fallback also
  opens the pointer's resolved path, delivering the same click to two
  handlers (e.g. Preview and the preferred editor at once).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-15 19:01:54 -07:00
austinpower1258 9d67c5c624 test: cover browser chrome authority across focus 2026-08-15 19:00:25 -07:00
austinpower1258 3fd79f1928 Reuse exact SPM state on warm reloads 2026-08-15 18:57:44 -07:00
Austin Wang 77adc69121 Merge pull request #10149 from manaflow-ai/issue-10146-dock-theme-mismatch
Fix Dock chrome theme authority mismatch
2026-08-15 18:36:09 -07:00
Abdulaziz Albahar ec4389e94e fix(ios): pin dogfood auth identity end to end (#10185)
* test: reproduce ambiguous iOS dogfood identity

* fix(ios): pin dogfood auth identity end to end

* docs: use every Mac fleet slot for heavy work

* fix(ios): reject agent profile on physical iPhone gates

* fix(ios): preserve profile resolution and auth replacement

* fix(ios): stop tagged Mac before auth relaunch

* fix(auth): gate Mac session replacement on resolved credentials

* fix(auth): reuse matching tagged Mac sessions

* fix(ios): preserve queued builds and secret isolation

* fix(auth): restart stale Mac for default device launch

* fix(ios): preserve queue fallback checkout

* fix(auth): close profile and tagged app teardown gaps

* fix(auth): use actor-backed app termination wait

* fix(ios): clarify separate build and launch argument contracts

* fix(auth): use bootstrap-owned Mac auth readiness

* fix(auth): launch tagged Mac with workspace environment

* fix(ios): mark unauthenticated queue intent explicitly

* fix(ios): authenticate staged queue deliveries
2026-08-15 17:02:30 -07:00
Abdulaziz Albahar 52e1b9c9a1 Merge remote-tracking branch 'origin/main' into fix-ios-push-toggle-off 2026-08-14 21:27:21 -07:00
austinpower1258 843a9ac2be Harden reload cache fallbacks 2026-08-14 21:20:48 -07:00
Abdulaziz Albahar 04ff18eea6 Remove iOS todo row sparkles and preserve native mobile surfaces
Remove iOS todo row sparkles and preserve native mobile surfaces
2026-08-14 21:18:27 -07:00
austinpower1258 152ec20140 Make reload DerivedData cache reusable 2026-08-14 20:54:51 -07:00
Abdulaziz Albahar 8677b43be3 fix(push): await bounded opt-out cleanup 2026-08-14 20:53:58 -07:00
Austin Wang 5734a451cc Merge pull request #10207 from manaflow-ai/issue-10155-browser-default-zoom
Add configurable default browser zoom level
2026-08-14 20:47:28 -07:00
austinpower1258 4407564458 fix: retain Claude launch path dependency 2026-08-14 20:43:02 -07:00
austinpower1258andClaude Fable 5 6b955a4b1c test: snapshot persisted defaults, not resolved values
preservingDefaults and the socket zoom test captured object(forKey:),
which reads through the registration domain; after BrowserPanel
registers browser fallbacks, restoring would persist a fallback for a
key that was never written. Snapshot the persistent domain instead.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 20:42:42 -07:00
Austin Wang 21fac23d5f Merge pull request #10193 from manaflow-ai/issue-10191-claude-teams-docs
Fix Claude Teams shim and panel diagnostics docs
2026-08-14 20:35:27 -07:00
austinpower1258 14f33ab531 fix: validate versioned Claude title metadata 2026-08-14 20:34:22 -07:00
austinpower1258 1a9453a3bc fix: initialize Claude teammate panel titles 2026-08-14 20:33:11 -07:00
Abdulaziz Albahar 869b9929c5 fix(push): preserve ambiguous cleanup state 2026-08-14 20:29:37 -07:00
austinpower1258 5a6d422034 Speed up Blacksmith reload builds 2026-08-14 20:28:00 -07:00
austinpower1258andClaude Fable 5 dd8ab01ba1 Add configurable default browser zoom
Adds browser.defaultZoomLevel (0.25–5.0, default 1.0) as a shared
BrowserZoomSettings policy: applied at webview creation and prewarm
adoption, used by the Actual Size/reset action, editable in Settings >
Browser, configurable via cmux.json, and documented in the schema and
key reference. browser.zoom.set now also accepts an absolute numeric
zoom and a `surface` alias for surface_id, mirrored in the CLI
(`cmux browser zoom 0.8`).

Fixes #10155

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 20:16:11 -07:00
Abdulaziz Albahar 8fa502120b fix(push): bound recovery workers 2026-08-14 20:15:33 -07:00
austinpower1258 218eb4dc69 test: cover configurable browser zoom 2026-08-14 20:12:21 -07:00
austinpower1258 67d408bfca test: cover initial Claude teammate panel title 2026-08-14 20:10:16 -07:00
Abdulaziz Albahar b4ff55127a fix(push): close notification intent races 2026-08-14 19:51:57 -07:00
austinpower1258 f438cdbd85 fix: recognize versioned Claude teammate launches 2026-08-14 19:47:07 -07:00
austinpower1258 564541f02b test: cover versioned Claude teammate executables 2026-08-14 19:45:59 -07:00
Abdulaziz Albahar d1ef41b535 fix(push): make cleanup migration transactional 2026-08-14 19:36:47 -07:00
austinpower1258 9e920b4373 fix: preserve agent title through spinner frames 2026-08-14 19:20:57 -07:00
austinpower1258 d6b75fe6ee fix: snapshot agent titles at runtime callback boundary 2026-08-14 19:15:36 -07:00
Abdulaziz Albahar d212b57054 fix(push): index durable cleanup obligations 2026-08-14 19:13:06 -07:00
austinpower1258 a4941d13a4 Complete Claude Teams locale parity 2026-08-14 19:11:32 -07:00
Abdulaziz Albahar 514684743a fix(push): preserve generation through cleanup recovery 2026-08-14 18:58:21 -07:00
austinpower1258 6b5c869114 fix: restore login PATH and names for agent panels 2026-08-14 18:55:57 -07:00
austinpower1258 7d0687b431 test: cover claude teams panel shell and title regressions 2026-08-14 18:55:57 -07:00
austinpower1258 58b475529f Clarify Claude Teams shim directory root 2026-08-14 18:54:22 -07:00
Abdulaziz Albahar 8dc9926c38 fix(push): preserve cleanup intent across retries 2026-08-14 18:48:30 -07:00
Abdulaziz Albahar d3841ca493 fix(push): supersede direct cleanup retries 2026-08-14 18:39:34 -07:00
austinpower1258 bb0733e104 Complete Claude Teams localization coverage 2026-08-14 18:36:24 -07:00
Abdulaziz Albahar 3a25ecf3d1 fix(push): continue paged cleanup drains 2026-08-14 18:33:45 -07:00
Abdulaziz Albahar 0c443b5520 fix(push): discard stale cleanup index entries 2026-08-14 18:25:39 -07:00
Austin Wang 970184b961 Merge pull request #10177 from manaflow-ai/issue-4250-omnibar-file-paths
Fix browser omnibar absolute local file paths
2026-08-14 18:22:07 -07:00
Abdulaziz Albahar 3f6a681ba2 fix(push): page durable cleanup overflow 2026-08-14 18:19:38 -07:00
austinpower1258 ced18fc9e2 Fix Claude Teams diagnostics documentation 2026-08-14 18:14:52 -07:00
Abdulaziz Albahar d7186080b8 Expose iOS Networking controls and reset (#10184)
* Add iOS networking diagnostics controls

* Harden networking settings reset
2026-08-14 18:11:34 -07:00
Lawrence ChenandAbdulaziz Albahar fb2b512e1a Add Keep Mac Awake control on Mac and iOS (#7564)
* Prevent Mac sleep while agents run or iPhone is connected

Keeps the Mac reachable for the iOS app and long-running agent tasks by
holding a single PreventUserIdleSystemSleep power assertion (display
still sleeps) while either gate is active:

- power.preventSleepWhileAgentsRunning.enabled (default off): any
  workspace has a registered agent PID (SleepyAgentCensus).
- power.preventSleepWhileMobileConnected.enabled (default on): at least
  one iOS client connection (MobileHostConnectionRegistry).

Mechanism: new PowerAssertionHolder extracted from SleepyModeController
(which now uses two holders, behavior unchanged); new PreventSleepManager
recomputes a pure decision (PreventSleepDecision) on agent-model changes,
per-TabManager tabsPublisher emissions, mobile host status changes, and
settings flips, and acquires/releases the assertion idempotently.
Released on app termination.

Settings: two toggles in the Mobile section (Keep Awake group), catalog
keys in cmux.json, search aliases, EN+JA localization.

Validated empirically on a Mac mini (macOS 26.5): with 1-minute idle
sleep armed and protections removed the machine slept and dropped off
Tailscale (brief DarkWake windows only); with a headless process holding
this exact assertion, 157/157 reachability probes over 8 minutes passed
with zero sleep entries.

* Address review: fix stale-tab reconcile, gate agent census behind setting

- tabsPublisher emits during willSet, before tabManager.tabs commits.
  Reconcile now counts agents from the emitting manager's new tab list
  (override) instead of the stale census, so closing the last agent
  workspace releases the sleep assertion immediately.
- Agent observation (tab subscriptions, per-workspace change streams,
  census scan) now attaches only while the default-off agents setting
  is enabled; disabled users pay no observer fanout.
- PreventSleepManager owns its own UserDefaults observer; AppDelegate
  keeps only start()/stop() lifecycle calls (budget +2 accepted).
- Register the two Keep Awake rows in the settings search anchor
  contract list (fixes SettingsRowAnchorResolutionTests).

* Make power assertion logger nonisolated, align subsystem with app convention

* Gate mobile keep-awake on authenticated connections; bound agent-event fanout

- MobileHostServiceStatus gains authenticatedConnectionCount (connections
  with at least one authorized request, from clientIDsByConnectionID);
  activeConnectionCount is unchanged for existing consumers. The service
  posts mobileHostStatusDidChange on the unauthenticated->authenticated
  transition and after authenticated-connection teardown so keep-awake
  reconciles at both edges.
- PreventSleepManager's mobile gate now uses the authenticated count via
  a lean accessor (no statusSnapshot route resolution per reconcile), so
  unauthenticated LAN peers holding TCP sessions cannot pin the power
  assertion.
- Per-agent runtime events no longer trigger app-wide sweeps: the manager
  maintains per-workspace running-agent counts, updates only the emitting
  model's count, and reconciles only when that count changes. Full rebuilds
  happen only on topology/settings/mobile syncs. SleepyAgentCensus reverts
  to main (helper no longer needed).
- Unit test: authenticatedConnectionCount tracks authorized connections
  only (no double-count per connection, drops on close).

* Only record client IDs for verbs that required authorization; lean mobile reconcile

- onAuthorizedRequest fires for auth-exempt verbs too (authorizeRequest and
  authorizationError both return nil for mobile.host.status), so a spoofed
  client_id on the unauthenticated status verb was recorded and counted as
  an authenticated connection, letting an unauthenticated LAN peer hold the
  keep-awake assertion. Both listener sites now skip recording unless
  requiresAuthorization(method:) is true.
- PreventSleepManager's mobile status observer now runs the constant-work
  reconcile instead of a full topology sync; mobile events cannot change
  workspace topology or agent observation.

* Count authenticated connections at the authorization gate, not the client-id hook

mobile.events.subscribe is authorized but intercepted before
onAuthorizedRequest, and subscribe requests need not carry client_id, so a
phone connected only for live events counted 0 and keep-awake released the
assertion mid-stream. Connections are now marked authenticated in the
authorizeRequest gate itself (any auth-required verb that passes), tracked
in a dedicated authorizedConnectionIDs set that backs
authenticatedConnectionCount; client-id bookkeeping stays as-is for
viewport cleanup. Set cleared on listener adoption/stop/debug-reset and on
connection close (with a status post so keep-awake drops promptly). Test
updated: client-id recording alone no longer counts, gate marking does.

* Refuse stale authenticated-connection inserts after mid-authorization disconnect

authorizationError suspends on network Stack verification; a client that
disconnects during that await has already been cleaned up by onClose, so an
unconditional insert in recordAuthorizedConnection would never be removed
and would pin the keep-awake assertion until the listener restarts. The
record now requires the connection to still be tracked (activeConnections
or the connection registry); record and removal both run on the main actor,
so either interleaving converges. Registry gains contains(id:). Test covers
the refused stale insert via a real liveness gate (never-started dummy
session registered through a DEBUG seam).

* Make PreventSleepDecision a value type instead of a top-level function

Mirrors MobileHostSyncDecision/MobileHostPortApplyOutcome: the pure policy
lives on a value type with stored inputs and an isDesired property, giving
the power feature a scoped API instead of a module-wide free function.

* Replace new debug seams with internal members reached via @testable import

Aziz test/debug-seam policy (cmux PR #6452 precedent): the two seams added
for authorized-connection tests are removed; recordAuthorizedConnection and
activeConnections are internal instead, and the test registers its
never-started dummy session directly.

* Add Keep Mac Awake control on Mac and iOS

* Tolerate headless app activation in caffeine UI test

* Fix iOS caffeine settings compilation

* Use mobile localization support in caffeine settings

* Remove reintroduced disabled toast setting

* Initialize task composer setting on iOS

* Test iOS Keep Mac Awake control

* Capture Keep Mac Awake menu evidence

* Fix iOS test Foundation import

* Revert "Fix iOS test Foundation import"

This reverts commit 37897fcf59.

* Harden mobile caffeine status handling

* Annotate caffeine RPC handlers on main actor

* Harden ambiguous mobile caffeine mutations

* Report failed caffeine mutations accurately

* Keep caffeine retry available after unknown failure

* Isolate caffeine controller injection on main actor

* Fence caffeine snapshots by state revision

---------

Co-authored-by: Abdulaziz Albahar <[email protected]>
2026-08-14 18:09:35 -07:00
Abdulaziz Albahar 48730ca18d Merge origin/main into fix-ios-push-toggle-off 2026-08-14 18:08:05 -07:00
Abdulaziz Albahar cd37b220f6 Preserve migrated Tailscale routes during Iroh refresh (#10188)
* test: preserve tailscale grant during iroh refresh

* fix: retain migrated tailscale grants during route refresh
2026-08-14 18:06:42 -07:00
Abdulaziz Albahar 708bc6b89b fix(push): preserve overflow cleanup obligations 2026-08-14 18:05:56 -07:00
Austin Wang 7418867200 Merge pull request #10175 from manaflow-ai/issue-10156-fork-session-id
Make Claude SessionStart the sole owner of fork session identity
2026-08-14 18:02:56 -07:00
austinpower1258andClaude Fable 5 1f637ec007 Address CodeRabbit review findings
- Dock setSurfaceResumeBinding no longer runs the mutating
  managedAgentResumeBinding sync-on-read before the Codex acceptance
  guard: a rejected incoming binding must not promote the effective
  binding to managed state as a side effect of the check.
- Dock setSurfaceResumeBinding now drops the panel's transient
  resume-session working-directory rescue when the binding replaces a
  different session, mirroring the Workspace path, so close/reopen
  cannot launch a replacement session in the previous session's cwd.
- The fork SessionStart/SessionEnd regression tests assert exact
  resume-set / resume-clear request counts, so an extra publication
  targeting the parent surface cannot slip past the last-request checks.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 17:47:50 -07:00
Abdulaziz Albahar 8033c260b7 Fix iOS diagnostics share link hit targets (#10180)
* test: cover iOS diagnostics share targets

* fix: restore iOS diagnostics share link taps
2026-08-14 17:33:33 -07:00
Austin Wang 5979603a89 Merge pull request #10130 from manaflow-ai/issue-10128-pi-hook-timeout-telemetry
fix(pi): diagnose configurable hook timeouts
2026-08-14 17:20:03 -07:00
Abdulaziz AlbaharandClaude Fable 5 b89cdb16e8 fix(ios): distinct group glyph in task composer group row (#10176)
The Workspace group row in the new-workspace composer fell back to the
folder SF Symbol, nearly identical to the Directory row's folder.fill
directly above it. Use rectangle.3.group (the glyph already used for
group actions) so the row reads as a group of workspaces instead of a
second directory picker.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-14 16:55:54 -07:00
Abdulaziz Albahar ad4da8fb87 fix: bound native auth request fan-out
Merge authorized by user.
2026-08-14 16:32:11 -07:00
austinpower1258 a1f2b605f1 Merge remote-tracking branch 'origin/main' into issue-10156-fork-session-id
# Conflicts:
#	Sources/DockSplitStore+SurfaceResume.swift
#	Sources/Workspace.swift
2026-08-14 15:52:21 -07:00
Austin Wang ac018861d9 Merge pull request #10133 from manaflow-ai/issue-9756-sidebar-git-watch-lstat-storm
fix: bound sidebar git status watching
2026-08-14 15:50:50 -07:00
Austin Wang 601f1d3630 Merge pull request #7459 from owenjohnson/feat-system-tree-layout-field
feat(control-socket): emit split-layout geometry in `system.tree`
2026-08-14 15:50:20 -07:00
austinpower1258 1af355d7d5 fix: classify omnibar absolute paths as file URLs 2026-08-14 15:46:41 -07:00
austinpower1258 5a2a3098b1 test: reproduce omnibar absolute file paths 2026-08-14 15:43:55 -07:00
cmux reload-cloudandClaude Fable 5 20a9d78a79 fix(pi): bound lifecycle backlog, respect feed deadline
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 14:16:15 -07:00
cmux reload-cloudandClaude Fable 5 c8c1225ac6 test(pi): shed feed work behind stalled hooks
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 14:13:30 -07:00
austinpower1258andClaude Fable 5 3131545160 Make Claude SessionStart the sole owner of fork session identity
Forked Claude tabs kept the parent session id: identity could be minted
from the --resume launch argument (process-scanner fork fallback), from
launch-argv sniffing in the hook CLI, and hooks could stamp identity onto
the workspace's focused pane. Fork-of-fork and close/reopen then resumed
the wrong conversation.

Now only an authoritative Claude SessionStart on its owning surface can
install or replace that surface's session identity:

- upsertAuthoritativeClaudeSessionStart installs the record and the
  active-surface boundary in one locked transaction; stale late
  startup/resume events are rejected unless the owner allows replacement
  or the incoming process is demonstrably newer; /clear stays an
  ordering barrier.
- Every Claude hook handler requires an authoritative delivery target;
  focused-pane fallback can no longer receive another process's identity.
- The accepted SessionStart immediately publishes the surface resume
  binding and projects it into the restorable-agent snapshot used by
  close history and workspace restore, so a fork cannot retain its
  parent's restore state.
- The Claude launch-argument identity fallback is removed from the
  process scanner and hook CLI; the generic fork-parent fallback remains
  for Codex, where the resumed id genuinely is the session id.

Test fixtures now model the new registration: SessionStart records the
pid on the hook record, so live-process evidence flows from the record,
never from --resume argv; a fail-closed hook that never wrote the store
file counts as "no record".

Closes #10156

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 13:10:19 -07:00
Austin Wang c742541432 Merge pull request #10100 from manaflow-ai/issue-9629-codex-resume-binding-verification
fix: verify Codex resume binding ownership
2026-08-14 13:00:48 -07:00
Abdulaziz Albahar 4364c2d23f fix(push): bound pending cleanup state 2026-08-14 03:15:56 -07:00
Abdulaziz Albahar c32c6311b7 test(push): bound pending cleanup storage 2026-08-14 03:14:54 -07:00
Abdulaziz Albahar d9a4ea5747 fix(push): authorize before backend reconciliation 2026-08-14 03:08:06 -07:00
Abdulaziz Albahar d34c247866 fix(push): let opt-outs bypass stale work 2026-08-14 02:54:04 -07:00
Abdulaziz Albahar 9800bddc17 test(push): require immediate opt-out cleanup 2026-08-14 02:52:47 -07:00
Abdulaziz Albahar 1fa42306cf refactor(push): own intent reconciliation 2026-08-14 02:37:40 -07:00
Abdulaziz Albahar 3607cc8f69 fix(ios): reconcile started push mutations 2026-08-14 02:21:29 -07:00
Abdulaziz Albahar c9be5246a7 fix(ios): supersede stale push intents 2026-08-14 02:15:50 -07:00
Abdulaziz Albahar 9aae5b34b8 test(ios): cover superseded push intent 2026-08-14 02:08:49 -07:00
Abdulaziz Albahar 38ffb32c68 fix(ios): honor resolved push state 2026-08-14 01:53:47 -07:00
Abdulaziz Albahar e6d722d5a8 fix(ios): preserve queued push intent 2026-08-14 01:49:42 -07:00
Abdulaziz Albahar 71818d7367 fix(ios): serialize push toggle mutations 2026-08-14 01:40:58 -07:00
Abdulaziz Albahar 1834813593 test(ios): isolate push fixture state 2026-08-14 01:36:12 -07:00
Abdulaziz Albahar 2f0e084e10 test(ios): bound pending push fixture 2026-08-14 01:35:46 -07:00
Abdulaziz Albahar 253e6707fd fix(ios): keep push toggle interactive 2026-08-14 01:34:57 -07:00
Abdulaziz Albahar dc379323d2 test(ios): signal pending push mutation 2026-08-14 01:29:30 -07:00
Abdulaziz Albahar ed6e5e0297 fix(ios): update push toggle optimistically 2026-08-14 01:20:18 -07:00
Abdulaziz Albahar 4666aa7b75 test(ios): cover pending push opt-out 2026-08-14 01:20:09 -07:00
Abdulaziz Albahar c9cc7c1309 refactor(ios): reduce push toggle fix scope 2026-08-14 01:18:46 -07:00
Abdulaziz Albahar 8a982c134a Merge remote-tracking branch 'origin/main' into fix-ios-push-toggle-off 2026-08-14 01:18:32 -07:00
Abdulaziz Albahar 8a8fb0b835 fix(ios): recover timed-out push cleanup 2026-08-14 01:09:28 -07:00
Abdulaziz Albahar d7ebaae630 test(ios): recover timed-out push opt-out 2026-08-14 01:03:57 -07:00
Abdulaziz Albahar c33317abdf refactor(push): use one reconciliation worker 2026-08-14 00:52:45 -07:00
Abdulaziz Albahar e599af99e5 fix(push): order sign-out with newer registration 2026-08-14 00:14:36 -07:00
Abdulaziz Albahar 86c89f5515 test(push): protect newer registration from stale sign-out 2026-08-14 00:11:59 -07:00
Abdulaziz Albahar 6cf7f5fc27 Fix legacy Tailscale route activation after Iroh upgrade
Preserve one pre-Iroh Tailscale fallback when the registry refresh is Iroh-only, keep registry-published routes authoritative, and serialize forced method-switch reconnects.
2026-08-13 23:51:34 -07:00
Abdulaziz Albahar 890b6b9531 refactor(push): isolate intent worker type 2026-08-13 23:51:33 -07:00
Abdulaziz Albahar 9b0f659a9e fix(push): bound cached registration recovery 2026-08-13 23:50:29 -07:00
Abdulaziz Albahar 61b1afca7a test(push): require fresh bounded recovery after timeout 2026-08-13 23:47:52 -07:00
Abdulaziz Albahar 601904189a test(push): wait for same-lane recovery state 2026-08-13 23:39:00 -07:00
Abdulaziz Albahar 01fdd8ef3a fix(push): bound same-lane recovery and startup cleanup 2026-08-13 23:33:37 -07:00
Abdulaziz Albahar 5528198cc1 test(push): cover startup drain and same-lane recovery 2026-08-13 23:30:29 -07:00
austinpower1258 b9aefd296f test: reject Claude fork parent fallback identity 2026-08-13 23:25:12 -07:00
austinpower1258 1c0e24f22c test: cover authoritative Claude fork identity 2026-08-13 23:21:38 -07:00
cmux reload-cloud ac370042d6 fix(pi): bound diagnostics and Feed timeouts 2026-08-13 23:18:39 -07:00
Abdulaziz Albahar 6832304b12 fix(push): fence late intents and timeout races 2026-08-13 23:18:38 -07:00
cmux reload-cloud 346e9c3399 test(pi): bound diagnostic and feed queues 2026-08-13 23:16:27 -07:00
austinpower1258 eae15a5a49 Merge remote-tracking branch 'origin/main' into issue-9629-codex-resume-binding-verification 2026-08-13 23:14:05 -07:00
austinpower1258 914373fef8 fix: close Codex resume review gaps 2026-08-13 23:13:45 -07:00
Abdulaziz Albahar 0cfa6daac2 test(push): cover late intent and timeout winner races 2026-08-13 23:11:01 -07:00
cmux reload-cloud 7a9cb46c64 fix(pi): reject unsafe diagnostic files 2026-08-13 23:06:55 -07:00
cmux reload-cloud 7da2028258 test(pi): reject symlinked diagnostic paths 2026-08-13 23:05:57 -07:00
austinpower1258 44bca39683 test: cover Codex review regressions 2026-08-13 23:05:44 -07:00
Austin Wang 96454b0fc1 Merge pull request #10140 from manaflow-ai/issue-10138-ssh-bootstrap-upload-timeout
Fix SSH daemon bootstrap on slow links and wedged masters
2026-08-13 23:03:23 -07:00
Abdulaziz Albahar 6e98cee5e1 fix(push): bound timeout recovery lanes 2026-08-13 23:02:12 -07:00
Abdulaziz Albahar 1f9c96da50 test(push): require bounded timeout recovery 2026-08-13 23:00:13 -07:00
Abdulaziz Albahar ea1d0ad48e Fix white tint for custom terminal icons (#10153)
* Fix white tint for custom terminal icons

* Avoid retaining terminal input view from icon tint

* Define active icon tint in accessory styling
2026-08-13 22:59:54 -07:00
austinpower1258 ef536b04af fix: avoid deprecated browser theme observer 2026-08-13 22:58:48 -07:00
cmux reload-cloud f12c7410e7 fix(pi): trust successful child exits 2026-08-13 22:50:56 -07:00
Abdulaziz Albahar faa340e657 fix(push): commit intents before bounded reconciliation 2026-08-13 22:50:16 -07:00
Abdulaziz Albahar ee553fb180 Keep iOS Simulator streams live in long sessions (#9886)
* Test simulator stream long-session recovery

* Keep simulator streams live under backpressure

* Test simulator replay recovery races

* Fix simulator recovery lifecycle races

* Fix simulator image task result inference

* Preserve simulator image preparation failures

* Test simulator selection survives pane unmount

* Keep simulator selection across pane remounts

* Test simulator stream navigation ownership

* Stop simulator streams when workspace route exits

* Scope simulator route visibility to shell

* Test simulator teardown resolves workspace row ID

* Resolve simulator teardown workspace identity

* Test simulator reader attachment retries

* Retry simulator reader attachment until ready

* Test simulator frame demand reconciliation

* Make simulator capture demand explicit

* Test framebuffer demand reconciliation

* Reannounce active simulator frame transport

* Test hidden simulator stream recovery

* Reconcile hidden simulator stream ownership
2026-08-13 22:49:39 -07:00
cmux reload-cloud 2ed3ed3123 test(pi): cover successful stdin pipe closure 2026-08-13 22:48:25 -07:00
cmux reload-cloud 2587929c69 test(pi): make FIFO diagnostics deterministic 2026-08-13 22:47:11 -07:00
Abdulaziz Albahar f3b831f81d test(push): require timed-out intent to reach service 2026-08-13 22:44:01 -07:00
austinpower1258 b8fa006c2c fix: require resolved sidebar appearance 2026-08-13 22:39:37 -07:00
austinpower1258 0a6a77feb6 Merge remote-tracking branch 'origin/main' into issue-9629-codex-resume-binding-verification 2026-08-13 22:38:27 -07:00
austinpower1258 a45252b833 fix: fail closed on unclassified Codex ownership 2026-08-13 22:37:10 -07:00
austinpower1258 22c6439147 test: pass exact Codex IDs to hook fixtures 2026-08-13 22:35:00 -07:00
Abdulaziz Albahar 4f0f2c7cb3 Revert "Revert "Fix iOS 27 keyboard re-open animation (#10006)" (#10151)" (#10152)
This reverts commit 897b4dfda3.
2026-08-13 22:30:20 -07:00
Abdulaziz Albahar 1130e64a88 refactor(push): keep intent lane state flat 2026-08-13 22:29:36 -07:00
cmux reload-cloud 02a928402a Merge remote-tracking branch 'origin/main' into issue-10128-pi-hook-timeout-telemetry 2026-08-13 22:27:20 -07:00
Abdulaziz Albahar 25ba6fd6a6 fix(push): retain bounded mutation lanes until completion 2026-08-13 22:27:09 -07:00
cmux reload-cloud 72e6ec57b0 fix(pi): avoid blocking on diagnostic pointers 2026-08-13 22:27:01 -07:00
cmux reload-cloud 6f634e5b8e test(pi): cover nonblocking diagnostic pointer reads 2026-08-13 22:24:30 -07:00
Abdulaziz Albahar c1458ba3d9 Refresh iOS artifact loaders after reconnect (#10121)
* test: cover artifact loader source replacement

* fix: refresh mobile artifact loaders after reconnect

* fix: isolate artifact caches across reconnects

* fix: guard canceled artifact thumbnail tasks
2026-08-13 22:24:02 -07:00
Austin Wang d22a611ab6 Merge pull request #10142 from manaflow-ai/issue-10129-tmux-two-client-cpu-spin
Mitigate redundant tmux PTY resize churn
2026-08-13 22:22:10 -07:00
austinpower1258 67e9cd25cd refactor: make resolved window chrome authoritative 2026-08-13 22:20:34 -07:00
Abdulaziz Albahar 897b4dfda3 Revert "Fix iOS 27 keyboard re-open animation (#10006)" (#10151)
This reverts commit e9fb69b4f3.
2026-08-13 22:19:16 -07:00
Abdulaziz Albahar 87c0033eb3 test(push): cover stalled intent and timeout worker ownership 2026-08-13 22:18:33 -07:00
austinpower1258 8517c1d5e0 test: cover Codex resume ownership gaps 2026-08-13 22:17:31 -07:00
austinpower1258 89338bf22d test: fix appearance matrix argument order 2026-08-13 22:07:56 -07:00
austinpower1258 1ebd0e7ab6 Merge remote-tracking branch 'origin/main' into issue-9629-codex-resume-binding-verification 2026-08-13 22:05:49 -07:00
austinpower1258 842d94b782 fix: close remaining Codex resume review gaps 2026-08-13 22:05:32 -07:00
Abdulaziz Albahar 75937e4297 fix(push): store boolean settings mutation task 2026-08-13 22:02:49 -07:00
Abdulaziz Albahar 8546b3dbd7 fix(push): reject unproven legacy cleanup credentials 2026-08-13 21:59:22 -07:00
Abdulaziz Albahar eb29f93ebb test(push): seed cleanup owner in queued sign-out 2026-08-13 21:58:22 -07:00
Abdulaziz Albahar c9721f902d Add workspace group selection to iOS task composer (#10123)
* Add task composer workspace group routing

* Move task composer group helpers to file scope

* Preserve restored task composer workspace group

* Gate task composer groups on authoritative inventory

* Require explicit task group recovery

* Fix task group capability scoping

* Fail closed on incomplete group owners

* Invalidate stale group authority
2026-08-13 21:56:57 -07:00
Abdulaziz Albahar a441e85c73 fix(push): require persisted owner for opt-out cleanup 2026-08-13 21:54:15 -07:00
austinpower1258 c21f052db1 fix: repair theme chrome compile errors 2026-08-13 21:53:30 -07:00
Abdulaziz Albahar 015f35393b test(push): fence opt-out cleanup to persisted owner 2026-08-13 21:52:18 -07:00
Abdulaziz Albahar 197fb04942 fix(push): fence direct registration commits and bound enables 2026-08-13 21:44:11 -07:00
austinpower1258 2557bb703b fix: resolve Dock startup chrome from theme authority 2026-08-13 21:40:25 -07:00
Abdulaziz Albahar 62a72a7f3c test(push): cover stale registration and public enable timeout 2026-08-13 21:37:27 -07:00
Abdulaziz Albahar f9a268144b Explain mobile connection methods in empty states
Merged after green required checks, tagged Mac rebuild, and focused iOS verification dispatch.
2026-08-13 21:35:17 -07:00
Abdulaziz Albahar 84bd0cc51f fix(push): persist sign-out cleanup before mutation gate 2026-08-13 21:24:48 -07:00
Abdulaziz Albahar f1582d5c35 test(push): preserve queued sign-out cleanup on cancellation 2026-08-13 21:21:56 -07:00
Abdulaziz Albahar e9fb69b4f3 Fix iOS 27 keyboard re-open animation (#10006)
* test(ios): cover stale keyboard completion race

* fix(ios): preserve keyboard rise animation

* fix(ios): harden keyboard transition ownership

* test(ios): cover delayed keyboard will race

* fix(ios): reject superseded keyboard will notifications

* fix(ios): bind keyboard transition legs to toggle intent

* ci(ios): build arm64 simulator artifacts

* fix(ios): fence scroll before verified replay reveal

* fix(ios): wait for in-flight scroll before replay reveal

* test(ios): require terminal edge to track keyboard dock

* fix(ios): pin terminal presentation to keyboard dock

* fix(ios): expose replay scroll fence across modules

* fix(ios): rebase dock before keyboard reversal

* fix(ios): rebase only interrupted keyboard motion

* fix(ios): bind toast settings toggle

* Fix iOS 27 keyboard toggle focus handoff
2026-08-13 21:20:26 -07:00
austinpower1258 e0336cc307 fix: wire sidebar model initializers and browser theme refresh 2026-08-13 21:18:42 -07:00
austinpower1258 27abb60c15 fix: resolve AppKit semantic colors under theme authority 2026-08-13 21:08:10 -07:00
Abdulaziz Albahar 7c6ef3c047 test(push): isolate mutation timeout generations 2026-08-13 21:05:23 -07:00
Abdulaziz Albahar 72d7ee68b4 fix(push): reconcile late authorization and cancel workers 2026-08-13 21:02:17 -07:00
Abdulaziz Albahar 81f03ee161 test(push): cover late prompt and worker cancellation 2026-08-13 21:02:08 -07:00
cmux reload-cloud 43a313271a fix(pi): claim stale sessions before logging 2026-08-13 21:01:52 -07:00
cmux reload-cloud 517e0965d1 test(pi): cover concurrent stale failure diagnostics 2026-08-13 21:00:48 -07:00
austinpower1258 8f6a9d4638 fix: unify dock chrome with resolved terminal theme 2026-08-13 20:57:04 -07:00
Abdulaziz Albahar 15f23ddb43 fix(push): permit retry after settings timeout 2026-08-13 20:38:25 -07:00
Abdulaziz Albahar fa2d641624 test(push): retry timed out enable intent 2026-08-13 20:37:04 -07:00
austinpower1258 83c1adc600 test: fix Codex resume regression compilation 2026-08-13 20:35:56 -07:00
Abdulaziz Albahar 86311258d8 chore(push): isolate timeout outcome type 2026-08-13 20:28:36 -07:00
Abdulaziz Albahar cb1914233c fix(push): bound settings mutation lifetime 2026-08-13 20:25:39 -07:00
Abdulaziz Albahar 59ded957b5 test(push): bound stalled settings mutation 2026-08-13 20:25:05 -07:00
Abdulaziz Albahar c2c06d7b2f test(push): fix cancellation regression setup 2026-08-13 20:14:14 -07:00
Abdulaziz Albahar a9d3eecc7a fix(push): recover cancelled registration attempts 2026-08-13 20:13:58 -07:00
Abdulaziz Albahar bf861abda8 test(push): recover cancelled queued registration 2026-08-13 20:12:17 -07:00
austinpower1258 93bd3af288 chore: keep Codex verifier within file budget 2026-08-13 20:06:59 -07:00
Abdulaziz Albahar be494245e2 fix(push): recheck cancellation at gate handoff 2026-08-13 20:05:37 -07:00
austinpower1258 36fd0e3e29 fix: close Codex resume binding review gaps 2026-08-13 20:03:15 -07:00
austinpower1258 01a6b04386 test: cover dock theme authority mismatch 2026-08-13 20:01:52 -07:00
Abdulaziz Albahar abe6ac92a5 fix(push): accept generation zero startup intent 2026-08-13 19:56:12 -07:00
Abdulaziz Albahar 18c558420d test(push): accept initial coordinator generation 2026-08-13 19:55:20 -07:00
Abdulaziz Albahar 886a28cdb2 fix(push): drop cancelled mutation waiters 2026-08-13 19:48:03 -07:00
Abdulaziz Albahar 845883cac1 test(push): cancel queued mutation waiter 2026-08-13 19:47:01 -07:00
cmux reload-cloud 9711c2bf7e fix(pi): await safe diagnostic appends 2026-08-13 19:46:00 -07:00
austinpower1258 7028786dfb fix: close sidebar git watcher review gaps 2026-08-13 19:43:34 -07:00
austinpower1258 76da0e05dc fix(ssh): make daemon bootstrap slow-link safe
Scale daemon uploads to the binary size, isolate bootstrap SSH traffic from stale ControlMasters, and clean remote writers and temporary files when transfers fail. Preserve process timeout details so workspace bootstrap errors remain actionable.
2026-08-13 19:42:54 -07:00
cmux reload-cloud fa452a4e2a fix(pi): make diagnostic writes nonblocking 2026-08-13 19:42:41 -07:00
cmux reload-cloud 3365e09ec1 test(pi): tolerate loaded hook harnesses 2026-08-13 19:42:21 -07:00
Abdulaziz Albahar e8a3df177b chore(push): isolate intent kind type 2026-08-13 19:40:53 -07:00
cmux reload-cloud 476eb388d8 test(pi): cover resilient diagnostic log writes 2026-08-13 19:34:59 -07:00
austinpower1258 beb8e01177 fix: make resize policy surface ownership explicit 2026-08-13 19:34:50 -07:00
Abdulaziz Albahar b93b7e06a4 fix(push): unify preference mutation ordering 2026-08-13 19:33:51 -07:00
austinpower1258 edb0008287 fix: address sidebar git watcher review 2026-08-13 19:33:13 -07:00
austinpower1258 9e3005ccff fix: coalesce redundant PTY grid resizes 2026-08-13 19:25:45 -07:00
austinpower1258 70f37e00c1 test: reproduce stable-grid tmux resize churn 2026-08-13 19:23:12 -07:00
Abdulaziz Albahar 6bdd586e0d fix(push): propagate denied enable generation 2026-08-13 19:16:59 -07:00
austinpower1258 a48abbd1f7 test(ssh): cover slow daemon upload recovery 2026-08-13 19:15:53 -07:00
Abdulaziz Albahar f5c966427e test(push): preserve denied reenable generation 2026-08-13 19:13:52 -07:00
Abdulaziz Albahar 9c94426350 fix(push): require explicit opt-out for startup cleanup 2026-08-13 19:07:45 -07:00
Abdulaziz Albahar 66b9763e37 test(push): preserve absent preference on startup 2026-08-13 19:06:07 -07:00
austinpower1258 f57a5fe5ce fix: preserve bounded large-repo git events 2026-08-13 19:05:31 -07:00
Abdulaziz Albahar 5f2c42b6d3 fix(push): let intent drains outlive caller cancellation 2026-08-13 18:58:49 -07:00
Abdulaziz Albahar 15270dcd88 test(push): preserve enable after caller cancellation 2026-08-13 18:56:39 -07:00
Abdulaziz Albahar cdffcba67e fix(push): submit every current enable generation 2026-08-13 18:48:07 -07:00
Abdulaziz Albahar d5818e9124 test(push): cover reenable generation during cleanup 2026-08-13 18:46:15 -07:00
cmux reload-cloud 3668dd2415 fix(pi): harden hook failure diagnostics 2026-08-13 18:38:00 -07:00
austinpower1258 4ae94b0ecf fix: bound sidebar git status watching 2026-08-13 18:36:54 -07:00
Abdulaziz Albahar 0e9a3fa33e fix(push): fence in-flight cleanup and stale snapshots 2026-08-13 18:33:20 -07:00
Abdulaziz Albahar 7c7f86f967 test(push): cover startup ownership and stale snapshots 2026-08-13 18:29:46 -07:00
cmux reload-cloud 917e18ee07 fix(pi): diagnose configurable hook timeouts 2026-08-13 18:21:32 -07:00
Abdulaziz Albahar 87da95bb3a refactor(push): align service package conventions 2026-08-13 18:20:48 -07:00
austinpower1258 478f8739e9 test: cover bounded sidebar git watching 2026-08-13 18:17:47 -07:00
Abdulaziz Albahar 9177de8354 fix(push): atomically commit coordinator intent 2026-08-13 18:11:59 -07:00
Abdulaziz Albahar 0b2802b5e4 test(push): persist opt-out before cleanup 2026-08-13 18:11:23 -07:00
cmux reload-cloud 6880530009 test(pi): cover hook timeout diagnostics 2026-08-13 18:10:01 -07:00
Abdulaziz Albahar ebcdf1ca61 fix(push): recover interrupted opt-out cleanup 2026-08-13 18:03:13 -07:00
Abdulaziz Albahar 88b75b085b test(push): recover disabled startup cleanup 2026-08-13 18:02:01 -07:00
austinpower1258 f95c3e317e Strengthen tree layout handle assertions 2026-08-13 17:58:52 -07:00
austinpower1258 245d272486 Request IDs for Dock tree regression 2026-08-13 17:54:19 -07:00
Abdulaziz Albahar d86f774bf5 fix(push): coalesce pending notification intents 2026-08-13 17:52:26 -07:00
austinpower1258 cc01478ea8 Make Dock regression cleanup authoritative 2026-08-13 17:51:28 -07:00
Austin Wang 1329f5a187 Merge pull request #10117 from sjiang647/claude-attach-passthrough
Include `claude attach` in `claude_builtin_command_name`
2026-08-13 17:41:40 -07:00
Abdulaziz Albahar 4379d55394 fix(push): coalesce repeated registration intents 2026-08-13 17:41:30 -07:00
Abdulaziz Albahar 9f86b86b51 test(push): deduplicate same-generation activation 2026-08-13 17:40:16 -07:00
austinpower1258 ba04a229e5 Cover dock and nested layout invariants 2026-08-13 17:36:00 -07:00
Abdulaziz Albahar 63007514af fix(push): make latest notification intent authoritative 2026-08-13 17:32:24 -07:00
Abdulaziz Albahar 8bde795686 test(push): cover latest intent ordering 2026-08-13 17:32:09 -07:00
austinpower1258 66a3f54d69 Fix tree layout handling for docks and missing refs 2026-08-13 17:21:12 -07:00
austinpower1258 52a05e561d Merge origin/main into feat-system-tree-layout-field 2026-08-13 17:12:57 -07:00
Abdulaziz Albahar 90a97c1921 fix(push): serialize notification intent mutations 2026-08-13 17:05:23 -07:00
Abdulaziz Albahar 4a5301de45 test(push): cover ordered opt-out intents 2026-08-13 17:05:18 -07:00
Abdulaziz Albahar 09868c8c42 test(push): require serialized opt-out mutation 2026-08-13 16:46:53 -07:00
Abdulaziz Albahar 5bc3c10d76 fix(ios): preempt stale push registration work 2026-08-13 16:36:49 -07:00
Abdulaziz Albahar e679e16b52 fix(ios): own push preference mutation in coordinator 2026-08-13 16:12:57 -07:00
chatmux-connections[bot]andchatmux-connections[bot] <chatmux-connections[bot]@users.noreply.github.com> fb2a058a5c fix(coderouter): use npx coderouter@latest add <tool> and show command text (#10108)
Follow-up to #10107: the Add account rows should copy-paste the per-tool
install commands (npx coderouter@latest add codex / add opencode) instead of
the cr routing commands, and render the command text visibly in the row.

Co-authored-by: chatmux-connections[bot] <chatmux-connections[bot]@users.noreply.github.com>
2026-08-13 15:51:02 -07:00
Abdulaziz Albahar 57bef7d12b fix(ios): serialize push retries after reconciliation 2026-08-13 15:49:15 -07:00
Abdulaziz Albahar 4492cf1eb3 fix(ios): serialize timed out push mutations 2026-08-13 15:37:38 -07:00
Abdulaziz Albahar 5f4c064122 fix(ios): bound push readiness reconciliation 2026-08-13 15:29:03 -07:00
Abdulaziz Albahar 7f4f7d3526 Merge remote-tracking branch 'origin/main' into fix-ios-push-toggle-off
# Conflicts:
#	Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
2026-08-13 15:22:58 -07:00
Abdulaziz Albahar 6b698958e7 fix(ios): surface uncertain push toggle outcomes 2026-08-13 15:22:01 -07:00
Abdulaziz Albahar a162e51119 refactor(ios): isolate push preview mutation gate 2026-08-13 15:09:33 -07:00
Abdulaziz Albahar 653a43d98a fix(ios): bound push toggle mutations 2026-08-13 15:04:00 -07:00
Abdulaziz Albahar 12cf0c540b fix(ios): tie push toggle mutation to view lifecycle 2026-08-13 14:55:45 -07:00
Austin Wang 57476721af Merge pull request #10101 from manaflow-ai/issue-10060-mosh-bootstrap-misreport
Fix mosh bootstrap staging: real argv for mosh-server, stage-specific errors, proxy address fallback
2026-08-13 14:42:51 -07:00
austinpower1258andClaude Fable 5 84f676a422 chore: address PR review feedback
- Drop MoshRemoteIPMode's unused cliValue initializer and Codable
  conformance: no CLI flag or persisted setting selects an IP mode, so
  the speculative API only implied a configuration surface that does
  not exist. Production always starts from .remote with the automatic
  proxy fallback.
- Make the fish login-shell tests report an explicit skip via
  .enabled(if:) instead of silently passing when fish is absent.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-13 14:41:14 -07:00
Abdulaziz Albahar 63ed21a31a fix(ios): initialize task composer setting 2026-08-13 13:25:11 -07:00
Abdulaziz Albahar 207491f365 fix(ios): remove retired toast settings toggle 2026-08-13 13:11:05 -07:00
Abdulaziz Albahar 952586ecc5 fix(ios): restore toast settings binding 2026-08-13 13:08:01 -07:00
Abdulaziz Albahar 4e4df0382f Merge pull request #10119 from manaflow-ai/feat-iroh-product-final
Complete Iroh diagnostics and connection recovery
2026-08-13 12:17:43 -07:00
Abdulaziz Albahar 99d1ea7502 merge: add bounded Iroh dial diagnostics 2026-08-13 12:12:30 -07:00
Abdulaziz Albahar 57ec73a950 fix(ios): keep live Iroh sessions healthy during probe timeouts 2026-08-13 12:10:58 -07:00
Abdulaziz Albahar b4ca1a9e82 fix(ios): complete diagnostic event presentation 2026-08-13 12:01:05 -07:00
Abdulaziz Albahar a0d3af8fc0 merge: port actionable Iroh recovery diagnostics onto main 2026-08-13 11:59:16 -07:00
Abdulaziz Albahar 2f5ba2d7d7 Merge pull request #9775 from manaflow-ai/task-ios-admission-revalidation
Preserve admitted Iroh sessions across auth refresh blips
2026-08-13 11:56:32 -07:00
Abdulaziz Albahar 7e00b41dfb Merge pull request #10113 from manaflow-ai/incident-relay-token
fix(relay): contain Stack Auth throttles before relay minting
2026-08-13 11:53:12 -07:00
Abdulaziz Albahar 26c94df813 Merge pull request #10094 from manaflow-ai/fix-iroh-compat-warning
Fix false compatibility warning for Iroh pairing
2026-08-13 11:53:09 -07:00
Abdulaziz Albahar 815ca6d626 Merge pull request #10004 from manaflow-ai/verify-iroh-aug11
Fix Iroh release settings and Tailscale skew gate
2026-08-13 11:48:33 -07:00
Abdulaziz Albahar be076d30dd fix(iroh): rotate production gate Mac credentials 2026-08-13 11:24:32 -07:00
Abdulaziz Albahar 856cb762a0 test(iroh): rotate production Mac credentials 2026-08-13 11:24:10 -07:00
Lawrence Chen c89290f808 Add CodeRouter CLI compatibility metadata (#10116)
* Add CodeRouter CLI compatibility metadata

* Cover legacy CLI config fields
2026-08-13 10:52:00 -07:00
Abdulaziz Albahar dcdf865948 fix(iroh): keep release gate simulator-only 2026-08-13 10:42:23 -07:00
Abdulaziz Albahar 3dd9bac553 test(iroh): isolate release gate from default iPhone 2026-08-13 10:42:04 -07:00
Sjiang647andClaude Fable 5 815724872a Pass the hidden claude attach subcommand through without hook injection
Add `attach` to claude_builtin_command_name so the wrapper classifies
`claude attach <id>` as a command invocation and execs the real CLI with argv
untouched, like agents/mcp/etc. Hook settings are meaningless for attach
anyway: hooks live in the already-running background writer process, and the
injected fresh --session-id is exactly what turned an attach into a new
session.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-13 10:36:23 -07:00
Sjiang647andClaude Fable 5 0ed5925de1 Add failing regression test: hidden claude attach subcommand gets hook injection
`claude attach <id>` (the attach door for --bg background sessions) is a real
subcommand but hidden from `claude --help`, so it's missing from the wrapper's
builtin-command list. The wrapper classifies it as a session entrypoint and
injects --session-id/--settings ahead of it, which makes the real CLI treat
"attach" as the [prompt] positional: instead of attaching, it mints a brand-new
session with "attach" pre-filled in the composer.

Test-only commit (two-commit regression policy): CI should go red here.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-13 10:36:23 -07:00
Lawrence Chen 3ee68badea Add cmux CodeRouter CLI passthrough aliases (#10109)
* Add cmux CodeRouter CLI passthrough aliases

* Harden CodeRouter CLI dispatch

* Preserve CodeRouter launch diagnostics

* Use bundled CLI localization for aliases

* Verify localized CodeRouter alias output
2026-08-13 09:57:04 -07:00
Abdulaziz Albahar 4b4d479b14 fix(ios): preserve ticket-authenticated attach startup 2026-08-13 09:45:42 -07:00
Abdulaziz Albahar d2d527a54e test(ios): make push toggle timing assertion deterministic 2026-08-13 09:44:13 -07:00
Abdulaziz Albahar 46edb3c92c fix(ios): give startup one scoped connection owner 2026-08-13 09:39:47 -07:00
Abdulaziz Albahar af722e3427 test(ios): keep account scope from owning startup dial 2026-08-13 09:38:00 -07:00
Abdulaziz Albahar 12bbdf210b fix(relay): bound auth error classification 2026-08-13 09:23:01 -07:00
Abdulaziz Albahar 814d3e165f fix(relay): contain auth throttles and gate ingress 2026-08-13 09:08:53 -07:00
Abdulaziz Albahar e7c9a6a7b4 fix(ios): update push toggle optimistically 2026-08-13 09:08:27 -07:00
Abdulaziz Albahar b4625c423e test(ios): cover pending push opt-out 2026-08-13 09:08:19 -07:00
Abdulaziz Albahar f8f24726c8 fix(ios): wait for account scope before reconnect 2026-08-13 09:06:38 -07:00
Abdulaziz Albahar 534acb2ff9 test(ios): gate reconnect on complete auth bootstrap 2026-08-13 09:06:00 -07:00
Abdulaziz Albahar d1ab6098a8 test(relay): cover Stack Auth throttle response 2026-08-13 09:03:15 -07:00
Abdulaziz Albahar 55e89ecffd fix(ios): retain startup attach across root rebuilds 2026-08-13 08:24:32 -07:00
Abdulaziz Albahar c942dc15e6 test(ios): keep startup attach across root rebuilds 2026-08-13 08:23:31 -07:00
Abdulaziz Albahar ef219f430c fix(ios): allow relay-scale pairing deadline 2026-08-13 07:52:15 -07:00
Abdulaziz Albahar 2001978fc1 test(ios): cover relay-scale pairing deadline 2026-08-13 07:51:31 -07:00
Abdulaziz Albahar 10bb1d84bf fix(cli): restore macOS 14 compiler support 2026-08-13 07:26:11 -07:00
Abdulaziz Albahar 2ca5588c4f fix(iroh): keep control session authoritative 2026-08-13 06:40:08 -07:00
Abdulaziz Albahar 2eb729055a fix(iroh): tolerate transient path transitions 2026-08-13 06:05:13 -07:00
Abdulaziz Albahar ee2f58670f test(iroh): preserve sessions across transient path loss 2026-08-13 06:04:09 -07:00
Abdulaziz Albahar 37854b926d ci(ios): recognize Swift Testing success 2026-08-13 06:00:09 -07:00
Abdulaziz Albahar a092a26701 ci(ios): keep focused dispatches focused 2026-08-13 05:56:07 -07:00
Abdulaziz Albahar dbebb16053 ci(iroh): run compatibility and transport gates concurrently 2026-08-13 05:52:30 -07:00
Abdulaziz Albahar 81476dcb56 ci(iroh): preserve host gate diagnostics 2026-08-13 05:35:52 -07:00
Abdulaziz Albahar c1d21c83c0 test(ios): construct diagnostic event explicitly 2026-08-13 05:35:51 -07:00
Abdulaziz Albahar 44793dabb2 test(ios): import NSNumber support 2026-08-13 05:21:42 -07:00
Abdulaziz Albahar 37a31806d1 test(ios): use scoped viewport generation API 2026-08-13 05:11:46 -07:00
Abdulaziz Albahar 2355a0164c refactor(iroh): scope relay origins to URL sequences 2026-08-13 05:08:48 -07:00
Abdulaziz Albahar 0ef91393a7 ci(iroh): track expanded compatibility suite 2026-08-13 04:54:47 -07:00
Abdulaziz Albahar 39fc67ab6d test(iroh): report release-gate failure stage 2026-08-13 04:51:34 -07:00
Abdulaziz Albahar f971bd0573 ci(iroh): serialize shared-account transport gates 2026-08-13 04:40:13 -07:00
Abdulaziz Albahar 3d2b359a64 test(iroh): scope framed RPC admission fixture 2026-08-13 04:26:42 -07:00
Abdulaziz Albahar 06727d662e test(iroh): classify RPC inventory failures 2026-08-13 04:10:07 -07:00
chatmux-connections[bot]andchatmux-connections[bot] <chatmux-connections[bot]@users.noreply.github.com> d0b0fb22a6 feat(dashboard): iOS sidebar section + coderouter add commands (#10107)
- Move iOS TestFlight into its own sidebar section, placed below coderouter
- Move coderouter Add account above Connected accounts
- Show copy-pastable commands for Codex (cr codex) and OpenCode (cr opencode)
  with their logos instead of the generic npx/cmux add commands

Co-authored-by: chatmux-connections[bot] <chatmux-connections[bot]@users.noreply.github.com>
2026-08-13 03:26:23 -07:00
Abdulaziz Albahar dc0b0d8b54 Initialize mobile task composer setting 2026-08-13 03:15:17 -07:00
Abdulaziz Albahar 992ab0698e Remove stale mobile toasts setting 2026-08-13 03:12:46 -07:00
Abdulaziz Albahar ffb91863c9 Fix toast binding in mobile settings 2026-08-13 03:09:46 -07:00
Abdulaziz Albahar b1ec086f1a ci(iroh): cap release gate build parallelism 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 32905873b1 ci(iroh): reap cancelled hosted builds 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 6601f297c6 ci(iroh): bound hosted build logs 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 489108796d ci(iroh): reject stale gate verdicts 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 1760e5211c fix(ios): import release-gate RPC owner 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar f5f584d4d6 fix(iroh): keep transport overrides in debug UI 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 737fa823cd fix(ios): scope Iroh gate RPC inventory 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar ec9696da01 fix(iroh): preserve release-gate readiness phase 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 657b6ff0ba test(iroh): classify readiness deadline failures 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 08d2567f57 fix(iroh): let release gate own readiness 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 77e890e0f8 fix(ios): await auth bootstrap before startup attach 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar c084c0c2cd ci(iroh): keep optimized gate builds observable 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 10773bc6d0 ci(iroh): allow cold session readiness 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 3194b50384 fix(iroh): normalize retired relay-only preference 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar ba34381558 test(iroh): verify full mobile RPC inventory 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 75c935e81c ci(iroh): stabilize optimized gate builds 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 858dd89026 fix(iroh): retire release relay-only state 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 3e322d3661 test(iroh): reject retired release relay-only state 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar a2943d692d fix(iroh): keep relay-only in debug builds 2026-08-13 02:40:18 -07:00
Abdulaziz Albahar 8e03abb930 Restore Xcode 16 simulator package compatibility (#10009)
* fix(simulator): require sendable mutation results

* fix(simulator): use macOS 14 spawn chdir API

* fix(simulator): support Xcode 16 button labels

* ci(macos): use available Sonoma Intel runner

* ci(macos): split Sonoma and Intel compatibility

* fix(compat): support Xcode 16 localized controls

* fix(compat): localize empty simulator state on Xcode 16

* test(compat): avoid nested actor compiler crash

* fix(compat): support localized controls on Xcode 16

* fix(compat): localize inspector highlight control

* fix(compat): support Swift 6.0 declarations

* Fix Xcode 16 timer destruction isolation

* Allow Intel Simulator cold boots to finish

* Fix Xcode 16 cancellation timer isolation

* Restore Xcode 16 type isolation syntax

* ci(macos): allow compatibility matrix to finish

* ci: run focused Iroh checks on every compatibility host

* ci: serialize Intel Iroh simulator tests

* ci: serialize Iroh simulator transport tests

* test: await diagnostic tap delivery

* fix(diagnostics): preserve relay-free bootstrap semantics

* fix(compat): support diagnostic correlation on Swift 6.0

* fix(compat): support app sources on Swift 6.0

* test(iroh): await admission before capacity assertion
2026-08-13 02:40:06 -07:00
chatmux-connections[bot]andpi 0d76b35154 web(dashboard): add iOS app card and drop the Stack team picker (#10099)
- Dashboard home now shows an iOS app card (next to CodeRouter) that
  links to the TestFlight page.
- Remove the Stack <TeamSwitcher> organization picker from the
  bottom-left account area so only the cmux account menu remains.
- Add dashboard.home iOS strings to all locales and update the account
  menu tests for the retained menu surface.

Co-authored-by: pi (manaflow-ai) <[email protected]>
2026-08-13 02:04:54 -07:00
austinpower1258andClaude Fable 5 b7f128b015 fix: pass staged bootstrap to mosh-server as real argv and prefer proxy fallback
mosh-server executes the received command with execvp and no shell, so
the staged launcher must be ['/bin/sh', '-c', script] instead of one
'/bin/sh -c ...' string that execvp treats as a literal pathname. The
OpenSSH string form is unchanged.

When SSH_CONNECTION is unusable, fall back to Mosh's SSH-proxy address
resolution instead of local mode: local mode resolves the destination
via DNS and breaks SSH-config-only aliases such as port-forwarded Coder
workspaces. Validate only the SSH_CONNECTION shape Mosh actually parses
(four fields, numeric ports, usable server address, now including
loopback rejection), not the unused peer address.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-13 01:20:17 -07:00
austinpower1258andClaude Fable 5 d4b95fe202 test: prove staged bootstrap must survive mosh-server execvp
mosh-server executes the remote command argv with execvp and no shell,
so a single '/bin/sh -c ...' string is treated as a literal pathname.
Also pin the address fallback to Mosh proxy resolution and validate
only the SSH_CONNECTION fields Mosh actually parses.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-13 01:16:38 -07:00
Austin Wang db743dc329 Merge pull request #10059 from manaflow-ai/issue-10054-ambient-cli-socket-fallback
Recover ambient CLI from stale dev socket
2026-08-13 01:07:37 -07:00
austinpower1258 4f10c3b525 fix: verify Codex resume binding ownership 2026-08-13 00:56:09 -07:00
austinpower1258 802ede81f2 test: cover Codex resume binding provenance 2026-08-13 00:53:31 -07:00
austinpower1258 06f231373a fix: keep Stack handler request-bound 2026-08-13 00:51:49 -07:00
Abdulaziz Albahar f8fc57e316 Initialize mobile task composer preference 2026-08-12 23:57:59 -07:00
Abdulaziz Albahar 814bd0e06f Keep disabled toast policy buildable on iOS 2026-08-12 23:54:13 -07:00
Abdulaziz Albahar 1ee4f0925f fix(ios): normalize route analyzer evidence 2026-08-12 23:44:48 -07:00
Abdulaziz Albahar 7a0123e786 test(ios): cover analyzer route variants 2026-08-12 23:44:31 -07:00
Abdulaziz Albahar 977c91303e Preserve canonical bootstrap diagnostic names 2026-08-12 23:40:51 -07:00
Abdulaziz Albahar 0363a6303b Fix close attribution diagnostic assertion 2026-08-12 23:36:17 -07:00
austinpower1258 9cc2485771 fix: restore web typecheck after base sync 2026-08-12 23:32:16 -07:00
austinpower1258 64a45bdf8d Merge remote-tracking branch 'origin/main' into issue-10054-ambient-cli-socket-fallback 2026-08-12 23:25:31 -07:00
Abdulaziz Albahar 5c7457deb2 Merge remote-tracking branch 'origin/main' into task-ios-connection-reliability
# Conflicts:
#	Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/DiagnosticEventCode.swift
#	Packages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/DiagnosticEventPresentationTests.swift
#	Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohClientSession.swift
#	Packages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohRegistryContextProvider.swift
2026-08-12 23:22:39 -07:00
Abdulaziz Albahar f7cb0fe057 fix(ios): bound reconnect dials and expose route failures 2026-08-12 23:13:31 -07:00
Abdulaziz Albahar 0d796790e1 test(ios): cover reconnect route diagnostics 2026-08-12 23:13:18 -07:00
austinpower1258 105f4f2b53 Merge remote-tracking branch 'origin/main' into issue-10054-ambient-cli-socket-fallback
# Conflicts:
#	CLI/cmux.swift
#	cmuxTests/TerminalAndGhosttyTests.swift
2026-08-12 23:11:39 -07:00
Abdulaziz Albahar db58d79ca6 Promote iOS terminal Files chip (#10083)
* Promote iOS terminal Files chip

* Fix terminal Files flag access

* Keep the canonical iOS artifact chip kill switch

* Own mobile feature flag refresh lifecycle
2026-08-12 23:03:47 -07:00
Lawrence Chen 22846c0b0d Make CodeRouter account setup CLI-first (#10095)
* Render CLI auth confirmation dynamically

* Use CLI-first CodeRouter account setup
2026-08-12 23:00:10 -07:00
Austin Wang e9887cf373 Merge pull request #10089 from manaflow-ai/issue-10085-sidebar-leading-edge-clip
Fix sidebar leading-edge clipping after resize
2026-08-12 22:57:56 -07:00
Austin Wang 01b142add4 Merge pull request #10058 from manaflow-ai/issue-10052-restore-socket-rebind
Fix stale control-socket rebind and restore startup race
2026-08-12 22:57:42 -07:00
austinpower1258 a70386fd88 fix: quote staged bootstrap launcher for login shells 2026-08-12 22:50:38 -07:00
austinpower1258 d688b760c9 fix: force POSIX shell for staged restore launch 2026-08-12 22:48:54 -07:00
austinpower1258 31c445e98f refactor: simplify socket connect errors 2026-08-12 22:44:20 -07:00
Lawrence Chen bec630020e Render CLI auth confirmation dynamically (#10093) 2026-08-12 22:41:48 -07:00
Austin Wang 10bf707504 Merge pull request #10074 from manaflow-ai/issue-10051-intel-wake-sigbus
Fix native sidebar row ownership during layout
2026-08-12 22:40:05 -07:00
Abdulaziz Albahar 9e3173e90d Release mobile terminal viewport when iOS deactivates
Release the Mac terminal viewport lease when the iOS scene deactivates, clear stale local viewport metadata, and restore fresh geometry on reactivation.
2026-08-12 22:36:56 -07:00
austinpower1258 59357fce29 test: remove real-time attachment staging waits 2026-08-12 22:32:33 -07:00
austinpower1258 76a0d5a6e6 fix: retry transient relay startup connects 2026-08-12 22:19:06 -07:00
Abdulaziz Albahar 98ea04d735 iOS: permanently disable mobile toasts (#10087)
* test(ios): keep legacy toast preference disabled

* fix(ios): permanently disable mobile toasts

* docs(ios): document toast presenter policy
2026-08-12 22:18:54 -07:00
austinpower1258 f97c3c3282 test: cover delayed relay restore startup 2026-08-12 22:14:23 -07:00
Abdulaziz Albahar 4c05363d69 Add relay-free bootstrap diagnostics and Never Use Relays
Merged with explicit user authorization.
2026-08-12 22:12:49 -07:00
Abdulaziz Albahar 5a2f8b88c2 Fix mobile task model discovery
Discover mobile task models from provider-owned local catalogs and gate Task Composer through the remote feature flag.
2026-08-12 22:09:52 -07:00
Abdulaziz Albahar 3b3e417149 CLAUDE.md: run verification on fleet by default
Document fleet-first verification defaults and local fallback guidance.
2026-08-12 22:03:53 -07:00
Abdulaziz Albahar d907a2d087 Fix dev auth precedence and env scrubbing (#10091) 2026-08-12 22:01:45 -07:00
austinpower1258 dd3c86a4dc Fix native sidebar row ownership during layout 2026-08-12 21:52:38 -07:00
austinpower1258 f7807bb655 test: cover native sidebar rows across resize 2026-08-12 21:52:38 -07:00
Abdulaziz Albahar 08246ae369 fix(ios): skip compatibility warning for endpoint-only Iroh QR 2026-08-12 21:47:13 -07:00
austinpower1258 b840281212 fix: bound socket wakeups and reject stale restore metadata 2026-08-12 21:46:40 -07:00
Abdulaziz Albahar 22cf4112fc test(ios): preserve unspecified Iroh QR compatibility 2026-08-12 21:46:23 -07:00
austinywang a581b1b9c8 test: wait for current sidebar geometry 2026-08-12 21:44:00 -07:00
Abdulaziz Albahar c25b64cadd Merge remote-tracking branch 'origin/main' into task-ios-connection-reliability 2026-08-12 21:41:58 -07:00
Lawrence Chen 09efbcd1b8 Add comprehensive privacy-safe CodeRouter analytics (#10088)
* Add privacy-safe CodeRouter analytics coverage

* Document fixed analytics endpoint contract

* Harden analytics ingestion and coverage
2026-08-12 21:35:42 -07:00
austinywang b0459f73a3 chore: normalize test project wiring 2026-08-12 21:34:10 -07:00
Abdulaziz Albahar c231a72d49 test(ios): fix throwing transport diagnostic double 2026-08-12 21:31:34 -07:00
austinywang 4bbc803c33 fix: keep sidebar overflow anchored to the leading edge 2026-08-12 21:29:07 -07:00
austinywang 30c6803f1d test: cover sidebar leading-edge anchor during resize 2026-08-12 21:27:40 -07:00
Abdulaziz Albahar 015459b42e Merge remote-tracking branch 'origin/main' into task-ios-connection-reliability 2026-08-12 21:20:08 -07:00
Abdulaziz Albahar 8d4810326c Retain iOS app and network log history (#10082)
* test(ios): cover retained log generations

* fix(ios): retain app and network log history

* test(ios): order retained logs by generation stamp

* fix(ios): harden retained log discovery
2026-08-12 21:15:00 -07:00
Abdulaziz Albahar e85329c990 fix(ios): complete connection diagnostics integration 2026-08-12 21:12:35 -07:00
austinpower1258 b980f3f207 fix: clear pre-existing sidebar Swift warning 2026-08-12 21:06:36 -07:00
austinpower1258 3586c7dedb fix: narrow restore handoff and stabilize focus setup 2026-08-12 20:55:32 -07:00
Abdulaziz Albahar 99f0e20080 fix(ios): refresh stale Iroh discovery before redial 2026-08-12 20:50:45 -07:00
Abdulaziz Albahar 01b070d7ac feat(ios): make transport recovery traces actionable 2026-08-12 20:50:35 -07:00
Abdulaziz Albahar eeff5ceb90 Recover unverified email-code sign-in on iOS (#10029)
* test(ios): expose password-account sign-in gap

* fix(ios): sign in existing password accounts

* test(ios): isolate sign-in fixture from onboarding

* fix(ios): tighten password sign-in metadata

* test: reject password-only email recovery

* fix: recover unverified email sign-in

* fix: keep IPv6 verification callbacks local

* fix: bound stack email verification rendering

* fix: harden email verification recovery
2026-08-12 20:38:07 -07:00
Abdulaziz Albahar 95eb30963f feat(ios): add privacy-safe Iroh bootstrap diagnostics 2026-08-12 20:25:56 -07:00
Abdulaziz Albahar d55eed16ac test(ios): cover explicit startup recovery replacements 2026-08-12 20:24:39 -07:00
Abdulaziz Albahar 812f7592f5 fix(ios): coalesce recovery during startup dial 2026-08-12 20:24:39 -07:00
Abdulaziz Albahar 0f8505b5cf test(ios): cover presence during startup reconnect 2026-08-12 20:24:38 -07:00
austinpower1258 aae3632aa0 test: cover address probe edge cases 2026-08-12 20:18:24 -07:00
austinpower1258 cf4ffee5fc fix: preserve newer restore binding data 2026-08-12 19:57:07 -07:00
Abdulaziz Albahar 9a63ba837b fix(ios): import shell status in release UI (#10067) 2026-08-12 19:55:32 -07:00
austinpower1258 d09bfdfdc7 test: wire socket resolver into unit target 2026-08-12 19:42:49 -07:00
Abdulaziz Albahar ccbf7e5853 fix(ios): keep release diagnostics symbols available (#10065) 2026-08-12 19:30:22 -07:00
austinpower1258 710c6246f0 fix: align reload markers and repair vault test compile 2026-08-12 19:21:44 -07:00
Abdulaziz Albahar b71e48880b Fix iOS artifact failure messaging (#9961)
* test: reject false artifact reachability errors

* fix: preserve artifact failure meaning

* docs: clarify artifact load failure contract

* test: reveal missing chat attachment before tapping

* fix: preserve attachment button semantics

* fix: keep native attachment button trait

* test: keep rich fixture lookup role agnostic

* test: tap attachment by visible identity

* fix: preserve loader across artifact navigation

* test: expose lossy artifact failure handling

* fix: preserve artifact fetch failure reasons

* fix: scope artifact storage classification

* fix: return terminal artifact failure view

* test: harden artifact failure UI coverage

* feat: add mobile artifact failure scenario picker

* docs: clarify artifact load failure
2026-08-12 19:15:31 -07:00
Abdulaziz AlbaharandLawrence Chen 83beac0f39 Restore canonical iOS task composer pills (#9892)
* Add failing canonical task composer UI regression

* Ship canonical iOS task composer controls

* Fix task options duplicate assertions

* Restore task options model import

* Centralize task composer prompt lookup

* Add failing accessibility pill row regression

* Keep task composer edge controls visible

* Make composer navigation assertion title agnostic

* test(ios): harden task composer UI coverage

* test(ios): wait for composer submission state

* test(ios): cover populated composer edge controls

* test(ios): enable connected task composer coverage

* Test task composer attachment capability handshake

* Test task composer size after pairing

* ci: build arm64 iOS reload simulator app

* Persist paired Mac state across UI-test relaunch

* Reuse manual-pair fixture across UI-test relaunch

* Key relaunch fixture by full loopback port

* Test iPad composer keyboard attachment

* Attach iPad task composer to keyboard

* Isolate manual pairing fixtures across runs

* Pin iPad composer to keyboard guide

* Open manual pairing in composer fixture

* Keep task composer mounted across size changes

* Make composer edge fixture deterministic

* Log task composer dock geometry in debug builds

* Make task composer geometry logging passive

* Measure task composer dock through accessibility

* Expose composer bar as an accessibility container

* Expose task composer dock geometry to UI tests

* Test iPad composer against keyboard assistant strip

* Test dynamic task model catalogs

* Discover task models dynamically

* Export model catalog transport types

* Verify model catalog source priority

* Make backend catalog parsing explicit

* Render refreshed task models immediately

* Restore task model CPU icon

* Add rejected model catalog regressions

* Validate backend model catalogs before serving

* Use connected composer fixture for model discovery test

* Enable attach tickets in model discovery fixture

* Stabilize host model discovery fixture

* Wait for visible host model in UI test

* Wait for discovered model menu state

* Preserve restored model draft identity

* test(ios): cover staged attachment previews

* feat(ios): preview staged task attachments

* fix(ios): refresh models when host capability arrives

* test(ios): trace task model host request

* test(ios): probe host models with stale capability hint

* fix(ios): probe host model catalog directly

* test(ios): trace model discovery routing

* test(ios): cover slow host model discovery

* fix(ios): stabilize dynamic model discovery

* test(ios): keep model refresh owner across Mac switch

* fix(ios): preserve model refresh across Mac switch

* fix(ios): retry model discovery after connection settles

* fix(ios): decouple model probes from connection retries

* debug(ios): trace task model routing

* test(ios): cover read-only model connection routing

* fix(ios): route model reads through live connections

* fix(ios): retry model discovery when connection appears

* debug(ios): expose task model refresh state

* test(ios): keep mock host event probe alive

* fix(ios): preserve presented model selections

* chore(ios): remove task model diagnostics

* test(ios): scope mock event subscriptions by client

* test(ios): preserve connected launch environment

* test(mac): reject hidden Codex cache models

* fix(mac): honor Codex model visibility

* test(ios): wait for backend model submit readiness

* test(ios): settle notification prompt before composer

* Fix task composer UI test permission setup

* Settle notification prompt before workspace checks

* Expand attachment removal hit targets

* Tolerate subpixel attachment hit frames

* test(ios): close task composer evidence gaps

* test(ios): await model request before unwrap

* test(ios): make composer evidence deterministic

* fix(ios): retain selected model across refresh

* fix(macOS): tolerate nil AppKit subview hooks

* test(iOS): verify model snapshot after menu dismissal

* test(iOS): await model pill repaint

* test(iOS): assert visible snapshot selection

* test(iOS): reacquire presented model item

* test(iOS): gate one presented model menu

* fix(iOS): snapshot model menu presentation

* test(iOS): require compact leading task controls

* fix(iOS): tighten task utility spacing

* test(iOS): require clearer attachment previews

* fix(iOS): clarify task attachment previews

* test(iOS): cover OpenCode cold discovery and seed deletion

* fix(iOS): load OpenCode models and protect task seeds

* test(iOS): exercise Shell template protection

* fix(iOS): neutralize task template row tint

* test(iOS): reproduce deleted built-in migration

* fix(iOS): reconcile built-in task template ownership

* chore: remove unrelated portal hook change

* Fix model selection diagnostic correlation after main merge

* Fix display settings initialization after main merge

* Fix tagged mobile discovery peer matching

* test(ios): reject model pill submit overlap

* fix(ios): compress composer pills within viewport

---------

Co-authored-by: Lawrence Chen <[email protected]>
2026-08-12 19:00:00 -07:00
Abdulaziz Albahar 1de40b8870 fix(ios): preserve Tailscale migration setup flow 2026-08-12 18:48:56 -07:00
austinpower1258 c0afdaaa21 fix: bound reload probes and preserve offline CLI commands 2026-08-12 18:48:28 -07:00
austinpower1258 aea1eddb67 test: cover reload liveness and offline themes 2026-08-12 18:42:19 -07:00
austinpower1258 2e61187f41 fix: make mosh bootstrap shell-safe and report stages 2026-08-12 18:21:00 -07:00
Abdulaziz Albahar 3166828491 fix(ios): make Tailscale setup banner dismissible
* test(ios): cover dismissing Tailscale setup banner

* fix(ios): make Tailscale setup banner dismissible

* fix(ios): order disconnected shell inputs

* refactor(ios): share Tailscale banner dismissal

* fix(ios): keep Tailscale recovery action available

* fix(ios): keep tailscale setup banner available after migration

* fix(ios): defer tailscale pairing until authorization resolves
2026-08-12 18:10:30 -07:00
austinpower1258 dc7916c507 fix: serialize ambient CLI pointer ownership 2026-08-12 18:07:45 -07:00
austinpower1258 1f145ce9ed fix: keep socket fallback instance-safe 2026-08-12 18:01:49 -07:00
austinpower1258 40885d8fb0 test: cover mosh bootstrap stage and address fallback 2026-08-12 17:51:53 -07:00
austinpower1258 2292391d61 fix: expose socket cleanup callback across source files 2026-08-12 17:47:52 -07:00
austinpower1258 b4b7a761cc fix: harden socket discovery ownership and reload fallback 2026-08-12 17:35:19 -07:00
austinpower1258 853aceeb79 fix: preserve socket retries without kqueue 2026-08-12 17:14:51 -07:00
austinpower1258 9b6008e533 refactor: centralize control socket startup waiting 2026-08-12 17:03:07 -07:00
austinpower1258 c0cbe56f27 fix: keep socket cleanup warning-free 2026-08-12 16:49:25 -07:00
austinpower1258 7cae83007c fix: recover ambient CLI from stale dev socket 2026-08-12 16:34:02 -07:00
austinpower1258 13ce7a24f3 fix: qualify CLI descriptor closes 2026-08-12 16:09:23 -07:00
austinpower1258 9dadda2243 fix: align baseline test and SwiftUI APIs 2026-08-12 15:55:15 -07:00
austinpower1258 8d140de85a fix: harden restore socket readiness 2026-08-12 15:55:09 -07:00
Abdulaziz Albahar a028ba24e1 Fix multi-Mac connection lifecycle
Merge authorized by the user. Focused Swift package tests passed on the merged tree.
2026-08-12 15:49:03 -07:00
Abdulaziz Albahar 359651390c Hide notification rows for deleted workspaces (#10055)
* test(ios): cover orphaned notification feed rows

* fix(ios): hide notification rows without live targets

* test(ios): wait for feed preview projection

* test(ios): reject ambiguous notification targets

* fix(ios): reject ambiguous notification targets

* refactor(ios): split notification target index types
2026-08-12 15:45:59 -07:00
austinpower1258 959f38a4c3 fix: reclaim stale sockets and wait for restore startup 2026-08-12 15:25:49 -07:00
Abdulaziz Albahar 3473b9bca4 Fix disabled iOS controls after forgetting a computer (#10010)
* test(ios): reproduce orphaned modal host after forgetting computer

* fix(ios): retain modal owner shell through computer deletion

* test(ios): cover controls after forgetting final computer

* test(ios): harden final computer deletion flow

* test(ios): follow disconnected add-device path

* Fix iOS deletion UI test build

* test(ios): isolate final computer deletion flow

* test(ios): isolate post-forget toolbar checks

* Keep iOS root modal presenter mounted across shell changes

* fix(ios): root-host Computers modal
2026-08-12 15:19:00 -07:00
Abdulaziz Albahar 1943f9e3f2 Add comprehensive privacy-safe iOS app diagnostics (#9977)
* Add comprehensive iOS app diagnostics

* Inject diagnostic correlation helper

* Complete diagnostic failure handling

* Add diagnostics correctness regressions

* Fix iOS diagnostics correctness gaps

* Handle new diagnostic failures in settings

* Fix task recovery diagnostic pattern

* Test presence diagnostic count tracking

* Keep presence diagnostics constant time

* Restore foreground route reservation

* Assert foreground transport ordering

* Test attachment staging ownership

* Fix iOS diagnostics task ownership

* Fix iOS diagnostics compilation

* Fix relay diagnostics test payloads

* Test chat diagnostics race ownership

* Fix chat diagnostics race ownership

* Import mobile analytics contracts in app delegate

* Test null portal subview mutation crash

* Fix null portal subview launch crash

* Isolate null portal subview regression

* Repair iOS diagnostics after main merge
2026-08-12 15:08:07 -07:00
austinpower1258 3902d0faae test: cover ambient dev CLI socket fallback 2026-08-12 14:03:09 -07:00
austinpower1258 e4acad6d8d test: cover stale socket rebind and restore startup wait 2026-08-12 13:58:18 -07:00
Austin Wang 1237f6e71c Merge pull request #10038 from manaflow-ai/issue-10033-tab-drag-wide-tabs
Fix pane tab drags silently never starting (#10033)
2026-08-12 13:22:13 -07:00
cmux reload-cloud b590e721cd Merge remote-tracking branch 'origin/main' into issue-10033-tab-drag-wide-tabs
# Conflicts:
#	Sources/BrowserPaneDropTargetView.swift
#	Sources/DockSplitStore.swift
#	Sources/TerminalPaneDropTargetView.swift
#	Sources/Workspace.swift
#	vendor/bonsplit
2026-08-12 13:12:32 -07:00
cmux reload-cloud bd222b33b3 Advance Bonsplit to single-active-pane fix 2026-08-12 13:08:33 -07:00
cmux reload-cloud 76be87b248 Finish accepted cross-container tab drags 2026-08-12 12:52:05 -07:00
Austin Wangandcmux reload-cloud 0136f66285 Unify Vault session pane-drop routing (#10032)
* test: cover Vault drops across pane targets

* fix: unify pane transfer drop routing

* fix: restore drop routing module import

* fix: return browser pane drag operation

* test: cover repeated Vault row drags

* fix: preserve repeated Vault row drag identity

* test: cover live Vault drags through browser portal

* fix: require live pane transfer sources

* test: cover Vault pane transfer lifecycle

* test: cover repeated Vault drag cleanup

* fix: preserve accepted Vault pane transfers

* test: satisfy Vault fixture isolation

* fix: end pane drags without invalid super dispatch

* test: cover pane drag completion cleanup

* refactor: share pane drop destination mapping

* test: cover repeated native Vault row drags

* test: cover repeated multi-click Vault drags

* fix: make Vault row drags native pane transfers

* test: update Vault popover drag fixture

* test: require row-owned Vault drag sources

* fix: make each Vault row own its drag source

* refactor: inject Vault drag registry

* test: cover native Vault portal pasteboard handoff

* test: cover hosted duplicate Vault row recycling

* fix: publish live Vault pane transfer capability

* fix: return Vault drag start result

* test: require Vault drags to publish pane capabilities

* fix: route Vault drags through Bonsplit capabilities

* fix: disambiguate Bonsplit Vault tab metadata

* fix: isolate Vault pane capability registration

* fix: construct pane registries on the main actor

* fix: share pane registry during app composition

* fix: unwrap app pane registry during composition

* fix: adopt the initial pane transfer registry

* test: require Vault capability portal routing

* fix: resolve pane drops through shared capabilities

---------

Co-authored-by: cmux reload-cloud <[email protected]>
2026-08-12 11:43:14 -07:00
austinpower1258andClaude Fable 5 75adf5be7c Restore 0.64.22 pane drop targets for tab drags
Advance Bonsplit to the restored legacy pasteboard payload
(manaflow-ai/bonsplit#219): pane/browser drop overlays decode
tab.id/kind/sourcePaneId/sourceProcessId again, so the blue drop zones
render and drops route. Also share one tab-drag capability registry
between workspaces and the Dock so bonsplit tab-strip destinations can
resolve drags that began in another controller, matching 0.64.22 where
any destination could decode the drag.

Fixes #10033

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-12 03:16:58 -07:00
austinpower1258andClaude Fable 5 3aa145e9a8 Advance Bonsplit to drag return-animation fix
Ends cancelled tab drags immediately instead of after AppKit's snap-back
animation, so drag state is revoked before the next press.
Bonsplit PR: manaflow-ai/bonsplit#218

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-12 02:49:40 -07:00
austinpower1258andClaude Fable 5 a79ae2c264 Advance Bonsplit to second-click tab drag fix
Tab drags silently never started when the press arrived with
clickCount >= 2 — the common click-to-select-then-immediately-drag flow.
Bonsplit PR: manaflow-ai/bonsplit#217

Fixes #10033

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-12 02:47:05 -07:00
austinpower1258 26a5acfe2a Advance Bonsplit for reliable tab drags 2026-08-12 02:46:14 -07:00
Austin Wang b17c260b61 Fix intermittent dragging for wide tabs (#10035)
* Fix wide-title tab drag hit testing

* Advance Bonsplit through current main lineage

* Advance Bonsplit to merged wide-tab drag fix
2026-08-11 23:47:00 -07:00
austinpower1258 7b6758316e Merge remote-tracking branch 'origin/main' into issue-10033-tab-drag-wide-tabs 2026-08-11 23:43:46 -07:00
austinpower1258 5d62505a2d Advance Bonsplit to merged wide-tab drag fix 2026-08-11 23:43:46 -07:00
AvoChangandClaude Opus 5 a42efb2fdf Drop into a collapsed group by releasing on its header row (#9992)
* Add failing tests for dropping onto a collapsed/empty group header

A workspace dragged from outside a group can only land inside it by
hitting an insertion gap that belongs to the group row scope. When the
group has no visible member row under its header - collapsed, or a group
whose only member is its anchor - the resolver treated the header bottom
edge as a group/root boundary and handed it to the ambiguous horizontal
lane rule, so the left half of the header silently planned a root slot
beside the group instead of inside it.

These tests pin the intended behavior and fail on the current resolver.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Adopt into the group when a drag is released on its header row

A group header row's lower half is the group's own adopt zone: releasing
there should put the workspace inside the group. It already did while the
group's members were rendered below the header, because the resolver
confirmed the group scope by looking at the next visible row. When there
is no member row under the header - the group is collapsed, or its only
member is its anchor - that lookup failed and the drop fell through to
the ambiguous group/root horizontal lane rule, so the left half of the
header planned a root slot beside the group instead. The gap that would
have accepted the workspace is the one the collapse is hiding, leaving no
discoverable way to drop into a collapsed group.

Treat a group header as an unambiguous group target on its bottom edge
and stop consulting the next visible row there; only member rows can sit
on a real group/root boundary. The header's top half still plans the root
slot before the group, so inserting above a leading group keeps working.

The fix lands in the shared resolver, so the AppKit sidebar table, the
SwiftUI reorder overlay, and the mobile move-intent resolver all pick it
up from one path.

Co-Authored-By: Claude Opus 5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-11 23:38:47 -07:00
austinpower1258 3a0db46f7e Advance Bonsplit through current main lineage 2026-08-11 23:32:09 -07:00
austinpower1258 9465641e04 Fix wide-title tab drag hit testing 2026-08-11 23:23:03 -07:00
Austin Wang 13932332be Preserve focus when closing an unfocused pane (#10021)
* Bump Bonsplit for close-pane focus preservation

* Advance Bonsplit focus-close coverage

* Retarget Bonsplit fix to tracked baseline
2026-08-11 20:06:17 -07:00
Abdulaziz Albahar 80633f61e1 Restore Tailscale pairing with cmux 0.64.17 (#9956)
* test(ios): expose legacy Tailscale migration dead ends

* fix(ios): restore authorized legacy Tailscale pairing

* fix(ios): make Tailscale readiness durable

* fix(ios): expose cached Tailscale readiness internally

* test(ios): cover persisted migration upgrade state

* fix(ios): enforce fitted migration sheet detent

* fix(ios): guard optional Tailscale recovery action

* test(ios): make authorization fixture deterministic
2026-08-11 19:48:14 -07:00
Abdulaziz AlbaharandClaude Fable 5 cba80c402e iPhone auth gate: hard-fail unauthenticated dogfood installs (#10011)
* iPhone auth gate: hard-fail unauthenticated dogfood installs

Installed-but-signed-out was a docs rule (PR 10001) but not enforced;
agents kept handing off phone builds sitting on the login screen. Three
mechanical changes:

1. Hard-fail: scripts/mobile-dev-launch.sh device launches default to
   --ensure-mac, and the post-launch readiness wait (mobile.rpc.ready with
   the device's dogfood client id) is now the iPhone auth gate: failure
   exits non-zero with the reason and exact retry command. --no-attach /
   --no-sign-in / --no-setup / --no-launch on the device leg are refused
   unless CMUX_ALLOW_UNAUTHENTICATED_INSTALL=1, which is human-only
   (agents never set it; same convention as CMUX_ALLOW_LOCAL_XCODEBUILD).
   ios/scripts/reload.sh forwards --no-attach explicitly and reports the
   verified auth state in its device summary.

2. Verification verb: scripts/verify-iphone-auth.sh --tag <tag>
   [--device-id <id>] relaunches the installed app WITHOUT injecting
   credentials or a ticket and passes only if persisted sign-in + pairing
   produce a usable RPC session with the tagged Mac (PASS/FAIL + reason +
   retry command, no screenshots). It writes the same readiness receipt as
   a gate pass.

3. Queue truthfulness: an auth-failed drain parks the entry in needs-auth/
   (kept, retryable via the new `retry --tag <tag>` verb) instead of
   failed/, and cmux notify reports the TRUE state ("VERIFIED signed in +
   paired" vs "installed but SIGN-IN FAILED: <reason>" with the retry
   command). A fresh readiness receipt is required to count a drain as
   verified, so a launcher that lies with exit 0 still parks as
   needs-auth. Unauthenticated enqueues need the human-only allowance,
   recorded in the entry for the headless LaunchAgent drain.

Install invocations are marked CMUX_SANCTIONED_IPHONE_INSTALL=1 for the
cmuxterm-hq local-build-guards devicectl interceptor
(manaflow-ai/cmuxterm-hq#297), which refuses raw devicectl installs on the
personal iPhone; merge this PR before that one.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address review: no-attach never reports verified; receipt freshness by removal

- ios/scripts/reload.sh: a --no-attach signed launch (human-authorized
  opt-out) now reports UNVERIFIED in the device summary instead of
  "verified signed in + paired" (the gate never ran on that path).
- iphone-install-queue.sh: remove any pre-existing readiness receipt
  before the signed launch, so freshness is existence-after-removal
  instead of a whole-second mtime comparison that a same-second stale
  receipt could pass.
- verify-iphone-auth.sh: warn explicitly when the receipt cannot be
  persisted after a PASS instead of staying silent.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Deferred delivery: locked/offline phone means enqueue-and-notify, exit 75

Agents burned hours in unlock-watcher loops (one takeover: 45min of
cycles; one script: 87 retries over 4 days). Now the device paths defer
promptly instead of retrying or watching:

- mobile-dev-launch.sh probes reachability ONCE before minting; an
  offline phone notifies (best-effort cmux notify), prints the exact
  retry command, and exits 75 (EX_TEMPFAIL). A launch that fails because
  the phone is LOCKED gets the same treatment.
- ios/scripts/reload.sh: the queued outcome (phone unreachable at build
  time) now notifies and exits 75 instead of 0 — queued-not-installed is
  never reported as success. If the phone locks mid-delivery (launcher
  exit 75 after install), the built signed app is parked in the install
  queue, the user is notified to unlock, and the reload exits 75.
- iphone-install-queue.sh drain maps launcher exit 75 to keep-pending
  (not needs-auth): a locked phone is a delivery deferral, not an auth
  failure, and the LaunchAgent's periodic drain retries it.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address review round 2: fail closed on unclearable receipt, truthful queue claim

- iphone-install-queue.sh: if a stale readiness receipt cannot be removed
  before the signed launch, fail the entry instead of risking a verified
  claim backed by an old file.
- ios/scripts/reload.sh: the locked-phone deferred path only prints
  "queued; unlock to receive" (and exits 75) when the enqueue actually
  succeeded; otherwise it errors with the manual retry command.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Preserve --no-attach in the deferred-delivery enqueue

A human-authorized --no-attach launch that hits a locked phone must queue
with the same intent; dropping the flag would escalate the queued drain
to the full ensure-mac paired flow.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-11 19:41:38 -07:00
Austin Wang c8b9afc23d Fix false SSH host-death verdict and remote diagnostics (#9971)
* test(ssh): cover false remote death recovery

* ci: exercise SSH regression baseline

* fix(ssh): preserve remote PTY recovery

* fix(ssh): harden recovery diagnostics

* fix(remote): keep log routing portable

* test: follow renamed resume command field

* test: cover recovery preservation edge cases

* fix: preserve recovery state through inconclusive probes

* test: exercise terminal teardown through app fixture

* test(remote): cover closed daemon output descriptors

* fix: harden recovery review edge cases

* test: substitute pinned SSH in teardown fixture

* test: cover final recovery review edge cases

* fix: preserve recovery through final reconciliation edges

* test: distinguish automatic and manual PTY recovery

* fix: report PTY recovery state accurately

* test: make reconciliation parameter type explicit

* test: require reconciliation acknowledgement contracts

* Fix SSH startup fixtures after main merge

* Test confirmed PTY end cleanup failure

* Finalize cleanup for confirmed PTY exit

* Keep SSH auth fixtures off live network

* Fix SSH tests after reconnect merge

* Test terminal reset before no-progress reattach

* Reset terminal modes before no-progress reattach

* Run no-progress reset regression in package tests

* Fix SSH test fixtures after reconnect merge
2026-08-12 00:43:25 +00:00
Frank QingandFrank Qing 16c3e69e8b Fix sidebar reorder breaking Mission Control (#9807)
* test(sidebar): cover reorder drop destination isolation

* fix(sidebar): isolate workspace reorder destination

* test(sidebar): cover reorder target rearming

* fix(sidebar): reset reorder drop lifecycle

---------

Co-authored-by: Frank Qing <[email protected]>
2026-08-11 17:39:21 -07:00
Austin Wang 8952b5e843 Fix Claude Teams teammate respawn environment (#9731)
* test: cover claude-teams respawn launcher environment

* fix: preserve claude-teams respawn environment

* refactor: package claude-teams respawn transport

* Fix restore argument return for Xcode 27

* test: cover claude-teams restore capture

* fix: capture claude-teams sessions for restore

* test: handle capability-wrapped claude-teams fixtures
2026-08-11 17:14:10 -07:00
Austin Wang 407a935362 Revert portal hierarchy mutation tracker launch crash (#10018)
* Revert "Fix portal mutation tracker launch crash (#10008)"

This reverts commit a161d16941.

* Revert "Portal: invalidate the split-divider hit-test cache on nested subview insertion (#8580)"

This reverts commit bcfb2d7b0b.
2026-08-11 17:09:54 -07:00
Austin Wang 5ff45e39c9 Docs: explain sandbox constraints for sidebar extensions (#9104)
* docs: explain sidebar extension sandbox constraints

* docs: clarify sidebar sandbox authorization boundaries

* docs: distinguish development tool paths from distribution

* docs: refresh sidebar sandbox guidance
2026-08-11 17:03:19 -07:00
Austin Wang 31fd3b9c77 Keep screenshot paste preparation off the main thread (#8838)
* test: require image pasteboard reads off main

* fix: prepare composer image pastes off main

* test: preserve composer selection during async paste

* fix: make pending composer pastes undo-safe

* fix: parse pasted HTML without AppKit importer

* refactor: make HTML paste parser an instance service

* refactor: satisfy paste review policy

* test: cover malformed composer HTML paste

* fix: parse pasted HTML with Foundation tokenizer

* refactor: remove blocking image prepare overload

* Revert "refactor: remove blocking image prepare overload"

This reverts commit 36f65f6b02.

* test: cover async paste caret and preformatted HTML

* fix: preserve async paste caret and preformatted text

* test: cover edits during async paste

* fix: preserve composer edits during async paste

* fix: bound asynchronous paste preparation

* fix: preserve paste ordering under blocking providers

* test: cover paste worker exhaustion and reservation sync

* fix: isolate paste preparation in killable worker

* test: cover paste binding and encoded HTML regressions

* fix: preserve composer state during async paste

* test: cover overlapping paste and undo ordering

* test: model separate undo events during pending paste

* fix: keep pending paste edits undo-stable

* test: cover image paste with whitespace HTML

* fix: preserve image detection and unique test wiring

* test: cover repeated and large text pastes

* fix: preserve input and bulk paste ordering

* fix: hop clipboard confirmation to main actor

* test: bound bulk paste text transport

* fix: bound bulk paste text transport

* refactor: inject paste file operations

* refactor: simplify clipboard input routing

* test: preserve existing file URLs through paste worker

* test: document pending paste typing order

* test: cover clipboard admission ordering race

* fix: admit clipboard reads before main actor hop

* refactor: isolate clipboard callbacks on main actor

* test: bound HTML paste parsing

* fix: bound rich clipboard parsing

* docs: explain clipboard input overflow policy

* fix: satisfy strict paste concurrency checks

* fix: resolve paste pipeline CI compilation

* Make screenshot recovery test deterministic

* Document paste worker cancellation contract

* Fix paste test helper imports

* Preserve hidden HTML templates during paste

* Align HTML normalizer with package policy

* Use public AppKit attachment character in test

* Test self-closing HTML templates during paste

* Preserve text after self-closing templates

* Test HTML entities from data paste input

* Preserve non-ASCII entities in HTML paste

* Test UTF-16 HTML pasteboard data

* Decode BOM-marked HTML before paste normalization

* Test bounded HTML nesting during paste

* Bound HTML paste traversal depth

* Test temporary image destination validation

* Validate temporary image adoption destination

* Reject stale clipboard confirmation callbacks

* Fix merged main actor default warning

* Fence async paste completion to surface lifetime

* Test paste rollback and rejected HTML fallback

* Harden paste failure and worker lifetimes

* Expose clipboard callback context across extension files

* Test pending paste publication edge cases

* Fix pending paste publication edge cases

* Test pointer ordering during terminal paste

* Harden asynchronous paste sequencing

* Test stale terminal callback identity

* Fence paste callbacks to native surface lifetimes

* Test paste sequencing and content fidelity edges

* Close paste sequencing and fidelity edge cases

* Test bounded generation-aware paste input

* Bound paste input by runtime generation

* Test hidden HTML and runtime clipboard teardown

* Test atomic runtime clipboard ownership

* Close clipboard preparation and teardown races

* Test pre-admission overflow and CSS visibility

* Preserve overflow input and visible HTML descendants

* fix: make clipboard reservation sendable

* fix: expose callback identity dependency

* fix: express overflow path as conditional

* fix: type isolated workspace lookup

* test: cover paste deadline and overflow ordering

* test: fix weak reference declaration

* fix: close paste deadline and overflow races

* fix: bound runtime clipboard admission

* test: cover remaining paste lifecycle regressions

* fix: close remaining paste preparation gaps

* test: fix terminal image concurrency target compilation

* test: cover admission-scoped paste deadlines

* fix: enforce paste deadlines from admission

* test: align clipboard pointer fixture epoch

* test: stabilize clipboard input sequencing fixture

* fix: publish one clipboard completion per read

* fix: distinguish invalid paste image files

* fix: keep clipboard overflow cancellation sendable

* test: discard pre-admission input during teardown

* test: cover programmatic input during clipboard reads

* test: publish orphaned pending paste commits

* fix: close clipboard ordering races

* fix: preserve clipboard rollback admission

* fix: compile strict clipboard test targets

* test: cover clipboard review regressions

* test: cover isolated clipboard rollback capture

* fix: isolate clipboard rollback capture

* fix: export pasteboard snapshot AppKit dependency

* test: cover cancellation before mutation publication

* fix: compile strict pasteboard lane paths

* test: expose clipboard overflow replay inversion

* fix: prevent deferred input replay inversion

* test: compile async clipboard readiness assertions

* fix: keep rollback ownership across cancellation

* fix: preserve overflow handler actor isolation

* test: compile terminal clipboard package coverage

* refactor: split clipboard concurrency coverage

* test: cover abandoned clipboard restore failure

* fix: retry abandoned clipboard restoration

* test: preserve adopted image on permission failure

* fix: roll back image adoption on permission failure

* test: preserve content after self-closing raw tags

* test: align callback fixture with agent shims

* fix: close self-closing raw-text elements

* test: preserve RTF after rejected image HTML

* fix: preserve RTF fallback after rejected HTML

* fix: preserve unquoted slash template values

* test: align clipboard fixtures with runtime validation

* test: await textbox paste admission

* test: align restored snapshot resume assertion

* test: await remaining textbox paste bindings

* test: hide iframe fallback paste text

* fix: hide iframe fallback paste text

* test: discard superseded clipboard write

* fix: discard stale coalesced clipboard writes

* test: preserve RTFD rejected HTML fallback

* fix: preserve RTFD rejected HTML fallback

* test: cover clipboard cancellation and whitespace fallback

* fix: report admitted clipboard writes accurately

* test: cover mobile click and quoted raw text ordering

* fix: keep mobile clicks and raw text boundaries intact
2026-08-11 17:02:36 -07:00
Austin Wang 66e44fd0e0 Keep browser portal refreshes below SwiftUI (#9774)
* test: reproduce browser portal reentrant layout

* fix: defer browser portal layout to AppKit

* fix: remove deprecated notification change handlers

* test: reproduce stale browser portal geometry

* fix: refresh deferred browser portal geometry

* test: reproduce browser portal geometry reentrancy

* fix: keep browser portal sync out of SwiftUI layout

* test: keep browser host instrumentation test-only

* test: isolate browser portal layout synchronization

* test: cover browser portal presentation ownership

* fix: keep browser portal refreshes below SwiftUI
2026-08-11 17:01:53 -07:00
Austin Wang 5b4eba1a67 Give SidebarRowTextView ownership of link styling so selected-row description links stay readable (#9613)
* Add failing test: sidebar description link is unreadable on the selected row

The link run in a workspace description renders in NSColor.linkColor, which
AppKit paints over the row palette. On an active row the sidebar selection
background is that same blue.

Refs https://github.com/manaflow-ai/cmux/issues/9596

* Give SidebarRowTextView ownership of link styling

AppKit paints .link runs in NSColor.linkColor and ignores the explicit
.foregroundColor the row applies, so a URL in a workspace description was
blue on the blue sidebar selection background when the row was selected.

SidebarRowPalette.attributed now moves every web link onto a private
.sidebarRowLink key and styles the run itself: the palette-derived color
plus an underline so it still reads as a link in both states. The active-row
color comes from sidebarSelectedWorkspaceForegroundNSColor, so a custom
sidebarSelectionColorHex stays legible. Covers the workspace description and
the metadata markdown blocks; SidebarRowTextView.linkURL(at:) reads the
private key, and the http(s)-only destination contract moves to a shared
SidebarRowWebLink.

Fixes https://github.com/manaflow-ai/cmux/issues/9596

* Cover the metadata markdown blocks in the row link tests

CodeRabbit: the new tests exercised customDescription only, while the same
SidebarRowPalette.attributed path also renders snapshot.metadataBlocks.

Adds active/inactive coverage asserting the row-owned .sidebarRowLink
destination, the palette foreground, the underline, and that AppKit's .link is
gone, plus an unsafe-scheme case.

* Measure only glyph pixels in the link raster assertions

CodeRabbit: minimumDistance scanned the whole bitmap, so the uniform selection
fill (itself a blue near NSColor.linkColor) could satisfy or break the
assertions without the link glyphs having any bearing on them. Background
pixels are now excluded, and the helper asserts some text pixels were found so
an empty raster cannot pass silently.

* Move sidebar link behavior onto the row text view

* Make sidebar link raster assertions antialiasing-safe

* Preserve sidebar link accessibility semantics

* Give row links actionable accessibility elements

* Test row link accessibility lifecycle

* Expose row links through accessibility tree

* Test stable and hidden row link lifecycles

* Preserve row link accessibility lifecycles

* Test row link semantic reuse and geometry

* Scope row link proxies to semantic owner

* Test pooled row link proxy release

* Release hidden row link accessibility state

* Test inline rename title restoration

* Preserve plain row text while hidden

* Test on-demand row link geometry

* Complete row link accessibility lifecycle

* Bridge row accessibility callbacks to main actor

* Test lazy row link accessibility materialization

* Defer detached row accessibility proxies

* Release pooled sidebar link proxies

* Harden sidebar AppKit lifecycle tests

* Stabilize sidebar rename field editor test

* Test sidebar link ownership lifecycles

* Repair extracted sidebar description compilation

* Fix sidebar link ownership lifecycle

* Test truncated sidebar links stay inaccessible

* Keep inline rename cancellation test focus-independent

* Hide truncated sidebar links from accessibility

* Apply one immutable sidebar link policy

* Keep sidebar description projection lightweight

* Keep sidebar link accessibility demand-driven

* Make sidebar retirement tests event-driven

* Test truncated sidebar link pointer hit

* Fix AppKit popover close test wait

* Update sidebar rename behavior test

* Make popover close wait state-driven

* Ignore truncated sidebar link glyphs

* Fix popover retirement close wait

* Fix checklist popover window lookup

* Test dark sidebar description color resolution

* Resolve sidebar row colors by appearance

* Test row palette preserves dynamic colors

* Preserve dynamic sidebar semantic colors
2026-08-11 16:58:28 -07:00
Austin Wang 766dee33c4 Fix Vault restore admission and chat persistence (#9964)
* test: preserve snapshot resume command coverage

* test: require Vault resume chat rebinding

* fix: admit Vault restores before terminal startup

* test: require explicit Vault runtime admission

* fix: explicitly admit Vault terminal startup

* test: preserve queued Vault restores across relaunch

* fix: persist in-flight Vault resume intent

* test: require topology-safe restore admission

* fix: admit restores after authoritative topology

* fix: close Vault restore persistence regressions

* refactor: centralize terminal startup restore commit

* test: align relaunch restore expectations

* test: model admitted restore lifecycle

* test: cover queued restore admission regressions

* fix: bind queued restores to structured startup work

* test: cover Dock startup restore transaction transfer

* fixup! test: cover Dock startup restore transaction transfer

* fix: transfer staged restores into Dock ownership

* test: cover pending restore cancellation cleanup

* test: preserve foreign claims during restore cancellation

* fix: cancel staged restores without leaking claims

* test: cover restore admission failure cleanup

* fix: gate and clean failed restored startup work

* test: preserve queued Vault restore identity

* fix: preserve queued Vault restore identity

* test: cover restore identity and teardown ownership

* fix: retain staged restore lifecycle ownership

* fix: keep restored lifecycle updates panel-scoped

* test: reject mismatched Vault restore liveness

* fix: match restore liveness to session identity

* test: preserve snapshotless restore completions

* fix: preserve snapshotless restore completions

* refactor: split restore transaction types
2026-08-11 16:43:06 -07:00
Austin Wang 1780ecd9d8 Merge pull request #9870 from manaflow-ai/issue-9869-dock-chromeless-browser
Add chromeless Dock browser controls
2026-08-11 16:41:50 -07:00
Lawrence Chen 71d80faac0 Merge pull request #9840 from manaflow-ai/task-cli-lifecycle-grammar
feat(tui): add local server lifecycle commands
2026-08-11 14:54:44 -07:00
Abdulaziz Albahar a161d16941 Fix portal mutation tracker launch crash (#10008)
* fix(portal): retain inserted views through hooks

* fix(portal): track insertion before AppKit consumes child
2026-08-11 13:27:36 -07:00
Lawrence Chen 3cdaf444e3 fix(tui): accept transport close after stop ack 2026-08-11 13:17:24 -07:00
Lawrence Chen 1e3290c3b1 test(tui): cover transport close after stop ack 2026-08-11 13:17:05 -07:00
Lawrence Chen 8ba2f6a69e fix(tui): restore staged state after reset mismatch 2026-08-11 11:31:14 -07:00
Abdulaziz Albahar 7ffe71cc84 fix(iroh): preserve verified sessions across auth blips 2026-08-11 11:28:18 -07:00
Abdulaziz Albahar d93c5c338b test(iroh): expose premature admission disconnects 2026-08-11 11:28:17 -07:00
Abdulaziz Albahar 006d2c8375 Fix iOS workspace selection after Iroh reconnect (#9979)
* test(ios): preserve workspace selection across reconnect

* fix(ios): preserve workspace selection on reconnect
2026-08-11 11:08:48 -07:00
Abdulaziz AlbaharandClaude Fable 5 f06d00e4fd docs: require authentication on every installed iPhone build (#10001)
Agents kept handing off phone builds that were installed but sitting on
the login screen. Installed-but-signed-out is now explicitly a failed
install: every install must be verified past login and paired before
handoff, raw devicectl installs and --no-sign-in are banned for dogfood
builds, and unresolvable sign-in blockers must be reported with the
retry command.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-11 10:44:42 -07:00
Abdulaziz Albahar b886b6c234 Hide Add Computer outside Tailscale (#9957)
* test(ios): hide manual pairing outside Tailscale

* fix(ios): gate Add Computer on Tailscale

* test(ios): cover automatic attach approval

* fix(ios): preserve automatic attach approval

* test(ios): exercise attach approval action

* fix(ios): hide unavailable push pairing repair

* test(ios): switch pairing capability in settings

* test(ios): verify onboarding connection actions

* fix(ios): refresh manual pairing capability

* fix(ios): model pairing availability as data

* fix(ios): observe pairing capability in shells

* refactor(ios): keep pairing gate at root

* test(ios): verify persisted pairing capability

* test(ios): seed connection method fixtures

* test(ios): isolate connection method launches

* test(ios): keep pairing gate test single launch
2026-08-11 10:40:06 -07:00
Abdulaziz Albahar 9d5b8d98f9 iOS: preserve workspace groups in Recent Activity (#9960)
* test(ios): preserve groups in recent activity

* fix(ios): keep recent workspace groups atomic

* perf(ios): reuse grouped workspace projection

* fix(ios): observe authoritative group reorder

* perf(ios): append group members in place

* perf(ios): reuse workspace lookup in grouped rows

* perf(ios): cache grouped workspace projection

* test(ios): make grouped scale coverage deterministic

* perf(ios): reuse grouped workspace snapshot

* test(ios): cover grouped workspace journeys

* fix(ios): align grouped workspace sort contracts

* test(ios): cover picker connection status accessibility

* fix(ios): preserve picker status accessibility

* test(ios): query sort tiles by identifier

* test(ios): stabilize workspace picker queries

* test(ios): scope computers before search journey

* test(ios): cover grouped filter transitions

* test(ios): seed grouped filter fixtures

* test(ios): sample grouped transitions densely
2026-08-11 10:33:22 -07:00
Lawrence Chen c5d5e41d3e fix(tui): classify deadline socket close 2026-08-11 10:30:35 -07:00
Lawrence Chen dcba51a2f3 style(cmux-tui): apply hosted rustfmt 2026-08-11 10:09:30 -07:00
Lawrence Chen 2f8eda988d fix(tui): accept terminal stop connection close 2026-08-11 10:04:33 -07:00
Lawrence Chen abfc0344af test(tui): complete Linux proc stat fixture 2026-08-11 10:04:24 -07:00
Lawrence Chen 7673659876 test(tui): synchronize stalled host timeout 2026-08-11 08:54:12 -07:00
Lawrence Chen f6ecd39c36 fix(tui): bind detach fence to smart renderer 2026-08-11 08:30:48 -07:00
Lawrence Chen 2696d60b2b style(tui): apply hosted rustfmt 2026-08-11 07:59:34 -07:00
Lawrence Chen 5561e45435 test(tui): require journal sensitivity fixtures 2026-08-11 07:54:31 -07:00
austinpower1258 ab6c8b5372 Fix SSH retry test zero-match counting 2026-08-11 07:45:03 -07:00
Lawrence Chen f28115d94f test(tui): opt in to sensitive journal tail 2026-08-11 07:16:49 -07:00
austinpower1258 55c544317e Restore web focus intent for chromeless panes 2026-08-11 06:29:46 -07:00
Lawrence Chen fa5e8bc5fe test(tui): avoid shutdown flush deadlock 2026-08-11 06:27:50 -07:00
austinpower1258 3656550fd2 Fix Dock close-others warning parity 2026-08-11 05:56:28 -07:00
austinpower1258 57dfb22ac4 Test Dock close-others clean-tab warning 2026-08-11 05:43:09 -07:00
Lawrence Chen 64aecd5833 Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-11 05:34:21 -07:00
austinpower1258 498c1457f4 Merge remote-tracking branch 'origin/main' into issue-9869-dock-chromeless-browser
# Conflicts:
#	Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/SSHTerminalExitPromptInputFilterTests.swift
2026-08-11 05:32:53 -07:00
austinpower1258 2cad7403a9 Fix mutating Swift Testing expectations 2026-08-11 05:15:59 -07:00
Austin Wang 93118b3b90 Merge pull request #9921 from manaflow-ai/issue-9920-workitem-chain-producer
Prevent recursive deferred-action release chains
2026-08-11 05:08:35 -07:00
Lawrence Chen 89b75e4c3d Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-11 04:33:55 -07:00
Lawrence Chen 22c82ea176 Merge pull request #9989 from manaflow-ai/fix-journal-bounded-replay-sensitivity
Fix bounded journal replay sensitivity fixture
2026-08-11 04:33:19 -07:00
austinpower1258 fe55b0bcdc Preserve main browser actions during sidebar focus 2026-08-11 04:11:13 -07:00
austinpower1258 baa99df7cc Merge remote-tracking branch 'origin/main' into issue-9920-workitem-chain-producer 2026-08-11 03:49:06 -07:00
austinpower1258 5405b0553d Fix Xcode 26.3 Swift Testing compilation 2026-08-11 03:49:03 -07:00
Lawrence Chen ebae1456ce test(tui): request sensitive workspace journal events 2026-08-11 03:32:12 -07:00
austinpower1258 209e13c858 Test main browser shortcuts during sidebar focus 2026-08-11 03:25:52 -07:00
Lawrence Chen dad25310f4 Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-11 03:25:47 -07:00
Lawrence Chen bd910bfe99 Merge pull request #9987 from manaflow-ai/fix-tui-unused-shell-quote-helper
Remove obsolete TUI shell quote test helper
2026-08-11 03:25:19 -07:00
Lawrence Chen 8eea0590ed test(tui): remove obsolete shell quote helper 2026-08-11 03:11:51 -07:00
austinpower1258 b6ac6716e3 Merge remote-tracking branch 'origin/main' into issue-9869-dock-chromeless-browser
# Conflicts:
#	Sources/DockSplitStore+SessionRestore.swift
#	Sources/DockSplitStore+SurfaceTransfer.swift
#	Sources/DockSplitStore.swift
2026-08-11 03:09:02 -07:00
austinpower1258 8513cdd0bb Restore Dock beta fixture on early exits 2026-08-11 02:55:46 -07:00
Lawrence Chen 784cecf336 docs(tui): correct protocol 12 event count 2026-08-11 02:54:47 -07:00
austinpower1258 766c95ad36 Fix Dock unread and browser focus fallbacks 2026-08-11 02:52:37 -07:00
austinpower1258 cf311e0b79 Enable Dock beta in shortcut test harness 2026-08-11 02:52:31 -07:00
Lawrence Chen 8c74ab408d Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-11 02:51:53 -07:00
Lawrence Chen 7217b1cf41 Watch hosted TUI verification completion (#9942)
* ci: watch hosted TUI verification completion

* fix: restart hosted completion watcher

* fix: bound hosted watcher retries

* fix: bound hosted watcher cleanup

* Report hosted cancellation failures

* Install hosted cleanup before FIFO setup
2026-08-11 02:50:05 -07:00
Lawrence Chen 36cfc19d45 test(tui): freeze protocol 11 conformance baseline 2026-08-11 02:47:04 -07:00
Lawrence Chen 11acc48058 Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-11 02:41:01 -07:00
austinpower1258 5e4007d3ff Fix Dock shortcut notification test harness 2026-08-11 02:37:52 -07:00
austinpower1258 fcdd0853ea Merge remote-tracking branch 'origin/main' into issue-9920-workitem-chain-producer 2026-08-11 02:26:49 -07:00
Austin Wang 50b1d2ac50 Cache and gate Ghostty macOS unified logging (#9368)
* Update Ghostty for cached macOS loggers

* Pin GhosttyKit for cached macOS logger

* Combine cached loggers with current Ghostty fixes

* Pin combined GhosttyKit artifact

* Remove unpublished GhosttyKit checksum mapping
2026-08-11 02:09:54 -07:00
Austin Wang df5889a515 Fix SSH wake reconnect aborts (#9966)
* test(ssh): cover wake reconnect input handling

* fix(ssh): preserve persistent wake reconnects

* fix(ssh): avoid polling closed prompt input

* fix(ssh): filter terminal prompt input continuously

* test(ssh): reject enter inside control input

* fix(ssh): keep embedded line endings filtered
2026-08-11 02:08:28 -07:00
Austin Wang d000289621 Fix file drag-and-drop in Dock sessions (#9778)
* test: cover file drops in global Dock sessions

* fix: route file drops through Dock containers

* refactor: harden Dock file-drop routing

* perf: focus Dock file-drop batches once

* fix: address Dock file-drop review findings

* fix: harden Dock drop routing and observation

* fix: finish Dock drop review follow-ups

* fix: keep preview metadata actor-independent

* fix: index Dock surface ownership

* fix: close Dock file-drop review gaps

* fix: compile Dock drop regression coverage

* test: require indexed Dock pane ownership

* test: cover Dock metadata ownership lifecycle

* refactor: centralize Dock preview metadata ownership

* test: cover late Dock drop target discovery

* fix: resolve late pane drop ownership

* test: cover Dock preview session round trip
2026-08-11 02:08:16 -07:00
Lawrence Chen 4a509fcc6a Publish session coordinator lock availability (#9940)
* test: cover coordinator release publication

* fix: publish session coordinator availability

* test: prove coordinator availability wakeup

* test: observe coordinator publication directly

* fix: recheck coordinator lock after missed signal

* fix: clean interrupted coordinator registrations

* fix: make coordinator release signal reliable

* Revert "fix: make coordinator release signal reliable"

This reverts commit 53a519d396.

* fix: publish coordinator release through fifo

* fix: publish only ready coordinator fifos
2026-08-11 02:08:13 -07:00
Lawrence Chen 21224fe770 Replace Ghostty helper test timing polls (#9941)
* test: replace Ghostty helper timing polls

* fix: keep desktop probe cleanup within deadline

* test: observe process-group child exits

* test: tolerate unavailable pidfd observers

* test: report unsupported pidfd coverage
2026-08-11 01:49:57 -07:00
lawrencecchen 7ca4cec15d Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-11 01:47:15 -07:00
Lawrence Chen 6969b53560 Merge pull request #9982 from manaflow-ai/fix-journal-subject-head-marker
test(tui): preserve exact journal subject marker
2026-08-11 01:46:03 -07:00
austinpower1258 7e567e43f3 Test window Dock manual unread toggles 2026-08-11 01:36:59 -07:00
austinpower1258 31602bb377 Merge remote-tracking branch 'origin/main' into issue-9869-dock-chromeless-browser 2026-08-11 01:24:04 -07:00
lawrencecchen b46724685d Test journal subject head with preserved marker 2026-08-11 01:19:37 -07:00
lawrencecchen e2852c493d Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-11 01:14:47 -07:00
austinpower1258 6f88cbdf61 Unify Dock and workspace unread toggles 2026-08-11 01:08:52 -07:00
austinpower1258 3af875b9e7 Test notification-derived Dock unread toggles 2026-08-11 00:48:35 -07:00
Abdulaziz Albahar cf291a4ac2 Fix iOS 27 keyboard dock tracking (#9958)
* test(ios): reproduce iOS 27 keyboard dock regression

* fix(ios): bypass broken iOS 27 keyboard guide

* Fix iOS 27 keyboard verification paths
2026-08-11 00:48:05 -07:00
austinpower1258 46653127d2 test: fix restored agent snapshot command assertion 2026-08-11 00:26:48 -07:00
lawrencecchen 70d80b174e Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-11 00:23:53 -07:00
austinpower1258 d66c3c2807 Merge remote-tracking branch 'origin/main' into issue-9869-dock-chromeless-browser 2026-08-11 00:16:25 -07:00
lawrencecchen d2f146744c test(tui): retain detached exit receipt in snapshot 2026-08-11 00:11:42 -07:00
Austin Wang 6edd0bec51 Fix literal tildes in terminal-inserted paths (#9734)
* test: cover literal tilde in inserted shell paths

* fix: escape literal tildes in terminal paths
2026-08-11 00:07:11 -07:00
Austin Wang ddfe3dcbfc Fix reentrant browser portal layout poisoning (#9773)
* test(browser): reproduce restored portal layout reentrancy

* test(browser): observe portal slot layout reentrancy

* test(browser): observe WebKit layout during portal refresh

* test(browser): force the outer portal layout probe

* test(browser): observe the forced portal layout boundary

* fix(browser): defer hosted WebKit presentation layout

* test(browser): preserve inspector width after reattach

* test(browser): await deferred portal refresh turns

* fix(browser): restore inspector after deferred reattach

* test(browser): isolate portal layout probe

* test(browser): observe WebKit flush without debug hook

* test(browser): cover anchor geometry after reparent

* fix(browser): let portal anchor own geometry updates
2026-08-11 00:06:43 -07:00
bcfb2d7b0b Portal: invalidate the split-divider hit-test cache on nested subview insertion (#8580)
* portal: invalidate the split-divider hit-test cache on nested subview insertion

The split-divider region cache trusted KVO of NSView.subviews to catch structural
changes, but addSubview does not reliably emit that KVO across macOS versions, so a
split view inserted into a nested container left the cache stale and hit-testing wrong.
Replace the root-only subview-id check with a structure fingerprint over all observed
views (root, its subviews, split ancestors), recomputed on the lookup path; the KVO
observers stay as an eager fast path but correctness no longer depends on them. Same
fix in the Browser portal, which duplicated the cache.

* portal: compare structure snapshots without intermediate arrays

* portal: cover deep-container split insertion and content-root replacement

The divider cache's structure snapshots only track the content root, its direct
children, and views that were split-related when the cache warmed up. Two gaps:
a split inserted under a container two levels deep changes no observed subview
list, and a replaced-but-still-alive content root passes validation because no
snapshot records which root it was built from. Both leave hit-testing on stale
empty regions. Failing tests first; the fix lands in the next commit.

* portal: validate the divider cache with a root-keyed full-tree split digest

The structure snapshots only covered the content root, its direct children,
and views that were split-related when the cache warmed, so a split inserted
under a deeper container changed no recorded subview list and the stale cache
kept winning. They also never recorded which root they were built from, so a
replaced-but-still-alive content root passed validation against the detached
tree. Replace the snapshots with a digest keyed to the root's identity that a
full-tree walk rebuilds on the lookup path: each split's identity, ancestor
chain, arranged subviews, orientation, and effective visibility. An insertion
under any container now misses the cache, while subview churn that cannot
affect dividers still reuses it, and the subviews KVO stays bounded to the
same views as before. Both portals share the digest through
PortalSplitDividerRegion.

* portal: prove cache hits skip large hierarchy traversal

* portal: invalidate divider caches at hierarchy mutations

* portal: scope hierarchy invalidation to cache roots

* portal: nest cache invalidation helpers

* portal: give hierarchy mutation routing one owner

* portal: hook every hierarchy mutation entrypoint

* test: align OMP resume environment expectation

* test: keep install command fixture inert

* test: cover app-host config path aliases

* ci: canonicalize app-host config evidence paths

* fix: resolve Xcode 26.3 Swift warnings

* fix: preserve Xcode 16 delegate compatibility

* test: bound portal hierarchy mutation hook work

* portal: make hierarchy invalidation generation-based

* test: cover no-op sorts and detached portal roots

* portal: preserve cache validity across sort and reattach

* test: cover detached nested portal subtree mutation

* test: calibrate portal mutation work against AppKit

* portal: distrust detached subtree hierarchy state

* fix: fail closed when portal hierarchy hooks are unavailable

* test: drop stale OMP path override from portal branch

* test: cover split-free subviews replacement

* portal: preserve split-free subviews fast path

* test: validate every warmed portal cache

* test: cover arranged split pane mutations

* portal: track arranged split pane mutations

* test: cover detached subtree parking path

* portal: revoke proofs at unindexed hierarchy boundaries

* test: isolate arranged pane mutation cases

* test: cover wrapped detached portal subtree

* portal: track detached indexed subtree mutations

* portal: keep mutation snapshots with their owner

* test: cover inactive portal proof lifetime

* portal: revoke proofs between cache lifetimes

* test: avoid arranged-pane teardown KVO trap

* test: cover detached portal reorder mutations

* test: restore split arrangement before teardown

* test: document detached cache boundary

* test: use valid arranged-pane lifecycles

---------

Co-authored-by: ejc3 <[email protected]>
Co-authored-by: austinpower1258 <[email protected]>
2026-08-11 00:05:53 -07:00
Austin Wangandcmux reload-cloud 2505c9bc5a Add group-scoped workspace cycle actions (#9352)
* test: cover group-scoped workspace cycling

* feat: add group-scoped workspace cycle actions

* test: compile mutating boundary assertions

---------

Co-authored-by: cmux reload-cloud <[email protected]>
2026-08-11 00:05:12 -07:00
Austin Wang 124917001f Keep cmux's bundled open shim first on PATH (#9781)
* test: reproduce cmux open PATH loss without Ghostty helper

* fix: keep bundled open first without Ghostty helper

* test: cover bundled bin in wrapper PATH invariant

* test: let fish PATH regression exit deterministically

* test: keep fish PATH coverage optional locally
2026-08-11 00:04:24 -07:00
Austin Wang 4704a5b43e Fix sidebar workspace-switch layout reentry (#9854)
* test: reproduce workspace switch layout reentry (#9612)

* fix: keep terminal portal layout within owned hierarchy (#9612)

* ci: enforce workspace-switch layout regression (#9612)

* fix: stop workspace model from forcing window layout (#9612)

* debug: capture workspace-switch layout reentry stack (#9612)

* fix: reconcile terminal portals outside view callbacks (#9612)

* fix: isolate terminal portal coordinator to main actor (#9612)

* cleanup: remove disproven layout experiments (#9612)

* test: reproduce per-pane portal layout fanout (#9612)

* fix: coalesce portal bind layout convergence (#9612)

* fix: address portal reconciliation review feedback (#9612)

* fix: make layout log helpers explicitly nonisolated (#9612)

* fix: isolate portal vacancy retry trampoline (#9612)

* test: reproduce detached portal visibility drift (#9612)

* fix: preserve detached portal visibility intent (#9612)

* test: make layout log sentinel polling lossless (#9612)

* test: distinguish detached visibility from geometry hide (#9612)

* test: drain detached reconciliation before portal pruning (#9612)
2026-08-11 00:01:24 -07:00
austinpower1258 869ba5d7c7 Isolate Dock socket test helper to main actor 2026-08-10 23:47:49 -07:00
lawrencecchen d12a942ab7 test(tui): preserve explicit close tombstone across restart 2026-08-10 23:24:25 -07:00
austinpower1258 a0cccf6c49 Fix Dock browser callback test typing 2026-08-10 23:21:32 -07:00
austinpower1258 c80f74a6aa Use Swift Testing for Dock omnibar coverage 2026-08-10 23:03:32 -07:00
austinpower1258 3b27a684c8 Remove Dock socket test seam 2026-08-10 22:59:49 -07:00
austinpower1258 9477539657 Split browser parity value types 2026-08-10 22:53:01 -07:00
lawrencecchen 65dc8ab300 fix(tui): bump lifecycle readiness protocol to 12 2026-08-10 22:49:57 -07:00
austinpower1258 70e0740522 Fix merged test API drift 2026-08-10 22:44:56 -07:00
lawrencecchen badbd88db6 test(tui): cover lifecycle-ready protocol advertisement 2026-08-10 22:28:54 -07:00
lawrencecchen eac01358e9 test(tui): require lifecycle readiness protocol bump 2026-08-10 22:26:50 -07:00
austinpower1258 79fbedaa1b Fix Dock browser parity test compilation 2026-08-10 22:26:49 -07:00
austinpower1258 95d49aa687 Fix Dock menu routing compilation 2026-08-10 22:06:34 -07:00
lawrencecchen 236f57ff60 Merge origin/main into task-cli-lifecycle-grammar 2026-08-10 22:01:35 -07:00
lawrencecchen 4a3bf32e8d fix(tui): reconcile terminal close replay revisions 2026-08-10 22:01:26 -07:00
austinpower1258 36e4237f99 Merge remote-tracking branch 'origin/main' into issue-9869-dock-chromeless-browser 2026-08-10 21:55:06 -07:00
Austin Wang d64a84ac63 Keep bracketed paste framing atomic (#9875)
* Add failing atomic paste regression

* Keep bracketed paste framing atomic

* Pin GhosttyKit for atomic paste fix

* Pin combined GhosttyKit artifact

* Pin minimal compatible Ghostty merge

* Document current atomic paste integration

* Clarify active Ghostty integration pin
2026-08-10 21:53:40 -07:00
austinpower1258 27c3bd5713 Fix Dock browser parity compilation 2026-08-10 21:51:39 -07:00
austinpower1258 8a08866c7e Merge remote-tracking branch 'origin/main' into issue-9920-workitem-chain-producer 2026-08-10 21:50:15 -07:00
austinpower1258 6c9e014d2b Merge remote-tracking branch 'origin/main' into issue-9869-dock-chromeless-browser
# Conflicts:
#	Sources/DockSplitStore+Reset.swift
2026-08-10 21:32:11 -07:00
Austin Wang a489eec3ea Merge pull request #9924 from manaflow-ai/issue-9923-vault-resume-restore-verb
Route Vault resume through cmux restore
2026-08-10 21:13:47 -07:00
lawrencecchen 69995dd2fd test(tui): correct restart terminal snapshot expectation 2026-08-10 21:10:29 -07:00
austinpower1258 86f1cd7939 Restore current Ghostty pointer after main merge 2026-08-10 20:47:24 -07:00
austinpower1258 27adb030ef Merge remote-tracking branch 'origin/main' into issue-9869-dock-chromeless-browser
# Conflicts:
#	Sources/DockSplitStore+SessionRestore.swift
#	Sources/DockSplitStore+SessionSnapshot.swift
#	Sources/DockSplitStore+SurfaceTransfer.swift
#	Sources/DockSplitStore.swift
#	Sources/TerminalController.swift
#	cmuxTests/DockControlDefinitionDecodingTests.swift
2026-08-10 20:45:32 -07:00
austinpower1258 048dd603c4 fix: avoid cross-isolation browser detector capture 2026-08-10 20:45:11 -07:00
austinpower1258 6c194c4e17 fix: unify restored-agent cwd lifecycle 2026-08-10 20:42:49 -07:00
austinpower1258 85f460a7a1 test: cover Vault restore cwd lifecycle 2026-08-10 20:33:58 -07:00
austinpower1258 11b8615812 Give Dock browsers main-pane feature parity 2026-08-10 20:32:14 -07:00
austinpower1258 45287586b1 fix: close deferred handle audit gaps 2026-08-10 20:21:19 -07:00
lawrencecchen 0f77254aef style(tui): apply hosted lifecycle rustfmt 2026-08-10 20:17:30 -07:00
austinpower1258 78b2492d2e test: cover Vault restore fallback and placement 2026-08-10 20:13:43 -07:00
austinpower1258 59dab37dfa Merge remote-tracking branch 'origin/main' into issue-9923-vault-resume-restore-verb 2026-08-10 20:13:40 -07:00
austinpower1258 b9a2907a6a Merge remote-tracking branch 'origin/main' into issue-9920-workitem-chain-producer 2026-08-10 20:11:43 -07:00
lawrencecchen 9c018f9b17 Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-10 20:10:20 -07:00
austinpower1258 7c866e496c refactor: quarantine Vault copy resume commands 2026-08-10 20:10:17 -07:00
austinpower1258 76bb995e57 test: cover deferred handle containers 2026-08-10 20:10:09 -07:00
austinpower1258 8935a7b69e fix: persist Vault restores in agent lifecycle 2026-08-10 20:10:05 -07:00
lawrencecchen 58c17ceb37 fix(tui): localize stopped-owner reload failures 2026-08-10 20:10:00 -07:00
lawrencecchen 48f6bac78d style(tui): format lifecycle regression coverage 2026-08-10 19:54:19 -07:00
lawrencecchen 901c56ce1e test(tui): expose stopped-owner reload localization 2026-08-10 19:50:19 -07:00
austinpower1258 28638efb92 test: cover Vault restore persistence lifecycle 2026-08-10 19:49:33 -07:00
lawrencecchen 5db9360501 Revert "fix(tui): close lifecycle review gaps"
This reverts commit f46ca1176f.
2026-08-10 19:49:00 -07:00
lawrencecchen f46ca1176f fix(tui): close lifecycle review gaps 2026-08-10 19:44:58 -07:00
austinpower1258 814ebd6e0c fix: close detached task installation race 2026-08-10 19:39:50 -07:00
Austin Wang 61ca64f534 Merge pull request #9776 from manaflow-ai/issue-9466-stage-manager-raise
Keep background attention from raising Stage Manager windows
2026-08-10 19:39:25 -07:00
lawrencecchen 7c14c57456 fix(tui): own pending host cleanup guard 2026-08-10 19:37:32 -07:00
austinpower1258 7e5cea062b test: strengthen Bonsplit teardown coverage 2026-08-10 19:32:07 -07:00
lawrencecchen c3c3d0cdcf style(tui): apply hosted rustfmt 2026-08-10 19:30:25 -07:00
austinpower1258 84ad00f525 fix: make task store teardown synchronous 2026-08-10 19:30:18 -07:00
lawrencecchen 4c441f6d91 fix(tui): cover running host publish window 2026-08-10 19:23:40 -07:00
austinpower1258 ab3a92cc3b fix: isolate macOS SwiftUI deferred state 2026-08-10 19:21:32 -07:00
lawrencecchen 6faa8b14a1 fix(tui): keep pending host storage portable 2026-08-10 19:19:24 -07:00
lawrencecchen fa363ef2a8 fix(tui): bind pending host callbacks exactly 2026-08-10 19:14:57 -07:00
austinpower1258 03f104f695 test: audit macOS SwiftUI deferred state 2026-08-10 19:10:00 -07:00
austinpower1258 b8889ba36a fix: isolate deferred work ownership 2026-08-10 19:05:29 -07:00
lawrencecchen a7252fdebf style(tui): apply hosted rustfmt 2026-08-10 18:50:50 -07:00
lawrencecchen db312c6297 fix(tui): accept reconnect before topology binding 2026-08-10 18:45:21 -07:00
lawrencecchen b093ca6475 Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-10 18:17:32 -07:00
Abdulaziz Albahar 4c28ef112a Explain Auto-Connect once to migrated iOS users (#9891)
* test: cover Auto-Connect migration introduction

* feat(ios): explain Auto-Connect migration

* fix(ios): isolate Auto-Connect migration state

* fix(ios): unify root modal presentation

* fix(ios): harden migration notice presentation

* fix(ios): coordinate migration with shell modals

* fix(ios): preserve child presenter identity

* test(ios): cover Auto-Connect migration outcomes

* fix(ios): unify root Settings presentation

* fix(ios): root-host shell Settings

* fix(ios): advance queued migration in root sheet

* fix(ios): preserve modal action ownership

* test(ios): require transactional modal acquisition

* fix(ios): acquire modal before side effects

* test(ios): require fitted migration sheet

* fix(ios): fit migration sheet to content

* fix(ios): let migration content size sheet

* fix(ios): measure migration sheet detent

* fix(ios): isolate migration accessibility containers

* fix(ios): decouple migration accessibility from layout

* test(ios): add migration viewport probe

* test(ios): ignore viewport probe hittability

* fix(ios): bound migration scroll viewport

* test(ios): verify migration visibility by frame

* test(ios): require migration title visibility

* test(ios): cap migration probe to app viewport

* test(ios): cover Japanese migration landscape action

* fix(ios): use native migration sheet sizing
2026-08-10 18:09:24 -07:00
austinpower1258 5661b21342 test: audit deferred work ownership 2026-08-10 18:06:01 -07:00
lawrencecchen 643939b082 style(tui): apply hosted rustfmt 2026-08-10 18:05:45 -07:00
Lawrence Chen bb5623585a Finish CodeRouter organization UX and analytics operations (#9882)
* Test dashboard organization switching

* Add direct dashboard organization switching

* Test CodeRouter metrics failure alerting

* Alert on CodeRouter analytics query failures

* Test authorized CodeRouter organization discovery

* Expose authorized CodeRouter organizations

* Test permission-filtered dashboard organizations

* Filter dashboard organization switching by permission

* Test CodeRouter review regressions

* Fix CodeRouter organization navigation and failure states

* Test CodeRouter-only Team pricing benefit

* Test removal of unshipped Team benefits

* Test inherited Team cloud allowance

* Limit Team pricing claims to shipped CodeRouter

* Test final CodeRouter review regressions

* Preserve cached orgs and validate analytics JSON

* Test organization cache and response isolation

* Isolate and validate organization catalog state

* Test personal organization selection

* Preserve personal organization selection

* Test URL-scoped permitted organizations

* Scope the switcher to URL-permitted organizations

* Test authoritative organization fallback

* Use the authoritative selected CodeRouter organization

* Test live and personal organization selection

* Use live Stack organization selection

* Test organization cache refresh and switch failure

* Reconcile and report organization switches

* Test filtered selected organization recovery

* Test dashboard Team benefit copy

* Bound organization loading and correct Team upsell

* Test authored pricing locale boundary

* Keep organization authority current

* Test keyboard placement and normalized team scope

* Make organization switching keyboard reachable

* Test concurrent organization switch guard

* Serialize organization switches

* Test empty organization catalog recovery

* Recover from an empty organization catalog

* Update billing copy expectations

* Test organization network and update deadlines

* Bound organization query and update waits

* Keep timed-out organization mutations exclusive

* Test latest organization switch serialization

* Queue organization switches behind active mutations

* Offer safe reload recovery for stuck switches

* Cancel organization switch effects on unmount

* Reconcile failed queued organization switches

* Discard queued switches after a deadline

* Reconcile late switches without replacing navigation

* Make URL scope authoritative during organization switches

* Keep timed-out persistence safely blocked

* Release timed-out switches with late reconciliation

* Test analytics failure classes and account clearance

* Fix account layout and analytics failure handling

* Persist CodeRouter scope independently of Stack

* Scope CodeRouter organization preference per user
2026-08-10 18:02:54 -07:00
lawrencecchen 151a48c688 test(tui): accept oversized-frame peer close 2026-08-10 17:54:05 -07:00
austinpower1258 e78fe95df4 Test workspace Dock browser socket routing 2026-08-10 17:42:23 -07:00
austinpower1258 63f31dcbae fix: route Vault resume through restore verb 2026-08-10 17:39:44 -07:00
lawrencecchen 5a636ace9e fix(tui): release unregistered Kitty quota 2026-08-10 17:30:00 -07:00
austinpower1258 af914dd3a8 test: start deferred Dock surface before input 2026-08-10 17:26:57 -07:00
lawrencecchen 5dab53fca2 fix(tui): retire unregistered Kitty surfaces 2026-08-10 17:26:17 -07:00
austinpower1258 48a6f727fd test: identify virtual mobile emission delays 2026-08-10 17:22:11 -07:00
austinpower1258 5be58f99c0 test: stabilize accepted Dock key input 2026-08-10 17:07:30 -07:00
austinpower1258 bfcfac8530 test: cover Vault restore-verb resume path 2026-08-10 17:02:27 -07:00
lawrencecchen 74ea49b4af test(tui): close Kitty runtimes explicitly 2026-08-10 16:43:15 -07:00
austinpower1258 5b714d679b fix: preserve mobile emission leading edge 2026-08-10 16:31:12 -07:00
austinpower1258 8c3c047dee test: require immediate mobile burst leading edge 2026-08-10 16:28:00 -07:00
austinpower1258 372ec7385c fix: coalesce mobile workspace emissions 2026-08-10 16:14:56 -07:00
austinpower1258 32822bf5dd Test Dock browser tab context routing 2026-08-10 16:10:32 -07:00
austinpower1258 2ccdd98d71 fix: preserve optional mobile unread injection 2026-08-10 15:59:35 -07:00
lawrencecchen 81aca8f6a3 test(tui): remove shutdown scheduler race 2026-08-10 15:55:02 -07:00
austinpower1258 26a34856e2 test: cap mobile summary burst emissions 2026-08-10 15:50:59 -07:00
austinpower1258 d11b5ad95c refactor: inject mobile workspace emission dependencies 2026-08-10 15:50:05 -07:00
lawrencecchen 87ab1555fb Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-10 15:36:19 -07:00
lawrencecchen ddfc6f799a style(tui): format stop completion coverage 2026-08-10 15:36:02 -07:00
lawrencecchen 88c49d1f48 fix(tui): preserve authoritative stop completion 2026-08-10 15:32:28 -07:00
lawrencecchen 5a4cfaf376 test(tui): preserve authoritative stop completion 2026-08-10 15:31:42 -07:00
austinpower1258 79dfe0f593 Merge remote-tracking branch 'origin/main' into issue-9466-stage-manager-raise 2026-08-10 15:28:37 -07:00
Lawrence Chen 621164d1f3 Make Windows state sync portable and terminal close atomic (#9915)
* Use portable state directory sync

* Close terminal host and resource atomically

* Satisfy hosted terminal close Clippy

* Test replayed terminal close isolation

* Keep terminal close replays effect-free

* Close durable terminals without runtimes

* Resolve terminal close replay before live state

* Hide terminal close state internals

* Test resource replay close isolation

* Keep resource close replays effect-free

* Fix terminal replay test identity borrows

* Tombstone detached terminals on explicit close
2026-08-10 15:24:54 -07:00
austinpower1258 784841e58d Test Dock browser routing after focus moves 2026-08-10 15:15:15 -07:00
austinpower1258 6af4abd181 fix: distinguish Dock reveal failures 2026-08-10 15:15:08 -07:00
austinpower1258 69c1b1d6f9 test: distinguish Dock reveal failures 2026-08-10 15:13:38 -07:00
austinpower1258 c1c03675a3 fix: preserve accepted input semantics 2026-08-10 15:13:13 -07:00
lawrencecchen 400fbaecf4 test(tui): use the lifecycle mux fixture directly 2026-08-10 15:10:22 -07:00
lawrencecchen ceabe59232 fix(tui): close final lifecycle review gaps 2026-08-10 14:58:54 -07:00
lawrencecchen a4370ff107 test(tui): cover final lifecycle review gaps 2026-08-10 14:43:03 -07:00
lawrencecchen 8570beb24b test(tui): accept indeterminate key delivery 2026-08-10 14:34:50 -07:00
lawrencecchen 4d0b246a6f fix(tui): preserve canonical CLI usage errors 2026-08-10 14:33:18 -07:00
lawrencecchen 54b2e0ef43 test(tui): wait for owner lifecycle readiness 2026-08-10 14:09:57 -07:00
lawrencecchen db9adeff4a fix(tui): satisfy lifecycle clippy gate 2026-08-10 13:50:56 -07:00
austinpower1258 1cf62495dd test: cover rejected explicit input attention paths 2026-08-10 13:49:07 -07:00
lawrencecchen f489d75107 fix(tui): preserve lifecycle JSON error messages 2026-08-10 13:42:01 -07:00
austinpower1258 ae654d497f refactor: centralize restored agent attention cleanup 2026-08-10 13:40:56 -07:00
austinpower1258 95269fc0a2 refactor: derive mobile previews from unread snapshots 2026-08-10 13:40:14 -07:00
austinpower1258 a9b7f8aa03 Test Dock browser focus precedence 2026-08-10 13:35:48 -07:00
austinpower1258 b6bd69f149 Merge remote-tracking branch 'origin/main' into issue-9466-stage-manager-raise
# Conflicts:
#	Sources/DockSplitStore+RestoredAgentLifecycle.swift
#	Sources/DockSplitStore+SessionRestore.swift
#	Sources/DockSplitStore+SessionSnapshot.swift
#	Sources/DockSplitStore+SurfaceTransfer.swift
#	Sources/DockSplitStore.swift
2026-08-10 13:31:42 -07:00
austinpower1258 eccd9a4155 fix: preserve explicit input attention ownership 2026-08-10 13:23:53 -07:00
lawrencecchen e06e93d7e9 test(tui): use current lifecycle resource protocol 2026-08-10 13:21:52 -07:00
lawrencecchen a4db2d196c test(tui): preserve lifecycle JSON errors 2026-08-10 12:59:29 -07:00
austinpower1258 17c4938616 ci: retain app-hosted Swift test counts 2026-08-10 12:59:26 -07:00
lawrencecchen ff5ae4206d style(tui): format lifecycle response handling 2026-08-10 12:52:05 -07:00
lawrencecchen 8aa038b557 fix(tui): validate lifecycle response contracts 2026-08-10 12:47:16 -07:00
lawrencecchen 127d6732d4 test(tui): cover lifecycle response review gaps 2026-08-10 12:26:11 -07:00
lawrencecchen 2d7c6aff44 fix(tui): preserve relay helper argument literals 2026-08-10 12:13:29 -07:00
lawrencecchen 35c79f35ae style(tui): format relay helper regression 2026-08-10 11:53:29 -07:00
lawrencecchen 9f6e143215 test(tui): preserve relay helper argument literals 2026-08-10 11:49:36 -07:00
lawrencecchen 67becf8434 fix(tui): close lifecycle review gaps 2026-08-10 11:42:37 -07:00
lawrencecchen 8362055f97 test(tui): cover remaining lifecycle review gaps 2026-08-10 11:20:25 -07:00
lawrencecchen e5c3b310fa fix(tui): keep daemon handoff ack-ordered 2026-08-10 11:08:51 -07:00
lawrencecchen c792e7e948 fix(tui): preserve shared mux shutdown access 2026-08-10 10:52:19 -07:00
lawrencecchen 328af43cfd Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar
# Conflicts:
#	cmux-tui/bindings/cpp/.cmux-sdk-manifest.json
#	cmux-tui/bindings/cpp/include/cmux/raw/generated/models.hpp
#	cmux-tui/bindings/go/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/go/raw/generated_commands.go
#	cmux-tui/bindings/go/raw/generated_events.go
#	cmux-tui/bindings/go/raw/generated_metadata.go
#	cmux-tui/bindings/go/raw/generated_presence_test.go
#	cmux-tui/bindings/go/raw/generated_types.go
#	cmux-tui/bindings/java/src/com/cmux/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/java/src/com/cmux/raw/Protocol.java
#	cmux-tui/bindings/python/cmux/raw/_generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/python/cmux/raw/_generated/_schema.py
#	cmux-tui/bindings/python/cmux/raw/_generated/metadata.py
#	cmux-tui/bindings/rust/src/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/rust/src/generated/commands.rs
#	cmux-tui/bindings/rust/src/generated/events.rs
#	cmux-tui/bindings/rust/src/generated/metadata.rs
#	cmux-tui/bindings/rust/src/generated/mod.rs
#	cmux-tui/bindings/rust/src/generated/types.rs
#	cmux-tui/bindings/typescript/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/typescript/src/raw/generated/commands.ts
#	cmux-tui/bindings/typescript/src/raw/generated/events.ts
#	cmux-tui/bindings/typescript/src/raw/generated/index.ts
#	cmux-tui/bindings/typescript/src/raw/generated/metadata.ts
#	cmux-tui/bindings/typescript/src/raw/generated/types.ts
#	cmux-tui/bindings/zig/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/zig/src/raw/generated/protocol.zig
#	cmux-tui/crates/cmux-tui-core/src/mux.rs
#	cmux-tui/crates/cmux-tui-core/src/server.rs
#	cmux-tui/crates/cmux-tui/src/app.rs
#	cmux-tui/crates/cmux-tui/src/cli.rs
#	cmux-tui/crates/cmux-tui/src/cli/command.rs
#	cmux-tui/crates/cmux-tui/src/main.rs
#	cmux-tui/spec/cli.md
2026-08-10 10:43:58 -07:00
lawrencecchen f182f3a016 fix(tui): preserve resource option values 2026-08-10 10:30:56 -07:00
lawrencecchen 9d87c3b5a5 style(tui): format resource value regression 2026-08-10 10:13:19 -07:00
lawrencecchen f0c2f7aeb9 test(tui): preserve relay literals in resource values 2026-08-10 10:07:17 -07:00
Lawrence Chen ada6abce88 Merge pull request #9726 from manaflow-ai/codex/cmux-browser-provider
cmux-tui: integrate journaling with the native browser provider
2026-08-10 10:04:18 -07:00
lawrencecchen 9f818adaf5 fix(tui): close lifecycle routing gaps 2026-08-10 09:40:54 -07:00
lawrencecchen abc85ab064 test(tui): cover paused resource lifecycle routes 2026-08-10 09:21:29 -07:00
lawrencecchen 80c70ba26e test(tui): await exact descendant readiness 2026-08-10 08:38:31 -07:00
lawrencecchen 83246496ce fix(tui): keep relay validation outside resource inventory 2026-08-10 08:37:03 -07:00
lawrencecchen 8c27a7e78c style(tui): use imported io error 2026-08-10 07:53:47 -07:00
lawrencecchen e939423128 test(tui): accept completed descendant cleanup 2026-08-10 07:42:58 -07:00
lawrencecchen 34ea6cc32e fix(tui): reject relay tickets before help 2026-08-10 07:41:45 -07:00
lawrencecchen 497dd98555 test(tui): match macOS hook fork denial 2026-08-10 07:14:09 -07:00
lawrencecchen a197119f6a style(tui): format reload completion errors 2026-08-10 06:48:20 -07:00
lawrencecchen d9f93ce0f9 fix(tui): make owner reload completion authoritative 2026-08-10 06:35:59 -07:00
lawrencecchen 7408018f9e style(tui): apply hosted producer rustfmt 2026-08-10 06:33:35 -07:00
lawrencecchen ca6971c881 test(tui): prove producer deadline rollback 2026-08-10 06:20:35 -07:00
lawrencecchen f221b49e25 test(tui): assert admitted journal identity 2026-08-10 06:18:23 -07:00
lawrencecchen 3ff17062c4 style(tui): format reload ownership test 2026-08-10 06:16:50 -07:00
lawrencecchen 99db4f0481 test(tui): expose reload ownership races 2026-08-10 06:10:35 -07:00
lawrencecchen 23bd7e5c92 fix(tui): publish lifecycle readiness in SDK schema 2026-08-10 05:58:37 -07:00
lawrencecchen 252eccb7da test(tui): activate checkpoint ingress fence 2026-08-10 05:58:18 -07:00
lawrencecchen db46de7776 style(tui): format lifecycle identify tests 2026-08-10 05:41:10 -07:00
lawrencecchen 508bffabcd fix(tui): publish lifecycle owner readiness 2026-08-10 05:37:10 -07:00
lawrencecchen e2cea2560a style(tui): apply hosted process fence rustfmt 2026-08-10 05:30:38 -07:00
lawrencecchen ae67462a7c fix(tui): close hosted process fence races 2026-08-10 05:22:58 -07:00
lawrencecchen 5f80c59eb9 style(tui): format lifecycle regression tests 2026-08-10 05:02:24 -07:00
lawrencecchen e09baa7ccd test(tui): cover lifecycle readiness receipts 2026-08-10 04:53:05 -07:00
lawrencecchen a9e6efc583 test(tui): use one-shot mac fork probe 2026-08-10 04:43:58 -07:00
lawrencecchen e63cd0b740 fix(tui): close exact-head lifecycle findings 2026-08-10 04:41:35 -07:00
lawrencecchen 9a99daa72a test(tui): repair lifecycle readiness harness 2026-08-10 04:29:14 -07:00
lawrencecchen 30ebe1c089 test(tui): bound mac process fence proof 2026-08-10 04:28:53 -07:00
lawrencecchen d2384c596c fix(tui): expose atomic handoff commit internally 2026-08-10 04:17:29 -07:00
lawrencecchen 655ca1401b style(tui): apply hosted lifecycle rustfmt 2026-08-10 04:09:09 -07:00
lawrencecchen dbaaee1817 test(tui): cover final lifecycle review gaps 2026-08-10 04:01:51 -07:00
lawrencecchen 78681b5e36 fix(tui): synchronize server lifecycle readiness 2026-08-10 03:54:40 -07:00
lawrencecchen 8a1ca9ae32 test(tui): cover lifecycle completion races 2026-08-10 03:49:10 -07:00
lawrencecchen 3d43760a69 fix(tui): prioritize inline relay secret rejection 2026-08-10 03:37:53 -07:00
lawrencecchen 849f9d86a6 Merge remote-tracking branch 'origin/main' into codex/cmux-browser-provider 2026-08-10 03:33:48 -07:00
lawrencecchen 4f63aa20e2 fix(tui): close hosted root verification gaps 2026-08-10 03:33:31 -07:00
lawrencecchen 23db759f8e Merge current main into task-cli-lifecycle-grammar 2026-08-10 02:38:10 -07:00
lawrencecchen 0968d0bdfe test(tui): harden lifecycle failure harness 2026-08-10 02:36:48 -07:00
lawrencecchen 95873a15d7 test(tui): qualify owner reload worker 2026-08-10 02:27:21 -07:00
Lawrence Chen cd15399d6c Merge pull request #9908 from manaflow-ai/fix-tui-windows-rustfmt
Apply Rust formatting to Windows cmux-tui fixes
2026-08-10 02:25:52 -07:00
lawrencecchen 911f16df14 style(tui): apply hosted rustfmt 2026-08-10 01:41:53 -07:00
lawrencecchen fbb2a1c405 style(tui): apply Rust formatting to Windows fixes 2026-08-10 01:28:55 -07:00
lawrencecchen 09974bab6a Merge remote-tracking branch 'origin/main' into codex/cmux-browser-provider 2026-08-10 01:15:53 -07:00
lawrencecchen 685d6538b1 Merge current main into task-cli-lifecycle-grammar 2026-08-10 01:09:57 -07:00
lawrencecchen e79a34bb5e fix(tui): close lifecycle startup races 2026-08-10 01:06:55 -07:00
lawrencecchen 7d6ba0868c style(tui): apply hosted long-path rustfmt 2026-08-10 01:01:20 -07:00
Lawrence Chen e49e7cdf30 Merge pull request #9904 from manaflow-ai/test-tui-windows-conpty-resize
Add valid Windows ConPTY resize coverage
2026-08-10 00:50:36 -07:00
lawrencecchen 9d79d919c8 test(tui): cover lifecycle startup races 2026-08-10 00:32:14 -07:00
lawrencecchen a4d3865ee6 ci(tui): use available hosted Windows Python 2026-08-10 00:06:46 -07:00
lawrencecchen 458160affa Merge remote-tracking branch 'origin/main' into codex/cmux-browser-provider 2026-08-10 00:01:54 -07:00
lawrencecchen 3c4d22c8b8 style(tui): apply hosted rustfmt 2026-08-09 23:48:15 -07:00
lawrencecchen 4ca8ca40b5 Merge remote-tracking branch 'origin/main' into test-tui-windows-conpty-resize
# Conflicts:
#	.github/workflows/cmux-tui-build-package.yml
2026-08-09 23:47:29 -07:00
Lawrence Chen 3e63708c1e Merge pull request #9906 from manaflow-ai/fix-tui-windows-long-state-path-v2
Support long Windows cmux-tui state paths
2026-08-09 23:46:45 -07:00
lawrencecchen 5cadd65aed style(tui): apply hosted Windows exit rustfmt 2026-08-09 23:42:52 -07:00
lawrencecchen df15557435 fix(tui): sanitize workspace state path errors 2026-08-09 23:33:42 -07:00
lawrencecchen 10d75472a8 docs(tui): correct raw protocol inventory 2026-08-09 23:27:40 -07:00
lawrencecchen dd75b8f61b fix(tui): close lifecycle review gaps 2026-08-09 23:26:54 -07:00
lawrencecchen 62068dd8a8 test(tui): validate Windows workspace list output 2026-08-09 23:25:23 -07:00
lawrencecchen 8a5abc14ae test(tui): cover lifecycle closeout review paths 2026-08-09 23:24:20 -07:00
lawrencecchen 70b8d86390 test(tui): harden Windows long-path smoke 2026-08-09 23:22:30 -07:00
lawrencecchen 7cc93320ff Merge origin/main into codex/cmux-browser-provider 2026-08-09 23:16:26 -07:00
lawrencecchen 7c43264f53 Merge current main into task-cli-lifecycle-grammar 2026-08-09 23:13:49 -07:00
lawrencecchen aee20dcdce fix(tui): use SQLite long-path VFS on Windows 2026-08-09 23:13:43 -07:00
lawrencecchen 6e872e9072 fix(tui): harden lifecycle shutdown output 2026-08-09 23:12:13 -07:00
lawrencecchen 8b3ab77560 test(tui): cover SQLite long VFS byte boundary 2026-08-09 23:11:56 -07:00
lawrencecchen 1d34506b10 test(tui): cover lifecycle closeout edge cases 2026-08-09 23:09:59 -07:00
lawrencecchen 239552386c Merge remote-tracking branch 'origin/main' into fix-tui-windows-long-state-path-v2
# Conflicts:
#	.github/workflows/cmux-tui-build-package.yml
2026-08-09 23:05:46 -07:00
Lawrence Chen a35880b103 Merge pull request #9905 from manaflow-ai/fix-tui-windows-process-wait-v2
Fix Windows terminal exit publication
2026-08-09 23:05:17 -07:00
lawrencecchen 1e6179718c test(tui): make ConPTY smoke failures reliable 2026-08-09 23:02:32 -07:00
lawrencecchen 59ea6383c8 ci(tui): pin Windows ConPTY smoke dependencies 2026-08-09 23:00:15 -07:00
lawrencecchen 4f5e5e1f12 fix(tui): satisfy hosted owner lint 2026-08-09 23:00:02 -07:00
lawrencecchen 9a480effec fix(tui): keep owner shutdown bound across machines 2026-08-09 22:58:55 -07:00
lawrencecchen fdd00b22aa Merge remote-tracking branch 'origin/main' into codex/cmux-browser-provider 2026-08-09 22:54:48 -07:00
lawrencecchen 955909df09 style(tui): apply hosted browser rustfmt 2026-08-09 22:52:42 -07:00
lawrencecchen e498eff0d1 fix(tui): use extended Windows SQLite paths 2026-08-09 22:50:20 -07:00
lawrencecchen 7fdde87b32 test(tui): cover long Windows state paths 2026-08-09 22:50:20 -07:00
lawrencecchen b339743674 fix(tui): publish Windows terminal exits 2026-08-09 22:50:18 -07:00
lawrencecchen 047066d3f6 test(tui): cover Windows terminal process exit 2026-08-09 22:50:18 -07:00
lawrencecchen 9352051a73 test(tui): cover Windows ConPTY resize 2026-08-09 22:50:15 -07:00
lawrencecchen 3fcc3ef369 fix(tui): satisfy hosted shutdown checks 2026-08-09 22:49:02 -07:00
Lawrence Chen be29dfa7c5 Merge pull request #9901 from manaflow-ai/fix-tui-windows-gnu-link
Fix Windows GNU cmux-tui linking with LLD
2026-08-09 22:48:24 -07:00
lawrencecchen b81620ae71 fix(tui): close journal and provider ownership 2026-08-09 22:42:52 -07:00
lawrencecchen da3cee0f11 style(tui): apply hosted rustfmt 2026-08-09 22:41:32 -07:00
lawrencecchen 9fe3649238 fix(cli): route start options by grammar 2026-08-09 22:36:38 -07:00
lawrencecchen b72d652a7c fix(cli): keep accepted shutdown fenced 2026-08-09 22:30:15 -07:00
lawrencecchen 1c8cb0a238 fix(cli): preserve start help during validation 2026-08-09 22:19:59 -07:00
lawrencecchen 121602ef74 fix(tui): borrow process scope program 2026-08-09 22:16:18 -07:00
lawrencecchen 5e2d5d2e01 fix(cli): close lifecycle review races 2026-08-09 22:15:31 -07:00
lawrencecchen c6b9d23bb9 test(cli): cover review lifecycle races 2026-08-09 22:12:07 -07:00
lawrencecchen 169a0c2f94 test(tui): move final journal test values 2026-08-09 22:04:39 -07:00
lawrencecchen 081ad824ac test(cli): import owner-loop deadline type 2026-08-09 22:04:00 -07:00
lawrencecchen e3df6e1aa0 refactor(cli): remove stale session binding 2026-08-09 22:02:03 -07:00
lawrencecchen 9cc12dc6d2 fix(cli): redact cross-session lifecycle errors 2026-08-09 21:57:36 -07:00
lawrencecchen 2aaa893f6f test(tui): correct shutdown completion channel 2026-08-09 21:57:17 -07:00
lawrencecchen 5d7248ed8e test(cli): keep cross-session errors private 2026-08-09 21:56:42 -07:00
lawrencecchen 9fda05344b Merge remote-tracking branch 'origin/main' into fix-tui-windows-gnu-link
# Conflicts:
#	.github/workflows/cmux-tui.yml
2026-08-09 21:54:22 -07:00
lawrencecchen 2e6f08f248 fix(cli): keep rejected relay tickets private 2026-08-09 21:52:15 -07:00
lawrencecchen 74705bedc7 test(cli): cover server start secret rejection 2026-08-09 21:51:08 -07:00
lawrencecchen 14c9184217 style(tui): apply hosted journal rustfmt 2026-08-09 21:49:13 -07:00
lawrencecchen c178ff48ef fix(cli): repair hosted lifecycle gates 2026-08-09 21:48:49 -07:00
lawrencecchen dfd8947e89 Merge remote-tracking branch 'origin/main' into codex/cmux-browser-provider
# Conflicts:
#	.github/workflows/cmux-tui.yml
2026-08-09 21:43:40 -07:00
lawrencecchen e8579cf232 fix(tui): close final journal ownership races 2026-08-09 21:40:58 -07:00
lawrencecchen 9ba9f6bbac test(tui): expose final journal ownership races 2026-08-09 21:39:26 -07:00
lawrencecchen 1754fff692 style(cli): apply hosted rustfmt output 2026-08-09 21:37:56 -07:00
lawrencecchen a20f6f677a test(cli): replace lifecycle readiness sleeps 2026-08-09 21:30:57 -07:00
lawrencecchen 1a4c8f09e0 fix(cli): repair lifecycle conformance build 2026-08-09 21:30:46 -07:00
lawrencecchen b28463485e test(tui): run GNU app smoke after link 2026-08-09 21:30:33 -07:00
lawrencecchen 1b0400aee6 Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/event_bus.rs
#	cmux-tui/crates/cmux-tui/tests/cli.rs
2026-08-09 21:28:04 -07:00
Lawrence Chen 1097eab636 Merge pull request #9837 from manaflow-ai/task-hosted-cmux-tui-verification
Add hosted cmux-tui verification
2026-08-09 21:26:58 -07:00
lawrencecchen a3cfc7fc0a Preserve terminals when closing views 2026-08-09 20:53:08 -07:00
lawrencecchen 259ef2e995 Merge remote-tracking branch 'origin/main' into fix-tui-windows-gnu-link 2026-08-09 20:51:47 -07:00
lawrencecchen 4008f042d6 Isolate cmux-tui core tests in hosted CI 2026-08-09 20:50:04 -07:00
lawrencecchen e8ce3f4c48 Merge remote-tracking branch 'origin/main' into codex/cmux-browser-provider 2026-08-09 20:46:26 -07:00
lawrencecchen 1be761a0eb fix(tui): fence journal shutdown ownership 2026-08-09 20:45:48 -07:00
lawrencecchen 766c8a11dc Fix terminal tombstone projections 2026-08-09 20:45:13 -07:00
lawrencecchen 29df8c1ae9 Merge remote-tracking branch 'origin/main' into task-hosted-cmux-tui-verification 2026-08-09 20:44:19 -07:00
lawrencecchen 454c0c8282 test(tui): run focused Windows GNU link probe 2026-08-09 20:42:43 -07:00
Lawrence Chen a5752bbed3 Brand account UI as cmux (#9833)
* Brand account UI as cmux

* Remove vendor term from account search metadata

* Revert "Remove vendor term from account search metadata"

This reverts commit 8d008f2514.
2026-08-09 20:42:05 -07:00
lawrencecchen ece3c26335 fix(tui): link Windows GNU gates with LLD 2026-08-09 20:40:23 -07:00
lawrencecchen 5de6ab3253 Merge remote-tracking branch 'origin/main' into task-hosted-cmux-tui-verification 2026-08-09 20:38:19 -07:00
lawrencecchen 35d4a9c6ec Complete hosted merge gate coverage 2026-08-09 20:36:42 -07:00
lawrencecchen b5649a0544 fix(tui): rescan GNU runtime after stack probe 2026-08-09 20:30:27 -07:00
lawrencecchen 8f61b66ccf test(tui): verify mac process launcher fence 2026-08-09 20:30:08 -07:00
Lawrence Chen dfe324d4cc Merge pull request #8991 from manaflow-ai/feat-tui-sdk-stack
Generate and verify seven cmux-tui SDKs
2026-08-09 20:26:44 -07:00
lawrencecchen a4b3be44cc Harden hosted verification checks 2026-08-09 20:21:04 -07:00
lawrencecchen d832d2597e ci(tui): prove process fences before shutdown 2026-08-09 20:20:23 -07:00
lawrencecchen 5c469e03d6 ci(tui): scope Windows GNU test linking 2026-08-09 20:19:30 -07:00
lawrencecchen 3a91bda158 ci(tui): run focused journal ownership tests 2026-08-09 20:10:52 -07:00
lawrencecchen c2cb2528d6 test(tui): cover mac process-scope fence 2026-08-09 20:08:18 -07:00
lawrencecchen f5cb93cdae Fix exact hosted verification gate 2026-08-09 20:07:44 -07:00
lawrencecchen 4a678e23e3 fix(tui): require GNU stack-probe runtime 2026-08-09 20:01:11 -07:00
lawrencecchen 1a0aeacbb6 Preserve stream read deadline across messages 2026-08-09 19:56:22 -07:00
lawrencecchen fafde2b494 test(tui): pass hook signal as owned descriptor 2026-08-09 19:54:33 -07:00
lawrencecchen 81e5677906 Merge remote-tracking branch 'origin/main' into closeout-pr9837-20260809
# Conflicts:
#	cmux-tui/crates/cmux-terminal-client/src/lib.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/tests.rs
#	cmux-tui/crates/cmux-tui/src/machine_agent/runtime.rs
#	cmux-tui/crates/cmux-tui/src/session/remote.rs
#	cmux-tui/crates/cmux-tui/tests/cli.rs
2026-08-09 19:49:59 -07:00
lawrencecchen 6e74c1cccd style(tui): apply hosted import format 2026-08-09 19:44:53 -07:00
lawrencecchen cef5714ae1 Avoid try-with-resources close warning 2026-08-09 19:44:09 -07:00
lawrencecchen 9a7d2eaa84 test(tui): require Windows GNU test linking 2026-08-09 19:42:23 -07:00
lawrencecchen 0f10feea9f test(tui): make journal event assertion borrow explicit 2026-08-09 19:40:49 -07:00
lawrencecchen b6dae65692 Clean up reader threads before readiness assertions 2026-08-09 19:40:42 -07:00
lawrencecchen f481c42d9a fix(tui): reconcile hosted journal test prerequisites 2026-08-09 19:39:23 -07:00
lawrencecchen 8841dfca00 fix(tui): remove stale hook writer import 2026-08-09 19:31:10 -07:00
lawrencecchen 32bb6d1c0e test(tui): satisfy hosted Rust loop lint 2026-08-09 19:24:29 -07:00
lawrencecchen 1b9a1ab9e9 Merge remote-tracking branch 'origin/main' into feat-tui-sdk-stack 2026-08-09 19:23:29 -07:00
lawrencecchen d44ed5228c fix(tui): keep hosted process tests warning-free 2026-08-09 19:16:42 -07:00
Lawrence Chen 8d9aaef703 Update Ghostty for correct VT cursor replay (#9877)
* Update Ghostty replay fix pin

* Pin GhosttyKit replay archive checksum
2026-08-09 19:14:55 -07:00
lawrencecchen c2e75449cd Merge remote-tracking branch 'origin/main' into feat-tui-sdk-stack 2026-08-09 19:08:06 -07:00
lawrencecchen 8f87e780d9 style(tui): apply hosted process-scope format 2026-08-09 19:06:09 -07:00
lawrencecchen 4699129769 fix(tui): reconcile hosted root prerequisites 2026-08-09 19:02:13 -07:00
Austin Wang 1d6910d87d Merge pull request #9754 from manaflow-ai/issue-9504-dragsplit-focused-cursor
Fix drag-to-split cursor focus reconciliation
2026-08-09 18:59:02 -07:00
lawrencecchen 7a3d00646d test(tui): cover process and writer shutdown escapes 2026-08-09 18:58:54 -07:00
Austin Wang bf903a0b28 Merge pull request #9358 from manaflow-ai/issue-9220-sidebar-click-hang
Fix sidebar-triggered main-actor terminal teardown hang
2026-08-09 18:58:32 -07:00
austinpower1258 8fec712fe5 test: isolate Dock pointer focus suite 2026-08-09 18:56:58 -07:00
Austin Wang a23d39612d Merge pull request #9764 from manaflow-ai/issue-9588-lightmode-text-contrast
Fix surface-scoped terminal theme reload ordering
2026-08-09 18:55:53 -07:00
Austin Wang 209a761923 Merge pull request #9340 from manaflow-ai/issue-9337-dock-terminal-live-title
Fix live titles for Dock terminals
2026-08-09 18:55:29 -07:00
lawrencecchen 8220a78eb6 Merge remote-tracking branch 'origin/main' into feat-tui-sdk-stack 2026-08-09 18:40:26 -07:00
austinpower1258 4a126b196b test: isolate captured right-click regression 2026-08-09 18:37:21 -07:00
Austin Wang 957481cf73 Merge pull request #9540 from manaflow-ai/issue-9520-hermes-first-class
Add first-class Hermes restore and lifecycle support
2026-08-09 18:36:34 -07:00
lawrencecchen a95a2612f8 Merge remote-tracking branch 'origin/main' into codex/cmux-browser-provider
# Conflicts:
#	cmux-tui/bindings/cpp/.cmux-sdk-manifest.json
#	cmux-tui/bindings/cpp/include/cmux/raw/generated/models.hpp
#	cmux-tui/bindings/go/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/go/raw/generated_commands.go
#	cmux-tui/bindings/go/raw/generated_events.go
#	cmux-tui/bindings/go/raw/generated_metadata.go
#	cmux-tui/bindings/go/raw/generated_presence_test.go
#	cmux-tui/bindings/go/raw/generated_types.go
#	cmux-tui/bindings/java/src/com/cmux/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/java/src/com/cmux/raw/Protocol.java
#	cmux-tui/bindings/java/tests/com/cmux/raw/GeneratedCoverageTest.java
#	cmux-tui/bindings/python/cmux/raw/_generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/python/cmux/raw/_generated/_schema.py
#	cmux-tui/bindings/python/cmux/raw/_generated/metadata.py
#	cmux-tui/bindings/rust/src/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/rust/src/generated/commands.rs
#	cmux-tui/bindings/rust/src/generated/events.rs
#	cmux-tui/bindings/rust/src/generated/metadata.rs
#	cmux-tui/bindings/rust/src/generated/mod.rs
#	cmux-tui/bindings/rust/src/generated/types.rs
#	cmux-tui/bindings/typescript/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/typescript/src/raw/generated/commands.ts
#	cmux-tui/bindings/typescript/src/raw/generated/events.ts
#	cmux-tui/bindings/typescript/src/raw/generated/index.ts
#	cmux-tui/bindings/typescript/src/raw/generated/metadata.ts
#	cmux-tui/bindings/typescript/src/raw/generated/types.ts
#	cmux-tui/bindings/typescript/test/generated.test.ts
#	cmux-tui/bindings/zig/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/zig/src/raw/generated/protocol.zig
#	cmux-tui/crates/cmux-tui-core/src/mux.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/tests.rs
#	cmux-tui/crates/cmux-tui/src/app.rs
2026-08-09 18:34:36 -07:00
lawrencecchen 9e7d2ab2b7 fix(tui): fence and drain journal process scopes 2026-08-09 18:29:59 -07:00
Austin Wang d13b4090dd Merge pull request #9759 from manaflow-ai/issue-9648-diffviewer-deadlock
Fix diff viewer scheme-handler deadlock
2026-08-09 18:27:51 -07:00
lawrencecchen 81d9090bd1 Make C++ receive signal idempotent 2026-08-09 18:27:26 -07:00
lawrencecchen b5d3a6ef23 Merge remote-tracking branch 'origin/main' into feat-tui-sdk-stack 2026-08-09 18:17:26 -07:00
lawrencecchen e9c67185be Close Java connection after write failure 2026-08-09 18:16:57 -07:00
lawrencecchen 99c7fc2aab fix(tui): scope macOS process metadata import 2026-08-09 18:14:31 -07:00
Lawrence Chen 33b588e993 Merge pull request #9447 from manaflow-ai/feat-tui-resource-columns
Add configurable native TUI resource views
2026-08-09 18:14:05 -07:00
lawrencecchen dda8c8f2a5 fix(tui): preserve process identity through cleanup 2026-08-09 18:12:21 -07:00
lawrencecchen 1d24221e91 Bound Java commands by one deadline 2026-08-09 18:07:49 -07:00
lawrencecchen baefeaaee8 Address deterministic wait review findings 2026-08-09 17:56:36 -07:00
lawrencecchen 13718cfe84 style(tui): apply hosted journal rustfmt 2026-08-09 17:56:10 -07:00
lawrencecchen 0247bf8734 test(tui): signal journal cursor validation 2026-08-09 17:54:36 -07:00
lawrencecchen de09a97732 fix(tui): drain journal shutdown and cleanup scans 2026-08-09 17:50:40 -07:00
austinpower1258 de986cedb0 test: await right-click runtime readiness 2026-08-09 17:48:38 -07:00
austinpower1258 6c29d0f5ff test: keep CLI socket fixtures off shared queues 2026-08-09 17:47:34 -07:00
lawrencecchen ef81dd215f Merge remote-tracking branch 'origin/main' into feat-tui-sdk-stack 2026-08-09 17:46:02 -07:00
Lawrence Chen 9d9ff9756d Merge pull request #9878 from manaflow-ai/feat-ghostty-pointer-compat
Restore compatible Ghostty pointer
2026-08-09 17:45:27 -07:00
austinpower1258 7fd62a813f test: keep font shortcut fixture package-safe 2026-08-09 17:31:49 -07:00
lawrencecchen 8207293067 style(tui): apply hosted journal rustfmt 2026-08-09 17:31:46 -07:00
lawrencecchen a8e3263013 fix(tui): fence journal shutdown ownership 2026-08-09 17:26:07 -07:00
lawrencecchen d73de828b3 Restore compatible Ghostty pointer 2026-08-09 17:25:44 -07:00
austinpower1258 f081144aef test: avoid starving CLI exit waits 2026-08-09 17:12:42 -07:00
lawrencecchen 2abcd55356 fix(tui): clear hosted journal diagnostics 2026-08-09 17:09:23 -07:00
lawrencecchen 06267609e3 Merge remote-tracking branch 'origin/main' into feat-tui-sdk-stack
# Conflicts:
#	.github/workflows/cmux-tui-sdks.yml
#	.github/workflows/cmux-tui-spec.yml
#	.github/workflows/sdk-publish-crates.yml
#	.github/workflows/sdk-publish-go.yml
#	.github/workflows/sdk-publish-java.yml
#	.github/workflows/sdk-publish-npm.yml
#	.github/workflows/sdk-publish-python.yml
#	cmux-tui/Cargo.lock
#	cmux-tui/bindings/ERGONOMICS.md
#	cmux-tui/bindings/RELEASING.md
#	cmux-tui/bindings/check-versions.py
#	cmux-tui/bindings/codegen/emit_cpp.py
#	cmux-tui/bindings/codegen/emit_go.py
#	cmux-tui/bindings/codegen/emit_java.py
#	cmux-tui/bindings/codegen/emit_python.py
#	cmux-tui/bindings/codegen/emit_typescript.py
#	cmux-tui/bindings/codegen/emit_zig.py
#	cmux-tui/bindings/codegen/tests/test_emit_cpp.py
#	cmux-tui/bindings/codegen/tests/test_emit_go.py
#	cmux-tui/bindings/codegen/tests/test_generate.py
#	cmux-tui/bindings/codegen/tests/test_ir.py
#	cmux-tui/bindings/conformance/README.md
#	cmux-tui/bindings/conformance/adapter-protocol.md
#	cmux-tui/bindings/conformance/adapters/cpp/CMakeLists.txt
#	cmux-tui/bindings/conformance/adapters/cpp/main.cpp
#	cmux-tui/bindings/conformance/adapters/go/main.go
#	cmux-tui/bindings/conformance/adapters/java/build.sh
#	cmux-tui/bindings/conformance/adapters/python/adapter.py
#	cmux-tui/bindings/conformance/adapters/rust/Cargo.lock
#	cmux-tui/bindings/conformance/adapters/rust/Cargo.toml
#	cmux-tui/bindings/conformance/adapters/rust/src/main.rs
#	cmux-tui/bindings/conformance/adapters/typescript/adapter.mjs
#	cmux-tui/bindings/conformance/adapters/zig/build.zig
#	cmux-tui/bindings/conformance/adapters/zig/main.zig
#	cmux-tui/bindings/conformance/fixtures.json
#	cmux-tui/bindings/conformance/runner.py
#	cmux-tui/bindings/conformance/test_runner.py
#	cmux-tui/bindings/cpp/.cmux-sdk-manifest.json
#	cmux-tui/bindings/cpp/CMakeLists.txt
#	cmux-tui/bindings/cpp/README.md
#	cmux-tui/bindings/cpp/include/cmux/client.hpp
#	cmux-tui/bindings/cpp/include/cmux/result.hpp
#	cmux-tui/bindings/cpp/include/cmux/transport.hpp
#	cmux-tui/bindings/cpp/include/cmux/version.hpp
#	cmux-tui/bindings/cpp/src/json.cpp
#	cmux-tui/bindings/cpp/src/unix_transport.cpp
#	cmux-tui/bindings/cpp/tests/consumer/CMakeLists.txt
#	cmux-tui/bindings/cpp/tests/consumer/main.cpp
#	cmux-tui/bindings/cpp/tests/test_attachment.cpp
#	cmux-tui/bindings/cpp/tests/test_client.cpp
#	cmux-tui/bindings/cpp/tests/test_generated.cpp
#	cmux-tui/bindings/cpp/tests/test_macro_collision.cpp
#	cmux-tui/bindings/cpp/tests/test_unix_transport.cpp
#	cmux-tui/bindings/examples/cpp-terminal-frontend/CMakeLists.txt
#	cmux-tui/bindings/examples/cpp-terminal-frontend/FRICTION.md
#	cmux-tui/bindings/examples/cpp-terminal-frontend/README.md
#	cmux-tui/bindings/examples/cpp-terminal-frontend/include/cmux_example/frontend.hpp
#	cmux-tui/bindings/examples/cpp-terminal-frontend/src/frontend.cpp
#	cmux-tui/bindings/examples/cpp-terminal-frontend/src/main.cpp
#	cmux-tui/bindings/examples/cpp-terminal-frontend/tests/frontend_test.cpp
#	cmux-tui/bindings/examples/go-terminal-bot/FRICTION.md
#	cmux-tui/bindings/examples/go-terminal-bot/README.md
#	cmux-tui/bindings/examples/go-terminal-bot/bot.go
#	cmux-tui/bindings/examples/go-terminal-bot/bot_integration_test.go
#	cmux-tui/bindings/examples/go-terminal-bot/cmd/go-terminal-bot/main.go
#	cmux-tui/bindings/examples/go-terminal-bot/config.go
#	cmux-tui/bindings/examples/go-terminal-bot/fake_server_test.go
#	cmux-tui/bindings/examples/go-terminal-bot/result.go
#	cmux-tui/bindings/examples/java-ci-orchestrator/FRICTION.md
#	cmux-tui/bindings/examples/java-ci-orchestrator/README.md
#	cmux-tui/bindings/examples/java-ci-orchestrator/src/com/cmux/examples/ci/CiOrchestrator.java
#	cmux-tui/bindings/examples/java-ci-orchestrator/tests/com/cmux/examples/ci/CiOrchestratorIntegrationTest.java
#	cmux-tui/bindings/examples/java-ci-orchestrator/tests/com/cmux/examples/ci/FakeCmuxServer.java
#	cmux-tui/bindings/examples/python-agent-watchdog/FRICTION.md
#	cmux-tui/bindings/examples/python-agent-watchdog/README.md
#	cmux-tui/bindings/examples/python-agent-watchdog/tests/test_watchdog.py
#	cmux-tui/bindings/examples/python-agent-watchdog/watchdog.py
#	cmux-tui/bindings/examples/rust-agent-dashboard/Cargo.lock
#	cmux-tui/bindings/examples/rust-agent-dashboard/Cargo.toml
#	cmux-tui/bindings/examples/rust-agent-dashboard/FRICTION.md
#	cmux-tui/bindings/examples/rust-agent-dashboard/README.md
#	cmux-tui/bindings/examples/rust-agent-dashboard/src/lib.rs
#	cmux-tui/bindings/examples/rust-agent-dashboard/src/main.rs
#	cmux-tui/bindings/examples/rust-agent-dashboard/src/model.rs
#	cmux-tui/bindings/examples/rust-agent-dashboard/tests/fake_server.rs
#	cmux-tui/bindings/examples/typescript-browser-controller/FRICTION.md
#	cmux-tui/bindings/examples/typescript-browser-controller/README.md
#	cmux-tui/bindings/examples/typescript-browser-controller/package-lock.json
#	cmux-tui/bindings/examples/typescript-browser-controller/package.json
#	cmux-tui/bindings/examples/typescript-browser-controller/scripts/verify-clean-linked-sdk.mjs
#	cmux-tui/bindings/examples/typescript-browser-controller/scripts/verify-packaged-consumer.mjs
#	cmux-tui/bindings/examples/typescript-browser-controller/src/controller.ts
#	cmux-tui/bindings/examples/typescript-browser-controller/src/node-demo.ts
#	cmux-tui/bindings/examples/typescript-browser-controller/src/websocket.ts
#	cmux-tui/bindings/examples/typescript-browser-controller/test/controller.test.ts
#	cmux-tui/bindings/examples/typescript-browser-controller/test/websocket.test.ts
#	cmux-tui/bindings/go/README.md
#	cmux-tui/bindings/go/client.go
#	cmux-tui/bindings/go/client_test.go
#	cmux-tui/bindings/java/README.md
#	cmux-tui/bindings/java/consumer-tests/com/cmux/consumer/ExternalJarConsumerTest.java
#	cmux-tui/bindings/java/pom.xml
#	cmux-tui/bindings/java/scripts/test.sh
#	cmux-tui/bindings/java/src/com/cmux/CmuxClient.java
#	cmux-tui/bindings/java/src/com/cmux/CmuxCommandException.java
#	cmux-tui/bindings/java/src/com/cmux/CmuxDecodeException.java
#	cmux-tui/bindings/java/src/com/cmux/JsonException.java
#	cmux-tui/bindings/java/src/com/cmux/Pane.java
#	cmux-tui/bindings/java/src/com/cmux/Screen.java
#	cmux-tui/bindings/java/src/com/cmux/Tab.java
#	cmux-tui/bindings/java/src/com/cmux/Workspace.java
#	cmux-tui/bindings/python/README.md
#	cmux-tui/bindings/python/cmux/__init__.py
#	cmux-tui/bindings/python/cmux/client.py
#	cmux-tui/bindings/python/cmux/errors.py
#	cmux-tui/bindings/python/cmux/transport.py
#	cmux-tui/bindings/python/pyproject.toml
#	cmux-tui/bindings/python/tests/support.py
#	cmux-tui/bindings/python/tests/test_consumer.py
#	cmux-tui/bindings/python/tests/test_convenience.py
#	cmux-tui/bindings/python/tests/test_events.py
#	cmux-tui/bindings/python/tests/test_lifecycle.py
#	cmux-tui/bindings/python/tests/test_protocol.py
#	cmux-tui/bindings/python/tests/test_schema_roundtrip.py
#	cmux-tui/bindings/rust/Cargo.toml
#	cmux-tui/bindings/rust/README.md
#	cmux-tui/bindings/rust/examples/e2e.rs
#	cmux-tui/bindings/rust/src/client.rs
#	cmux-tui/bindings/rust/src/codec.rs
#	cmux-tui/bindings/rust/src/convenience.rs
#	cmux-tui/bindings/rust/src/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/rust/src/generated/commands.rs
#	cmux-tui/bindings/rust/src/generated/events.rs
#	cmux-tui/bindings/rust/src/generated/metadata.rs
#	cmux-tui/bindings/rust/src/generated/mod.rs
#	cmux-tui/bindings/rust/src/generated/types.rs
#	cmux-tui/bindings/rust/src/lib.rs
#	cmux-tui/bindings/rust/src/presence.rs
#	cmux-tui/bindings/rust/src/topology.rs
#	cmux-tui/bindings/rust/tests/mock_server.rs
#	cmux-tui/bindings/rust/tests/public_api.rs
#	cmux-tui/bindings/typescript/README.md
#	cmux-tui/bindings/typescript/e2e/e2e.ts
#	cmux-tui/bindings/typescript/package-lock.json
#	cmux-tui/bindings/typescript/package.json
#	cmux-tui/bindings/typescript/scripts/verify-packaged-consumer.mjs
#	cmux-tui/bindings/typescript/src/browser.ts
#	cmux-tui/bindings/typescript/src/client.ts
#	cmux-tui/bindings/typescript/src/errors.ts
#	cmux-tui/bindings/typescript/src/index.ts
#	cmux-tui/bindings/typescript/src/node-client.ts
#	cmux-tui/bindings/typescript/src/node-transport.ts
#	cmux-tui/bindings/typescript/src/protocol/index.ts
#	cmux-tui/bindings/typescript/src/transport-limits.ts
#	cmux-tui/bindings/typescript/src/websocket-transport.ts
#	cmux-tui/bindings/typescript/test/authority.test.ts
#	cmux-tui/bindings/typescript/test/client.test.ts
#	cmux-tui/bindings/typescript/test/field-compatibility.test.ts
#	cmux-tui/bindings/typescript/test/generated.test.ts
#	cmux-tui/bindings/typescript/test/package-consumer.test.ts
#	cmux-tui/bindings/typescript/test/presence.test.ts
#	cmux-tui/bindings/typescript/test/protocol-types.ts
#	cmux-tui/bindings/typescript/test/websocket-transport.test.ts
#	cmux-tui/bindings/zig/README.md
#	cmux-tui/bindings/zig/build.zig
#	cmux-tui/bindings/zig/build.zig.zon
#	cmux-tui/bindings/zig/examples/watch.zig
#	cmux-tui/bindings/zig/src/cmux.zig
#	cmux-tui/scripts/check-sdk-schema.py
#	cmux-tui/scripts/test_check_sdk_schema.py
#	cmux-tui/spec/README.md
#	cmux-tui/spec/bindings.md
#	cmux-tui/spec/sdk-schema.json
2026-08-09 17:08:02 -07:00
lawrencecchen 3e5f5f5485 fix(tui): close journal process scope review gaps 2026-08-09 17:06:30 -07:00
austinpower1258 9f663a85ba test: keep right-click regression deterministic 2026-08-09 17:05:53 -07:00
austinpower1258 88144ebdf1 test: cover accepted attention transitions 2026-08-09 17:01:41 -07:00
austinpower1258 a72ec33b16 refactor: isolate attention ownership types 2026-08-09 16:54:10 -07:00
Lawrence Chen 2e8eedf991 Merge pull request #8856 from manaflow-ai/task-cmux-tui-spec-completeness
Make cmux-tui programmability inventory enforceable
2026-08-09 16:54:07 -07:00
lawrencecchen 1a60931f55 style(tui): apply hosted journal rustfmt 2026-08-09 16:53:39 -07:00
lawrencecchen 092a5f44e9 Merge remote-tracking branch 'origin/main' into feat-pr-8856-closeout
# Conflicts:
#	.github/workflows/cmux-tui-spec.yml
#	cmux-tui/bindings/generate.sh
#	cmux-tui/crates/cmux-tui-core/src/server.rs
#	cmux-tui/crates/cmux-tui/src/app.rs
#	cmux-tui/crates/cmux-tui/src/config.rs
#	cmux-tui/scripts/check-spec-inventory.py
#	cmux-tui/scripts/test_check_spec_inventory.py
#	cmux-tui/spec/README.md
#	cmux-tui/spec/bindings.md
#	cmux-tui/spec/cli.md
#	cmux-tui/spec/commands.md
#	cmux-tui/spec/events.md
#	cmux-tui/spec/frontends.md
#	cmux-tui/spec/inventory.json
#	cmux-tui/spec/inventory.schema.json
#	cmux-tui/spec/machine-agent.md
#	cmux-tui/spec/native-frontend.md
#	cmux-tui/spec/programmability.md
#	cmux-tui/spec/terminal-host.md
2026-08-09 16:52:30 -07:00
lawrencecchen 467c8ccd67 Merge remote sleep audit updates
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/unix_process_scope.rs
2026-08-09 16:47:33 -07:00
lawrencecchen dcc1e243f5 fix(tui): close journal shutdown lifecycle gaps 2026-08-09 16:47:10 -07:00
lawrencecchen 726ff99682 Remove SDK sleep-based synchronization 2026-08-09 16:43:16 -07:00
lawrencecchen 54f3fae4cf style(tui): apply hosted Rust format 2026-08-09 16:41:39 -07:00
austinpower1258 e004773638 fix: refresh and harden diff viewer trust 2026-08-09 16:41:35 -07:00
lawrencecchen 293daadd1d test(tui): replace machine creation sleep 2026-08-09 16:35:36 -07:00
lawrencecchen 561f5abb23 Document bounded process cleanup probe 2026-08-09 16:26:54 -07:00
austinpower1258 d549189e00 test: cover diff viewer trust hardening 2026-08-09 16:23:36 -07:00
lawrencecchen 62a9d5a8e2 test(tui): isolate terminal close CLI contract 2026-08-09 16:21:58 -07:00
lawrencecchen 6117fe34c4 Test detached hook environment replacement 2026-08-09 16:21:53 -07:00
austinpower1258 b264787d04 test: cover deferred diff viewer allowlist growth 2026-08-09 16:19:49 -07:00
lawrencecchen d67ca74f05 Fence journal shutdown and reconnect recovery 2026-08-09 16:14:18 -07:00
austinpower1258 a07a4fe92d test: exercise routed sidebar terminal shortcut 2026-08-09 16:13:38 -07:00
lawrencecchen a9e662b8f3 Test journal detach and detached hook containment 2026-08-09 16:07:14 -07:00
austinpower1258 97f2007217 fix: address diff viewer review feedback 2026-08-09 15:55:53 -07:00
lawrencecchen d47f000747 style(tui): apply hosted Rust format 2026-08-09 15:54:10 -07:00
austinpower1258 cc5afd057e fix: preserve compatible observation teardown 2026-08-09 15:49:44 -07:00
lawrencecchen d5b26aec5a Fence terminal reads and contain detached hooks 2026-08-09 15:47:53 -07:00
austinpower1258 e93082b68c fix: bind deferred telemetry to runtime lifecycle 2026-08-09 15:33:55 -07:00
austinpower1258 e3c8a35475 fix: isolate observation teardown 2026-08-09 15:33:27 -07:00
austinpower1258 c31c07200a test: require synchronous observation teardown 2026-08-09 15:31:57 -07:00
lawrencecchen 3791300984 test(tui): retry inspected terminal close 2026-08-09 15:23:41 -07:00
lawrencecchen ca0b32c603 Pin merged Ghostty dependency mainline 2026-08-09 15:13:01 -07:00
lawrencecchen fa4c2da7a6 Apply hosted Rust formatting 2026-08-09 15:12:53 -07:00
lawrencecchen f18ace71aa Pin merged Ghostty dependency for hosted verification 2026-08-09 15:09:01 -07:00
lawrencecchen 28d2043ee3 Close journal shutdown and hook lifecycle gaps 2026-08-09 15:08:50 -07:00
austinpower1258 fe135b7344 fix: preserve passive attention boundaries 2026-08-09 15:07:00 -07:00
austinpower1258 e881d2f598 test: cover remaining attention ownership gaps 2026-08-09 15:05:20 -07:00
lawrencecchen 8469a8e117 Fix hosted cmux-tui verification review findings 2026-08-09 15:02:09 -07:00
lawrencecchen 99e90fa8db Merge remote-tracking branch 'origin/main' into closeout-pr9837-20260809
# Conflicts:
#	ghostty
2026-08-09 14:59:29 -07:00
austinpower1258 11071d6ad1 test: reject deferred retired-runtime telemetry 2026-08-09 14:53:02 -07:00
lawrencecchen bfa3c46d06 Wake blocked journal writers on failure 2026-08-09 14:52:32 -07:00
austinpower1258 7be9b49c68 Merge remote-tracking branch 'origin/main' into issue-9588-lightmode-text-contrast 2026-08-09 14:52:16 -07:00
lawrencecchen 7dddd819a4 fix(sdk): include agent event in subscribe inventory 2026-08-09 14:52:08 -07:00
lawrencecchen 15dcfccd32 Merge remote-tracking branch 'origin/codex/cmux-browser-provider' into codex/cmux-browser-provider 2026-08-09 14:51:24 -07:00
lawrencecchen e8e018c015 Fix hosted cross-platform test compilation 2026-08-09 14:51:20 -07:00
lawrencecchen b1141bb172 fix(tui): replace journal polling with signals 2026-08-09 14:49:19 -07:00
lawrencecchen 8b80d0d52c Apply hosted Windows hook formatting 2026-08-09 14:39:51 -07:00
lawrencecchen 996d12c8eb test(tui): reconcile uncertain close outcome 2026-08-09 14:38:36 -07:00
lawrencecchen 0cc8d57ff1 Close Windows hook spawn race 2026-08-09 14:33:42 -07:00
austinpower1258 45757ce499 fix: enforce attention observation ownership 2026-08-09 14:32:36 -07:00
austinpower1258 213840cee3 test: expect workspace Dock attention ownership 2026-08-09 14:28:14 -07:00
lawrencecchen 04ce6435fa Close journal deadline and process-tree gaps 2026-08-09 14:26:46 -07:00
lawrencecchen 49a6bf8af5 Bound journal receipts and provider resolution 2026-08-09 14:15:23 -07:00
austinpower1258 c57a99c5e6 fix: preserve exact attention ownership 2026-08-09 14:13:44 -07:00
austinpower1258 d0b4e74e59 test: cover workspace Dock Feed attention cleanup 2026-08-09 13:59:08 -07:00
austinpower1258 05a7b68476 fix: harden Dock unread ownership 2026-08-09 13:33:54 -07:00
austinpower1258 3890650b0d Require restored diff viewer URL in test 2026-08-09 13:31:20 -07:00
austinpower1258 5793a774e1 fix: render Dock agent decision attention 2026-08-09 13:26:39 -07:00
austinpower1258 f84d774b28 Keep localized Dock metadata within SEO bounds 2026-08-09 13:26:00 -07:00
austinpower1258 f6a08aa9b3 Fix Dock locale schema placement 2026-08-09 13:18:15 -07:00
austinpower1258 909878e7b1 test: require rendered Dock decision attention 2026-08-09 12:53:40 -07:00
austinpower1258 ad31e3f75b fix: accept explicit input without empty scans 2026-08-09 12:35:06 -07:00
austinpower1258 b2d2c827d5 Address chromeless browser review feedback 2026-08-09 12:27:42 -07:00
austinpower1258 5cf7c3920c test: require accepted input fast paths 2026-08-09 12:14:47 -07:00
austinpower1258 e5cdb714a0 fix: decode persisted Hermes registration 2026-08-09 12:11:18 -07:00
austinpower1258 a978650bb0 Fix browser chrome toggle return 2026-08-09 12:06:51 -07:00
austinpower1258 dbddb211ba test: cover Hermes app snapshot decoding 2026-08-09 12:02:49 -07:00
austinpower1258 bf743ad8b0 Preserve chromeless browser policy 2026-08-09 11:53:52 -07:00
austinpower1258 07a4bef146 Add chromeless Dock browser controls 2026-08-09 11:45:50 -07:00
austinpower1258 b13f5bf0b0 fix: dismiss unread for copy-mode input 2026-08-09 11:31:32 -07:00
austinpower1258 8c5942c496 test: clear unread for accepted copy-mode input 2026-08-09 11:30:54 -07:00
austinpower1258 71d854e04e fix: dismiss unread only after accepted input 2026-08-09 11:23:41 -07:00
austinpower1258 0787062387 test: require accepted input before unread dismissal 2026-08-09 11:20:19 -07:00
austinpower1258 7fda3a8739 fix: route Hermes Vault resume through managed wrapper 2026-08-09 11:15:39 -07:00
austinpower1258 c429a0a5c7 test: cover Hermes Vault managed wrapper routing 2026-08-09 11:13:08 -07:00
austinpower1258 2765657827 fix: isolate Feed attention from agent lifecycle 2026-08-09 11:02:59 -07:00
austinpower1258 fb717d63c3 test: isolate Feed attention lifecycle ownership 2026-08-09 11:01:09 -07:00
austinpower1258 404eed2115 fix: restore lifecycle after Feed attention 2026-08-09 10:50:25 -07:00
austinpower1258 31d45e4096 test: reject stale Feed lifecycle ownership 2026-08-09 10:48:57 -07:00
austinpower1258 a8892038f0 fix: preserve exact attention state across owner transfer 2026-08-09 10:41:58 -07:00
austinpower1258 5da6f8a90c test: cover stale owners and atomic unread transfer 2026-08-09 10:33:46 -07:00
austinpower1258 12ea44f482 fix: parse attached Hermes short resume 2026-08-09 10:04:21 -07:00
austinpower1258 9071bbcd68 test: cover attached Hermes short resume 2026-08-09 10:03:59 -07:00
austinpower1258 633bb492f8 fix: stabilize attention ownership and unread replacement 2026-08-09 09:53:13 -07:00
austinpower1258 905072c8fc test: cover overlapping attention across owner transfer 2026-08-09 09:48:43 -07:00
austinpower1258 31a8f9d5a2 test: cover attention transfer and atomic unread replacement 2026-08-09 09:46:49 -07:00
austinpower1258 021d8714dc fix: rearm Hermes after hook record reuse 2026-08-09 09:31:06 -07:00
austinpower1258 969d72f708 test: cover reused Hermes resume record 2026-08-09 09:30:01 -07:00
austinpower1258 78bd52122e fix: route Dock attention through exact panel owners 2026-08-09 09:23:04 -07:00
austinpower1258 d237b3ef93 test: cover surface-less Dock attention and input dismissal 2026-08-09 09:19:10 -07:00
austinpower1258 6ddb161d7b fix: rearm Hermes restore after completed turn 2026-08-09 08:58:38 -07:00
austinpower1258 3eab38be4b fix: return panel owner lifecycle explicitly 2026-08-09 08:55:13 -07:00
austinpower1258 6ff6098401 fix: close remaining Dock attention ownership gaps 2026-08-09 08:48:21 -07:00
austinpower1258 08c9d7a81d test: cover remaining Dock attention ownership gaps 2026-08-09 08:42:32 -07:00
austinpower1258 2170d3607e fix: fail closed across remaining attention routes 2026-08-09 08:14:53 -07:00
austinpower1258 d9a2994887 test: cover remaining fail-closed attention routes 2026-08-09 08:06:31 -07:00
austinpower1258 8ed29f23ae test: cover idle Hermes second-launch restore 2026-08-09 08:02:42 -07:00
austinpower1258 bf560e85fa fix: keep Dock reveal bound to its owner window 2026-08-09 07:42:48 -07:00
austinpower1258 8b8b561f34 test: fail closed when Dock owner window is unavailable 2026-08-09 07:28:12 -07:00
austinpower1258 0ff8b44b22 Merge remote-tracking branch 'origin/main' into issue-9520-hermes-first-class 2026-08-09 07:26:32 -07:00
austinpower1258 53539c021e fix: constrain Python Hermes entrypoint detection 2026-08-09 07:26:24 -07:00
austinpower1258 f5c036fd81 refactor: unify unread observation ownership 2026-08-09 07:05:55 -07:00
austinpower1258 e9d3e79698 fix: separate live and rendered surface ownership 2026-08-09 06:53:17 -07:00
austinpower1258 0cef8a8849 test: reject unrelated Python Hermes arguments 2026-08-09 06:43:03 -07:00
austinpower1258 ff429a05bd fix: close scoped unread attention gaps 2026-08-09 06:19:13 -07:00
austinpower1258 a7ab936b53 fix: detect venv Hermes resume sessions 2026-08-09 06:02:01 -07:00
austinpower1258 49e9c8ccc2 test: cover window Dock notification open parity 2026-08-09 05:53:27 -07:00
austinpower1258 fc065ac05c test: cover idle Python Hermes autoresume 2026-08-09 05:41:53 -07:00
austinpower1258 45765c8ff1 test: provide Dock focus fixture state 2026-08-09 05:33:22 -07:00
austinpower1258 c7ed0ee1e2 fix: scope background unread publication 2026-08-09 05:15:17 -07:00
austinpower1258 18acebc387 test: cover owner-scoped unread publication 2026-08-09 04:52:13 -07:00
austinpower1258 4d743baf9d fix: keep Hermes launch shims native 2026-08-09 04:47:17 -07:00
austinpower1258 c17c9f98f2 test: isolate Dock focus fixtures 2026-08-09 04:46:39 -07:00
austinpower1258 c3ec08c7c8 test: cover PATH-shadowed Hermes bash launchers 2026-08-09 04:46:05 -07:00
austinpower1258 ba01c8fc36 fix: fail open stale Hermes Python bridge 2026-08-09 04:39:38 -07:00
austinpower1258 b6e7b86881 test: cover Hermes bridge fallback without watcher 2026-08-09 04:36:53 -07:00
austinpower1258 bce2108d85 fix: reject stale Hermes Python wrappers 2026-08-09 04:33:14 -07:00
austinpower1258 6822bba9dc test: cover stale Hermes Python wrapper restore 2026-08-09 04:32:32 -07:00
Lawrence Chen 111ceca888 Add private team-scoped CodeRouter usage metrics (#9864)
* Add regressions for private team usage metrics

* Add private team-scoped CodeRouter usage metrics

* Harden CodeRouter analytics isolation

* Match the deployed PostHog Endpoint dialect

* Make the usage window exactly 30 UTC days

* Accept both PostHog Endpoint row encodings
2026-08-09 04:02:55 -07:00
austinpower1258 11a1ddf66a fix: preflight workspace Dock focus 2026-08-09 03:56:30 -07:00
austinpower1258 eb5db49558 test: cover unavailable workspace Dock focus 2026-08-09 03:55:32 -07:00
Lawrence Chen e3c09b9d4c Upgrade web to Next.js 16.3 instant navigation (#9841)
* Upgrade web to Next.js 16.3 instant navigation

* Add regressions for auth loading and Vault gating

* Gate Vault and restore Stack Auth rendering

* Add regressions for CodeRouter pricing and Vault gating

* Ship CodeRouter branding behind Vault pricing flag

* Keep Vault flag tests isolated

* Fix instant server and pricing test isolation
2026-08-09 03:49:19 -07:00
austinpower1258 11538d4bb7 fix: make Dock attention updates incremental 2026-08-09 03:43:55 -07:00
austinpower1258 8234e17774 test: cover incremental Dock unread and focus failures 2026-08-09 03:39:37 -07:00
austinpower1258 8c580ff090 fix: rearm retired Hermes restore sessions 2026-08-09 03:36:18 -07:00
austinpower1258 f04e62cce3 test: cover retired Hermes restore checkpoint 2026-08-09 03:28:18 -07:00
austinpower1258 f6000cbac6 fix: ignore hidden workspace Dock notifications 2026-08-09 03:06:10 -07:00
austinpower1258 73d554aec2 test: reject hidden workspace Dock notifications 2026-08-09 03:04:04 -07:00
austinpower1258 316b7ae8ee fix: preserve Dock scope during notification opens 2026-08-09 02:49:46 -07:00
austinpower1258 047da6840e test: cover workspace Dock notification opens 2026-08-09 02:48:32 -07:00
austinpower1258 6859f8073a test: avoid process-wide AppKit interception 2026-08-09 02:39:57 -07:00
austinpower1258 1467f10911 fix: route attention through live focus owners 2026-08-09 02:37:21 -07:00
austinpower1258 9090fde894 fix: close attention review gaps 2026-08-09 02:23:43 -07:00
austinpower1258 aa0c789aed test: cover sidebar-focused terminal bell 2026-08-09 02:20:58 -07:00
austinpower1258 e4fac0ed84 Merge origin/main into issue-9466-stage-manager-raise 2026-08-09 02:18:54 -07:00
austinpower1258 433865de68 fix: keep quit snapshot loading off main actor 2026-08-09 02:18:49 -07:00
austinpower1258 8d661ac8b1 fix: recover durable Hermes resume sessions 2026-08-09 02:18:43 -07:00
austinpower1258 5d543e7675 fix: preserve exact Dock mark and autosave state 2026-08-09 01:57:02 -07:00
austinpower1258 7a94f55639 test: cover exact Dock mark and autosave state 2026-08-09 01:44:00 -07:00
austinpower1258 c086ce435d test: cover Hermes gateway bootstrap routing 2026-08-09 01:37:41 -07:00
austinpower1258 8d9033aad9 fix: keep background attention owner-scoped 2026-08-09 01:08:18 -07:00
austinpower1258 12ca7a13fe test: cover missing Hermes resume checkpoints 2026-08-09 00:51:56 -07:00
austinpower1258 a66fab4b26 test: correct Dock attention fixture coverage 2026-08-09 00:50:00 -07:00
austinpower1258 e0cf8fc4a0 test: cover focused Dock attention commands 2026-08-09 00:10:06 -07:00
austinpower1258 5bf855d1f1 fix: pin Hermes autoresume to session profile 2026-08-08 23:50:26 -07:00
austinpower1258 ef83242195 test: cover default Hermes autoresume profile drift 2026-08-08 23:47:05 -07:00
austinpower1258 84d9e9f299 fix: route projected terminal attention 2026-08-08 23:38:05 -07:00
austinpower1258 93b7e0f733 test: isolate Dock availability fixtures 2026-08-08 23:31:49 -07:00
austinpower1258 6c298ae559 fix: pin Hermes Vault resumes to indexed profile 2026-08-08 23:24:40 -07:00
austinpower1258 cfe925984b test: cover Hermes Vault profile mismatch 2026-08-08 23:18:38 -07:00
austinpower1258 7a3b405d5b test: cover mirror-owned terminal attention 2026-08-08 23:18:29 -07:00
austinpower1258 81213b24ac fix: complete background attention routing 2026-08-08 22:55:20 -07:00
austinpower1258 86c6bf05a3 test: cover remaining attention routing gaps 2026-08-08 22:33:13 -07:00
austinpower1258 21b232d6d1 fix: close attention owner lifecycle gaps 2026-08-08 22:14:57 -07:00
austinpower1258 d4e6f5ff22 fix: deduplicate Hermes completion replays 2026-08-08 22:02:17 -07:00
austinpower1258 62f8762ed5 Merge remote-tracking branch 'origin/main' into issue-9520-hermes-first-class 2026-08-08 21:47:54 -07:00
austinpower1258 2c6df311a2 fix: preserve durable Hermes resume recovery 2026-08-08 21:47:31 -07:00
austinpower1258 68d8b5caa6 test: cover repeated Hermes recovery failures 2026-08-08 21:47:26 -07:00
austinpower1258 4e0112adfb test: make non-key window fixtures deterministic 2026-08-08 21:44:25 -07:00
austinpower1258 b1592dc39f test: stabilize attention owner lifecycle coverage 2026-08-08 21:39:23 -07:00
austinpower1258 e9000ddb8a fix: repair Hermes restore identity and Vault indexing 2026-08-08 21:18:45 -07:00
austinpower1258 9578b85056 test: cover legacy Hermes restore and Vault indexing 2026-08-08 21:18:45 -07:00
austinpower1258 faf35dc651 test: cover attention owner lifecycle gaps 2026-08-08 20:58:43 -07:00
austinpower1258 08b8ab8490 test: close dock unread review gaps 2026-08-08 20:34:29 -07:00
austinpower1258 17e00f22b7 fix: preserve repeated terminal bell flashes 2026-08-08 20:32:56 -07:00
Abdulaziz AlbaharandClaude Fable 5 6d37f62a47 Add iPhone control for Simulator panes (#9401)
* Add iPhone simulator pane control

* Wire mobile simulator control files into app target

* Fix mobile simulator observer lookup

* Use IrohLib for endpoint signing

* Fix mobile simulator stream control

* Fix simulator stream tap policy lint

* Address simulator stream review feedback

iOS control-plane correctness: per-panel serialized start/stop operation
chains on the composite (background stop can no longer interleave a
foreground restart), failed start attempts settle back to idle instead of
spinning forever, simulator selection stops the previous panel before
starting the new one, and a pane's onDisappear deactivates only its own
panel so A->B switches keep B selected.

Ownership integrity: descriptor ownership is now tri-state; broadcast
payloads (state-sync rows, workspace lists) carry unknown ownership, so a
shared tick cannot flip the owning phone to view-only or locked mid-drag.
Passive simulator.state events merge descriptors without promoting idle
panels to starting.

Input and rendering: pointer events flow through one buffered stream drained
in order (no reordered began/moved/ended), and frame base64+image decode
moved off the main actor with forced decompression.

Mac host: advertised simulator capabilities gate on the same feature flag as
RPC dispatch, the stream coordinator prunes cached frames and workspace
mappings for closed panels, and a session whose frame delivery is refused
ends itself so the panel lock releases immediately.

Also drops @unchecked from the frame reader's now-checked Sendable, removes
a redundant initializer, renames the mapper's clamped: parameter to
allowsOutsideImage:, and adds focused tests for the new store behavior.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Move Iroh peer-identity derivation into CmuxIrohTransport

The app target imported IrohLib directly for SecretKey.fromBytes but never
links the IrohLib product, so the hosted test build failed with undefined
symbols for arm64 (the tagged reload only linked by toolchain accident).
CmuxIrohTransport owns the IrohLib dependency, so the derivation now lives
on CmxIrohIdentityMaterial there and the app consumes the public property.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Retry dogfood attach after startup teardown

* Improve mobile simulator tap precision

* Fix mobile simulator capability lint

* Fix mobile simulator input hit testing

* Fix simulator app switcher action

* Keep simulator frame stream live under backpressure

* Add simulator diagnostics telemetry

* Wire simulator diagnostics into macOS target

* Make simulator diagnostics queue test selectable

* Settle failed stream preflight and clear stale locked ownership

A start attempt that exits through the preflight guard (disconnected,
missing capability, or no client) now settles the activation spinner
back to idle, and a locked start rejection clears ownership remembered
from an earlier start so controls cannot stay live under the overlay.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Detect stalled simulator streams and self-heal on the phone

The dogfood freeze: the phone's transport died (direct path lost, relay
fallback, 30s idle timeout), the Mac cleanly released the stream, but the
pane kept showing the last frame under an 'iPhone Control' pill with no
way to tell it was dead. Nothing in the stream layer detected silence.

Mac side now emits simulator.state on a 5s cadence while a session is
active (capability simulator.keepalive.v1), so clients can treat event
silence as staleness without misreading a static Simulator screen; the
keepalive's frame-send request also retries a refused frame that no new
publication would retrigger. The phone arms a capability-gated watchdog
per active panel: a 15s interval with no frame or state event marks the
pane visibly stalled ('Reconnecting to Simulator' overlay, EN+JA) and
re-requests the stream through the serialized per-panel chain, retrying
each silent interval. Stalled is sticky until a fresh frame arrives so a
recovering pane cannot masquerade as live.

Also wires the previously dead includingSimulator parameter so simulator
capabilities are actually omitted when the feature flag is off.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Add consolidated on-disk AppLog split from network diagnostics

AppLog (CMUXMobileCore) persists the structured diagnostic ring to two
rotating 5MB files in Application Support, always on since events are
integer-encoded and privacy-safe: cmux-app.log carries everything
app-wide (simulator, browser, composer, render, plus the mirrored
string debug log) and cmux-network.log carries transport dials,
discovery, relay policy, paths, and session lifecycle; app lifecycle
and reachability context land in both. The composition root chains the
ring's event tap into AppLog ahead of the Sentry reporter and mirrors
MobileDebugLog's line stream, so one file tells the in-app story in
wall-clock order. Consecutive frame-pipeline events coalesce into a
'repeated xN' summary when the run breaks. Both files are shareable
from Iroh connection settings (strings EN+JA, plus the previously
uncataloged verbose-log strings).

Simulator diagnosis hardening: a stalled watchdog fire now records its
own DiagnosticSimulatorStreamLifecycle.stalled event (warning-level in
Sentry, so stalls become breadcrumbs and budgeted logs); identical
simulator.state keepalives short-circuit as .unchanged in the stream
store, feeding the watchdog without flooding the ring, breadcrumbs, or
disk every 5s; and the Mac keepalive only retries frames when a reader
exists, avoiding a readerMissing diagnostic per tick on panels without
a frame transport.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Await actor-isolated debug log line stream

Co-Authored-By: Claude Fable 5 <[email protected]>

* Move log sharing to a top-level Diagnostics settings section

The app log covers every feature and the network log covers all
connection diagnostics, so neither belongs on the Iroh screen; that
screen keeps only its connection report and verbose-connection-log
toggle.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Fall back to appending when log rotation fails

A failed .1 move followed by createFile truncated the current log in
place, erasing exactly the diagnostics a user might be about to share.
The writer now reopens the existing generation for appending and
retries rotation on later appends.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Discard unused frame-publication handler results

The two clear-handler calls kept their tokens implicitly, tripping the
zero-budget warning bucket for this new file in CI's warning gate.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Bound failed-rotation fallback: no header spam, no offset-0 writes

A sustained rotate failure now retries only after another full byte
budget accrues (threshold watermark instead of per-line attempts), the
fallback writes no extra session header, and a generation that cannot
be opened and positioned at its end disables writing instead of
overwriting from offset zero.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-08 20:27:34 -07:00
austinpower1258 d5362ece33 fix: route terminal bells through exact owners 2026-08-08 20:25:58 -07:00
Abdulaziz AlbaharandClaude Fable 5 866d15982c iOS: open workspace search selections inside the search tab (#9820)
* Add regression test: workspace search selection stays inside the search tab

Selecting a workspace from the search tab's results on iOS could strand
the app on the Workspaces list with no tab bar, no toolbar, no search
field, and a stale query filter: the selection deactivated search,
transitioned to the Workspaces tab, and pushed onto that tab's
NavigationStack while it was still off-window behind the search-field
dismissal, so the path recorded a push that never happened.

The test drives the exact repro: activate search, type a query that
filters to one workspace, tap the result, pop back, and require the app
to still be inside the search tab with usable bottom controls.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Open workspace search selections inside the search tab

Selecting a workspace from search results previously committed the draft
query, transitioned to the Workspaces tab, and pushed the detail onto
that tab's NavigationStack from onChange handlers that can run while the
stack is off-window mid search-dismissal. UIKit drops such pushes: the
path records the detail, so the root list stays up with the tab bar,
root toolbar, and compose button hidden and the committed query still
filtering the list, with no control left to escape (the reported stuck
state). Notification search never had this bug because it pushes results
onto its own stack.

Workspace search now does the same: the search tab's stack gets a real
path plus a workspace navigationDestination, and a tapped result pushes
there directly with no tab transition and no query commit, so popping
back lands on the live search results and the Workspaces tab keeps its
unfiltered list.

The pending cross-tab machinery still serves deeplinks and device-tree
selections made while searching; those consumes are now gated on the
destination stack being on screen (set by its onAppear/onDisappear), so
a deferred push replays from onAppear instead of landing off-window.

The layout-preview fixture mirrors the new shape (path-based search
stack, system back), and the minimized-search UI test is updated to the
new contract: popping back returns to search results, and the committed
filter check moves behind an explicit switch to the Workspaces tab.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Do not assert system search chrome over the pushed search detail

Whether iOS keeps the bottom search control visible above a detail
pushed inside the search tab is platform chrome, not part of the
selection contract; asserting its disappearance made the minimized-
search test fail against the in-stack push flow.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Name each assertion in the minimized-search test

The hosted runner only surfaces assertion messages, so each step of the
updated search-selection flow carries one to make failures diagnosable.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Fail fast with named steps in the minimized-search flow

The hosted runner surfaces XCTFail messages as annotations but not
XCTAssert messages; guard-and-fail makes the first broken step visible.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Expect the live search session after popping the selected workspace

Popping the detail returns to the active search (field and keyboard
restored), so the keyboard-dismissal expectation moves to the explicit
leave-search tab switch. Matches the verified on-device behavior.

Co-Authored-By: Claude Fable 5 <[email protected]>

* End the search session when a result is selected

Left presented across the push, the search field re-presents after
popping anchored to the navigation bar at the top instead of the search
tab's bottom control (reported from device dogfood). Deactivating on
select commits the query like every other search exit, so popping back
lands on the still-filtered results with the collapsed bottom control.
The regression test now pins the restored control to the bottom half
and rejects a top-anchored field after popping.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Finish the search round on the Workspaces tab after popping the detail

Popping back from a workspace opened via search previously left the app
on the deactivated search tab: the tinted search control read as a live
search, and the committed query kept filtering a list that looks
identical to the Workspaces root. Popping now returns selection to the
Workspaces tab and clears the committed query, so the search round ends
with the full list and no highlighted control. An explicit submit still
commits the query as the Workspaces filter; the minimized-search test
now exercises that path directly.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-08 20:15:12 -07:00
austinpower1258 b73d704eba test: cover exact window dock bell navigation 2026-08-08 20:05:56 -07:00
Abdulaziz AlbaharandClaude Fable 5 0883e93109 iOS: custom sort for the All Computers workspace list (#9828)
* feat(ios): custom sort for the All Computers workspace list

Workspaces from different computers have no deterministic cross-Mac order
of their own, so make the aggregate order a device-local choice with three
modes: Automatic (foreground Mac first, then name — the old behavior),
Computer Order (user drags computers into a priority order; each computer
keeps its own sidebar order), and Recent Activity (one flat list across
every computer, latest lastActivityAt first).

Computer Order runs in the aggregation (through the macIDsInDisplayOrder
seam) so group sections and workspaces reorder together. Recent Activity
runs at the presentation layer only, because time interleaving cannot keep
group members contiguous; it presents flat and disables drag reorder (a
derived order has no spatial move to send).

The preference persists in MobileWorkspaceSortStore (injected UserDefaults,
mirroring the group-collapse store) and is exposed through a Sort By picker
in the workspace list filter menu, All Computers scope only, plus a
drag-to-reorder Computer Order sheet. First-time picks of Computer Order
auto-open the editor. Strings localized en+ja.

Co-Authored-By: Claude Fable 5 <[email protected]>

* test(ios): env-seed the layout fixture's sort mode for headless sim verification

Simulator windows on another Space cannot be tap-driven without stealing
the user's display, so the fixture accepts
CMUX_UITEST_WORKSPACE_LIST_PREVIEW_SORT and ..._SORT_PRIORITY to render
each All Computers sort mode for simctl screenshot verification, matching
the existing COUNT/GROUPS seeding knobs.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): sort menu gates on known computers, not visible machines

The sort section hid whenever fewer than two computers had workspace rows
on screen, so a paired-but-offline (or connection-wedged) secondary Mac
made the control undiscoverable exactly when cross-computer order matters.
Gate on distinct known computers instead: machines with visible rows plus
every paired Mac. The computer-order editor now also lists paired-but-
offline computers so they keep their slot while disconnected.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): show the sort menu regardless of computer count

The preference is worth setting before a second computer pairs, and any
count gate hides the control behind connection state (a wedged secondary
Mac already did once). All Computers scope alone decides visibility now.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(ios): Mail-style illustrated sort tiles in a view-options card

Sort hidden inside a filter-icon menu was undiscoverable, and a text-only
'Computer Order' label communicates nothing. Replace the filter Menu with
a menu-styled popover whose top row is three drawn schematic tiles, each a
miniature of what the mode does to the list (computer sections; ranked
sections with drag grips; a flat time-stamped run), with Mail-style radio
checks. Selection re-sorts the list live behind the card. The computer-
order editor presents from the card itself, and the filter rows (read
state, machines) share it, keeping one entry point. UIMenu rows only
render text plus icon-sized images, hence a popover rather than a Menu.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): view-options card round 2 per dogfood

Drop the machines filter section (the title picker already owns computer
selection), make selecting Computer Order inert (the editor opens only
from the explicit Edit Computer Order row), inset the schematic rank
badges off the tile border, and use regular-weight selection checkmarks.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): pin the view-options card to regular-weight body text

The popover inherits the presenting toolbar button's font environment, so
rows rendered with toolbar weight; set body + regular at the card root.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): indent schematic rank badges further, shrink edit-row arrows

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): rename the Automatic sort mode to Connected First

'Automatic' says nothing; 'Connected First' states the rule: the connected
computer leads, the rest follow by name. Raw value and persistence keep
the automatic spelling.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(ios): automatic sort mode becomes Last Opened

'Automatic'/'Connected First' mislabeled the rule and the alphabetical
tail matched no user model. The mode is now Last Opened: the connected
computer counts as opened now, the rest order by when this device last
used them (pairing lastSeenAt as the device-local record), never-opened
computers alphabetical last. Raw value and persistence keep 'automatic'.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): review round — recompute on pairing refresh, robust editor seed

Cursor: the Last Opened order read pairedMacs.lastSeenAt but a pairing
refresh never rebuilt the derived list, so the aggregate order went stale
until the next workspace event; recompute on pairedMacs change (the
derivation already depended on pairedMacs for customization stamping).
Also seed the computer-order editor from the aggregated rows instead of
the filter-menu machine list, which empties below its two-machine floor
and would drop a singleton computer or mismatch the tail order.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-08 20:01:11 -07:00
austinpower1258 a8132adcdd fix: bound restored diff viewer requests 2026-08-08 19:48:40 -07:00
Abdulaziz AlbaharandClaude Fable 5 ba47b1dc0d Keep the iOS terminal dock pinned during keyboard reversals (#9836)
* Add standalone iOS keyboard pinning lab

* Test rapid iOS keyboard dock reversals

* Unify iOS keyboard dock presentation

* Fix CLI compile break from classify() tuple access

https://github.com/manaflow-ai/cmux/pull/9804 landed
`FeedEventClassifier.classify(...).0` while classify() already returned
the named FeedEventClassification struct, so CLI/cmux.swift no longer
compiles on main (every app-host and tests-build-and-lag CI job fails
with "value of type 'FeedEventClassification' has no member '0'").
Use .hookEventName, matching the other call site.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Strengthen rapid keyboard dock coverage

* test(panes): drop stale MobileInjectedAttachStartupTests referencing removed API

The main merge replaced MobileStartupConnectionCoordinator's
connectInjectedAttach with the claim/finish lifecycle, and
DogfoodAttachPreparationTests already covers that lifecycle end to end.
The stale file kept the whole CmuxMobileShellUITests target from
compiling, so no package UI suite could run in CI.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Scope pairing scanner guidance copy onto MobilePairingScannerSheet

The caseless MobilePairingScannerGuidanceCopy enum (from #9493) trips the
namespace-enum rule in scripts/lint-ios-package-conventions.sh, turning the
package-conventions-lint job red for every branch that touches Packages/.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Scope keyboard dock seam measurement to transitions

* Scope dock seam metric to keyboard transitions

* Test whole dock during keyboard reversal

* Isolate keyboard dock from terminal layout

* Animate hosted keyboard dock reflows

* Localize keyboard pinning lab name

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-08 19:47:24 -07:00
24cb551c9f Inline notification replies (macOS + iOS) with schema-driven reply shapes and a notification debug mode (#8670)
* Add inline notification replies and debug mode

* Add iOS inline replies for terminal pushes

* Address review: serialize question-category writes, live-retarget replies, iOS reply re-park

- FeedCoordinator: all CMUXFeedQuestion.* category get->set round trips now
  append to one MainActor-serialized chain so a mint racing a mint or a
  cancel can no longer clobber the other's setNotificationCategories write
  (Greptile P1, CodeRabbit TOCTOU).
- Banner text replies resolve the live surface owner via
  agentNotificationDeliveryTarget before surface.send_text and route with
  the resolved workspace_id, failing closed when the surface is gone.
- iOS: a failed inline-reply RPC re-parks the reply (original createdAt, so
  the 120s TTL still bounds retries) instead of dropping it; a newer reply
  parked mid-send still wins.
- Debug: caller-target resolution moved behind the shared production seam
  resolvedCallerNotificationTarget; DEBUG-only param parsing lives in
  NotificationDebugTarget.swift and the resolver's helpers are private
  again. debug.notification.emit fails closed for feed kinds without a
  resolved target. Missing-param errors now say what to pass (EN+JA).

Co-Authored-By: Claude Fable 5 <[email protected]>

* Debug: add debug.notification.status verb reporting system notification settings

Reports authorizationStatus, alertStyle, and per-surface settings straight
from UNUserNotificationCenter so authorization problems on a dev build are
diagnosable over the socket.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Fix post-merge compile: replyShape in new PhonePushPayload call sites

Co-Authored-By: Claude Fable 5 <[email protected]>

* Debug: local reply-notification emitter on iOS Settings

A DEBUG-only button in Settings > Push Alerts schedules a LOCAL notification
carrying the same cmux.terminal.reply category and cmux userInfo schema as a
Mac-forwarded push, addressed at the selected workspace/terminal. The response
path cannot tell local from remote, so the inline Reply UX, parking, and the
terminal.input RPC back to the Mac are verifiable on a device without APNs —
dev web deployments have no push service configured.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Debug Macs default the push lane to shared staging

Dev iPhones register APNs tokens with the staging deployment (the device
rig default), so a Debug Mac posting pushes to its tag-local localhost
port can never deliver: that origin has no token registry, and every
forward died queued. Route /api/notifications/* through a push-specific
base that mirrors irohBrokerBaseURL: explicit CMUX_PUSH_API_BASE_URL or
VM-API overrides win, Debug defaults to staging, Release keeps the
production VM-API origin.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Push lane ignores the rig-baked VM origin

The tag rig bakes a localhost CMUX_VM_API_BASE_URL into every Debug
bundle's LSEnvironment, so deferring to that knob re-broke the push lane
on every fresh build. Only an explicit CMUX_PUSH_API_BASE_URL (env or
~/.cmux-dev.env) overrides the Debug staging default now.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Log per-attempt push delivery status

The queue only logged terminal outcomes, so a failing rig read as opaque
invalid_response/retry_exhausted lines with no way to tell a redirect from
a decode mismatch from a transport error. Log host, HTTP status, byte
count, and classification per attempt (never content).

Co-Authored-By: Claude Fable 5 <[email protected]>

* Fix nonisolated access to logValue in delivery attempt log

Co-Authored-By: Claude Fable 5 <[email protected]>

* Default replyShapeWire in control notification witnesses

Main added direct test call sites for the control notification entrypoints
that predate the reply-shape parameter; a nil default keeps every legacy
caller source-compatible while the socket dispatcher still passes the wire
value through.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address cursor Mediums: preserve minted categories on install, retry parked reply while channel is down

- The launch/category (re)install now merges live CMUXFeedQuestion.*
  categories through the new bounded read instead of replacing the whole
  set, so a re-configure can no longer strip a live question banner's
  option buttons. Regression test included.
- A reply parked because the RPC channel is unavailable arms the same
  bounded retry ladder as a failed send, so a channel that recovers
  without emitting a store event cannot strand the reply until TTL.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: cmux reload-cloud <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-08 19:12:38 -07:00
austinpower1258 4e2603af45 fix: cancel diff viewer picker commands 2026-08-08 19:07:55 -07:00
austinpower1258 5f19f1682f fix: reconcile focus after drag-to-split 2026-08-08 18:36:01 -07:00
austinpower1258 a315547cba fix: keep dock unread lookup constant-time 2026-08-08 18:32:36 -07:00
austinpower1258 5cf87c893b fix: preserve dock terminal bell unread state 2026-08-08 18:19:14 -07:00
austinpower1258 c478fc8fd6 Merge remote-tracking branch 'origin/main' into issue-9648-diffviewer-deadlock 2026-08-08 18:10:33 -07:00
austinpower1258 ff63cb3a2d fix: bound diff viewer asset decoding 2026-08-08 18:10:08 -07:00
austinpower1258 6c42f973fa test: cover dock terminal bell unread state 2026-08-08 18:06:07 -07:00
Abdulaziz AlbaharandClaude Fable 5 cea3a768dd Stop redundant Iroh registration publication (#9350)
* Add IROH client refresh coalescing regression tests

* Coalesce IROH client registration refreshes

* Test unchanged Iroh events avoid broker traffic

* Publish Iroh registration only when reachability changes

* Stress unchanged Iroh endpoint events

* Exercise sustained Iroh endpoint churn

* Cover Iroh publication fingerprint changes

* Add failing test: raced live discovery must still read the broker

A live discovery that observes an in-flight unchanged-fingerprint refresh
and its coalesced successors can return .refreshed without any
authoritative broker read. TestIrohEndpoint gains an armable address()
gate so each raced refresh is deterministically held in flight.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Keep raced live discovery authoritative across coalesced no-op refreshes

Observing a coalesced successor no longer forfeits the live discovery
request's right to schedule one discovery-forced refresh, and a
no-op .refreshed outcome without a generation advance no longer
satisfies the request or masks an earlier real failure.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address review quick wins: skip signing on read-only refreshes, cover IPv6 ports

Payload signing now happens only after the read-only eligibility gate
declines, so the read-only fast path no longer performs a discarded
signature. The publication-state test also pins IPv6-only direct-port
changes as requiring publication.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Add failing test: host sign-out must clear publication state

Co-Authored-By: Claude Fable 5 <[email protected]>

* Clear host publication state on successful sign-out

Mirrors the client sign-out path so a stale fingerprint cannot suppress
the next session's non-forced publications.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-08 18:01:47 -07:00
austinpower1258 572f229d98 fix: bind coalesced titles to runtime lifecycle 2026-08-08 17:49:13 -07:00
austinpower1258 f6bd09fd40 test: prove stale teardown fallback lane 2026-08-08 17:49:05 -07:00
austinpower1258 0d5e6e6151 Merge remote-tracking branch 'origin/main' into issue-9520-hermes-first-class
# Conflicts:
#	Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface+ClaudeCommandShimLifecycle.swift
#	Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Surface/TerminalSurface.swift
2026-08-08 17:45:02 -07:00
austinpower1258 38207a4909 test: reject titles from hibernated runtimes 2026-08-08 17:38:47 -07:00
austinpower1258 f5b268c8fd fix: bound terminal bell flash work 2026-08-08 17:38:44 -07:00
austinpower1258 47b968be9f Merge remote-tracking branch 'origin/main' into issue-9220-sidebar-click-hang
# Conflicts:
#	Packages/macOS/CmuxTerminal/Tests/GhosttyRuntimeTestStubs/include/GhosttyRuntimeTestStubs.h
2026-08-08 17:38:10 -07:00
austinpower1258 40b27969e4 fix: preserve Hermes TUI resume identity 2026-08-08 17:19:10 -07:00
austinpower1258 f3dd028b05 test: cover Hermes TUI approval resume identity 2026-08-08 16:49:27 -07:00
austinpower1258 8c42a40ffa Merge remote-tracking branch 'origin/main' into issue-9648-diffviewer-deadlock 2026-08-08 16:45:02 -07:00
austinpower1258 e0bdf48c5a test: keep terminal bell fixture local 2026-08-08 16:43:17 -07:00
austinpower1258 a03080d64a fix: prevent diff viewer scheme deadlock 2026-08-08 16:38:00 -07:00
austinpower1258 769a002c07 test: make cached Iroh renewal deterministic 2026-08-08 16:37:26 -07:00
austinpower1258 2962fa83ec fix: require terminal bell ownership 2026-08-08 16:35:28 -07:00
Austin Wang f9a29b4806 Merge pull request #9798 from manaflow-ai/issue-9495-dblclick-rename-commit
Fix AppKit sidebar double-click inline rename committing instantly (#9495)
2026-08-08 16:31:55 -07:00
austinpower1258 a7c7a0b186 fix: preserve in-app terminal bell attention 2026-08-08 16:28:43 -07:00
austinpower1258 708d7b8146 Merge branch 'main' into issue-9588-lightmode-text-contrast 2026-08-08 16:27:23 -07:00
austinpower1258 d1821d920c Merge remote-tracking branch 'origin/main' into issue-9337-dock-terminal-live-title
# Conflicts:
#	Sources/Panels/FilePreviewPDFSharingPresenter.swift
#	scripts/ci/run-app-host-xcodebuild.sh
#	tests/test_ci_app_host_xcodebuild_retry.sh
2026-08-08 16:24:00 -07:00
Austin Wang 560d347b58 Merge pull request #9808 from manaflow-ai/issue-9583-korean-nfc-nfd-font
Resolve NFD Hangul clusters via canonical composition for font selection
2026-08-08 16:10:52 -07:00
Austin Wang f9cb0df9a8 Merge pull request #9800 from manaflow-ai/issue-9591-hibernation-omp-sighup
Scope OMP lifecycle hooks to the pane-owning top-level session
2026-08-08 16:08:35 -07:00
Austin Wang bd19583aad Merge pull request #9796 from manaflow-ai/issue-9769-cli-burst-pty-wedge
Make cold PTY spawn unstarvable: reclaim bootstrap-window custody, bound the shim gate, unpin prime slots, surface queued sends (#9769)
2026-08-08 15:59:34 -07:00
Austin Wang a9b3bb2e3a Merge pull request #9777 from manaflow-ai/issue-9550-invisible-ax-windows
Stop AX snapshots from retaining transient windows
2026-08-08 15:56:46 -07:00
Austin Wang 3148f79410 Merge pull request #9794 from manaflow-ai/issue-9677-browser-cmdz
Fix browser pane Cmd+Z/Cmd+Shift+Z: perform undo/redo on a per-web-view undo manager
2026-08-08 15:31:52 -07:00
lawrencecchen feb726043b Pin safe Ghostty cursor replay 2026-08-08 00:26:52 -07:00
lawrencecchen 5fc83a619f Pin emitted-state cursor replay fix 2026-08-08 00:20:46 -07:00
lawrencecchen c7d60a7ac7 style(tui): apply hosted CLI format 2026-08-08 00:18:46 -07:00
lawrencecchen fe152ab641 Fence journal commits and provider snapshots 2026-08-08 00:18:10 -07:00
lawrencecchen aa4f83ec6b Verify Ghostty cursor replay on hosted runners 2026-08-08 00:16:48 -07:00
lawrencecchen a2d0c77650 Pin reviewed Ghostty tabstop replay coverage 2026-08-08 00:15:07 -07:00
lawrencecchen b79608cb6d test(tui): update hosted CLI contracts 2026-08-08 00:14:33 -07:00
lawrencecchen 484e10874c Own forced cleanup in CLI test fixtures 2026-08-08 00:10:35 -07:00
lawrencecchen 1288cb62a9 Close final journal deadline review findings 2026-08-08 00:05:54 -07:00
lawrencecchen bf2f5d4e2e Apply hosted Rust formatting 2026-08-08 00:04:42 -07:00
lawrencecchen 666fd27fa2 Test TUI CLI cleanup failures precisely 2026-08-07 23:58:49 -07:00
lawrencecchen 79a1253b8d Make oversized frame test platform neutral 2026-08-07 23:54:52 -07:00
lawrencecchen 28277c23be Apply hosted Rust formatting 2026-08-07 23:54:18 -07:00
lawrencecchen f11f490ce3 Include registry admission in journal deadlines 2026-08-07 23:50:11 -07:00
lawrencecchen 2d628601ae test(tui): accept reset after oversized frame 2026-08-07 23:46:23 -07:00
lawrencecchen 20edfd5ea2 Keep SQLite waits inside journal deadlines 2026-08-07 23:42:17 -07:00
lawrencecchen d5feb39b8b test(tui): accept oversized-frame disconnect race 2026-08-07 23:41:27 -07:00
lawrencecchen a482343a7d Fix CLI terminal host fixture after merge 2026-08-07 23:40:07 -07:00
lawrencecchen 265ffadabd Bound journal retries and unblock frontend events 2026-08-07 23:37:07 -07:00
lawrencecchen db20c03d33 Fix terminal host reset fixtures after merge 2026-08-07 23:32:49 -07:00
lawrencecchen 0694e9d5a5 test(tui): update merged host record fixtures 2026-08-07 23:28:09 -07:00
lawrencecchen 00f0b7b30a Bound journal shutdown and close hosted test gaps 2026-08-07 23:25:39 -07:00
lawrencecchen 0e4d869fb1 fix(cli): close owner reload receiver on exit 2026-08-07 23:25:02 -07:00
lawrencecchen fd615d5e4d Merge remote-tracking branch 'origin/main' into task-hosted-cmux-tui-verification 2026-08-07 23:24:44 -07:00
lawrencecchen 5e5220616c test(cli): cover owner event loop wake 2026-08-07 23:23:49 -07:00
lawrencecchen ab05234d6f Serialize hosted TUI integration tests 2026-08-07 23:22:40 -07:00
lawrencecchen 9af268b301 style(tui): apply hosted rustfmt 2026-08-07 23:21:11 -07:00
lawrencecchen 3eca049dca fix(cli): preserve lifecycle shutdown fences 2026-08-07 23:18:06 -07:00
lawrencecchen 5639228d24 test(cli): cover lifecycle shutdown fences 2026-08-07 23:16:17 -07:00
lawrencecchen a752a1f82d Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-07 23:15:47 -07:00
lawrencecchen 158f4c6eda fix(cli): flush shutdown reply before owner exit 2026-08-07 23:13:36 -07:00
lawrencecchen a388069d8f Scope hosted memory checks to startup ownership 2026-08-07 23:09:44 -07:00
lawrencecchen dcfbf2ca7e test(cli): cover shutdown ack flush ordering 2026-08-07 23:09:14 -07:00
lawrencecchen 0535e85726 Close exact-head shutdown and provider review findings 2026-08-07 23:08:56 -07:00
lawrencecchen 5bf6936165 test(tui): pipe helper output in drain coverage 2026-08-07 22:39:11 -07:00
lawrencecchen f951e96b82 Make hosted TUI cleanup checks deterministic 2026-08-07 22:32:58 -07:00
lawrencecchen fd5d79a300 fix(cli): isolate owner reload delivery 2026-08-07 22:32:52 -07:00
lawrencecchen d1021fbbc3 test(cli): cover reload mailbox overflow 2026-08-07 22:32:10 -07:00
lawrencecchen e7328c5d78 Apply hosted Rust formatting and repair C++ parser 2026-08-07 22:30:45 -07:00
lawrencecchen b6e4b9fb09 fix(cli): unify local shutdown completion 2026-08-07 22:28:46 -07:00
lawrencecchen 674bf97eef test(cli): cover interactive session shutdown 2026-08-07 22:27:40 -07:00
lawrencecchen 21dcb7a7a2 Close final journal review findings 2026-08-07 22:27:34 -07:00
lawrencecchen 0b5429a759 fix(tui): clear final hosted test gates 2026-08-07 22:25:25 -07:00
lawrencecchen af26d64e79 Pin corrected Ghostty cursor replay 2026-08-07 22:19:03 -07:00
lawrencecchen 58d8650a4f test(cli): assert lifecycle timeout bounds 2026-08-07 22:18:53 -07:00
lawrencecchen e2c85b15c8 fix(cli): reject machine lifecycle targeting 2026-08-07 22:17:07 -07:00
lawrencecchen f924eb6468 test(cli): reject machine lifecycle targeting 2026-08-07 22:16:32 -07:00
lawrencecchen f9e0467ed2 fix(cli): preserve remote help routing 2026-08-07 22:13:51 -07:00
lawrencecchen 36676067d2 test(cli): cover remote typo help routing 2026-08-07 22:13:14 -07:00
lawrencecchen 8f78b38a9c Close exact-head journal review findings 2026-08-07 22:08:49 -07:00
lawrencecchen 3f2fec28ab fix(cli): close final lifecycle review gaps 2026-08-07 22:08:33 -07:00
lawrencecchen 459345c375 test(cli): cover final lifecycle review gaps 2026-08-07 22:05:59 -07:00
lawrencecchen 4b029b7796 test(tui): update SDK event inventory counts 2026-08-07 22:04:51 -07:00
lawrencecchen 84f7450c32 fix(cli): signal interactive shutdown by disconnect 2026-08-07 22:03:16 -07:00
lawrencecchen fa2ded8847 Fix baseline terminal replay verification 2026-08-07 21:59:46 -07:00
lawrencecchen 07617e6b1c Validate watcher settings before dispatch 2026-08-07 21:59:13 -07:00
lawrencecchen 8eb60437ca Poll hosted verification at a safe rate 2026-08-07 21:58:33 -07:00
lawrencecchen 006210dd4b fix(cli): apply reloads in the local owner 2026-08-07 21:58:18 -07:00
lawrencecchen 2f18651bf9 Repair hosted root test build 2026-08-07 21:57:22 -07:00
lawrencecchen f0e37f88c5 test(cli): cover owner config reload dispatch 2026-08-07 21:57:21 -07:00
lawrencecchen a8624dfd86 fix(cli): stop interactive local owners 2026-08-07 21:55:52 -07:00
lawrencecchen 92e0a8b84d Verify hosted outcomes without timing assumptions 2026-08-07 21:55:35 -07:00
lawrencecchen 7a0f325fac test(cli): cover interactive owner stop 2026-08-07 21:54:57 -07:00
lawrencecchen e946cedeed fix(tui): type agent event state fields 2026-08-07 21:54:35 -07:00
lawrencecchen 7d4ca1d55e fix(cli): document embedded remote ownership 2026-08-07 21:52:24 -07:00
lawrencecchen 49c446ae42 test(cli): cover embedded remote stop ownership 2026-08-07 21:50:46 -07:00
lawrencecchen b70f33f67f Merge remote-tracking branch 'origin/main' into codex/cmux-browser-provider
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/lib.rs
#	cmux-tui/spec/cli.md
2026-08-07 21:49:23 -07:00
lawrencecchen 6867191b33 fix(tui): keep refreshed agents on live surfaces 2026-08-07 21:48:42 -07:00
lawrencecchen 4074ff2bc0 fix(cli): close lifecycle review findings 2026-08-07 21:47:09 -07:00
lawrencecchen ce9b7ade4b Keep secret filter test compile-only safe 2026-08-07 21:46:15 -07:00
lawrencecchen 4c38eb094f test(cli): cover lifecycle review findings 2026-08-07 21:45:48 -07:00
lawrencecchen b473634704 Close journal review findings 2026-08-07 21:45:12 -07:00
lawrencecchen 050264f057 Separate replay behavior from Valgrind 2026-08-07 21:42:33 -07:00
lawrencecchen bd4167ea77 fix(tui): expose layout overrides to tests 2026-08-07 21:35:21 -07:00
lawrencecchen acd04ea18c Sync root workspace lockfile from hosted metadata 2026-08-07 21:34:15 -07:00
lawrencecchen 6e93bd36d8 Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-07 21:33:32 -07:00
Lawrence Chen 219f2b971e Merge pull request #9733 from manaflow-ai/task-cmux-tui-saved-state-recovery
Fix cmux-tui incompatible session recovery
2026-08-07 21:33:21 -07:00
lawrencecchen c8bced6ccd Keep full replay coverage under Valgrind 2026-08-07 21:33:05 -07:00
lawrencecchen 246d0622ac Stabilize full hosted TUI suites 2026-08-07 21:32:34 -07:00
lawrencecchen 22215b71e3 fix(cli): isolate process routing from resource grammar 2026-08-07 21:32:17 -07:00
lawrencecchen 74e3e854fc Fix remaining root merge source defects 2026-08-07 21:30:56 -07:00
lawrencecchen 68693031fe fix(cli): bound lifecycle protocol responses 2026-08-07 21:28:12 -07:00
lawrencecchen 4f56d2f0de fix(tui): publish agent event SDK contract 2026-08-07 21:28:03 -07:00
lawrencecchen 060fcc1e2e Serialize Ghostty replay tests under Valgrind 2026-08-07 21:25:48 -07:00
lawrencecchen 5d8a34ced2 fix(cli): harden local server lifecycle grammar 2026-08-07 21:25:07 -07:00
lawrencecchen 4150f8ec06 test(cli): cover lifecycle recovery gaps 2026-08-07 21:24:53 -07:00
lawrencecchen f8b8452b60 fix(tui): clear hosted lint and inventory gates 2026-08-07 21:22:49 -07:00
lawrencecchen 40212bef4d fix(tui): satisfy hosted Linux Clippy 2026-08-07 21:22:44 -07:00
lawrencecchen 30a2e8a82c Tolerate hosted verification API errors 2026-08-07 21:19:01 -07:00
lawrencecchen ae12710402 Remove constant-only protocol test 2026-08-07 21:18:15 -07:00
lawrencecchen f6b9542ea1 Fix duplicated root merge test blocks 2026-08-07 21:17:34 -07:00
lawrencecchen 4d6f27337a Merge remote-tracking branch 'origin/task-cmux-tui-saved-state-recovery' into task-cli-lifecycle-grammar
# Conflicts:
#	cmux-tui/crates/cmux-tui/src/cli.rs
2026-08-07 21:16:04 -07:00
lawrencecchen 9ff4d5288e style(tui): apply hosted review format 2026-08-07 21:12:12 -07:00
lawrencecchen 3978f9a958 Match pinned cmux-tui rustfmt 2026-08-07 21:11:38 -07:00
lawrencecchen d6c8c930c9 Merge remote-tracking branch 'origin/main' into task-cli-lifecycle-grammar 2026-08-07 21:10:02 -07:00
lawrencecchen 44d66969fa perf(tui): keep reset host scan linear 2026-08-07 21:08:15 -07:00
lawrencecchen ee79f2c681 Address hosted cross-platform Clippy findings 2026-08-07 21:08:04 -07:00
lawrencecchen 8c3ad3c18c fix(tui): close reset safety review 2026-08-07 21:05:45 -07:00
lawrencecchen a4a685ab9f fix(tui): satisfy hosted clippy gates 2026-08-07 21:04:50 -07:00
lawrencecchen eb7f9744b2 Merge latest main into session journal root 2026-08-07 21:04:49 -07:00
lawrencecchen 38e85ade72 Fix root stack source contract regressions 2026-08-07 21:04:22 -07:00
lawrencecchen 4e796f5a01 Keep release verification on pinned Rust 2026-08-07 21:03:36 -07:00
lawrencecchen f5c75d74e4 Fix remaining hosted TUI Clippy findings 2026-08-07 21:02:52 -07:00
lawrencecchen fc4cab47c9 style(tui): apply hosted Rust format 2026-08-07 21:00:17 -07:00
lawrencecchen f7f1903d06 Pin hosted cmux-tui Rust toolchain 2026-08-07 20:59:50 -07:00
Lawrence Chen d95fbb38c3 Clarify temporary coderouter account status (#9838)
Show an accurate temporary-unavailability state without exposing internal migration details or incorrectly reporting a service connection failure.
2026-08-07 20:58:25 -07:00
lawrencecchen e461b1eeac test(tui): cover removed agent refresh 2026-08-07 20:57:30 -07:00
lawrencecchen 57cf263064 Merge latest main into session journal root 2026-08-07 20:54:52 -07:00
lawrencecchen b966954e71 Merge source reconciliation for session journal stack
# Conflicts:
#	cmux-tui/bindings/ERGONOMICS.md
#	cmux-tui/bindings/cpp/.cmux-resource-api.json
#	cmux-tui/bindings/cpp/.cmux-sdk-manifest.json
#	cmux-tui/bindings/cpp/include/cmux/raw/generated/models.hpp
#	cmux-tui/bindings/cpp/src/raw/generated/protocol.cpp
#	cmux-tui/bindings/go/.cmux-resource-api.json
#	cmux-tui/bindings/go/README.md
#	cmux-tui/bindings/go/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/go/raw/README.md
#	cmux-tui/bindings/go/raw/client_test.go
#	cmux-tui/bindings/go/raw/generated_commands.go
#	cmux-tui/bindings/go/raw/generated_events.go
#	cmux-tui/bindings/go/raw/generated_metadata.go
#	cmux-tui/bindings/go/raw/generated_presence_test.go
#	cmux-tui/bindings/go/raw/generated_types.go
#	cmux-tui/bindings/java/.cmux-resource-api.json
#	cmux-tui/bindings/java/src/com/cmux/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/java/src/com/cmux/raw/Protocol.java
#	cmux-tui/bindings/python/.cmux-resource-api.json
#	cmux-tui/bindings/python/cmux/raw/_generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/python/cmux/raw/_generated/_schema.py
#	cmux-tui/bindings/python/cmux/raw/_generated/metadata.py
#	cmux-tui/bindings/python/tests/test_protocol.py
#	cmux-tui/bindings/rust/.cmux-resource-api.json
#	cmux-tui/bindings/rust/src/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/rust/src/generated/commands.rs
#	cmux-tui/bindings/rust/src/generated/events.rs
#	cmux-tui/bindings/rust/src/generated/metadata.rs
#	cmux-tui/bindings/rust/src/generated/mod.rs
#	cmux-tui/bindings/rust/src/generated/types.rs
#	cmux-tui/bindings/typescript/.cmux-resource-api.json
#	cmux-tui/bindings/typescript/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/typescript/src/raw/generated/commands.ts
#	cmux-tui/bindings/typescript/src/raw/generated/events.ts
#	cmux-tui/bindings/typescript/src/raw/generated/index.ts
#	cmux-tui/bindings/typescript/src/raw/generated/metadata.ts
#	cmux-tui/bindings/typescript/src/raw/generated/types.ts
#	cmux-tui/bindings/zig/.cmux-resource-api.json
#	cmux-tui/bindings/zig/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/zig/src/raw/generated/protocol.zig
#	cmux-tui/crates/cmux-tui-core/src/resource_router.rs
#	cmux-tui/crates/cmux-tui-core/src/resource_router/content.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/resource_store.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/tests.rs
#	cmux-tui/docs/README.md
#	cmux-tui/docs/protocol.md
#	cmux-tui/spec/README.md
#	cmux-tui/spec/cli.md
#	cmux-tui/spec/commands.md
#	cmux-tui/spec/frontends.md
#	cmux-tui/spec/render.md
2026-08-07 20:54:49 -07:00
lawrencecchen 2632b0b2f5 fix(tui): align reset platform support 2026-08-07 20:54:44 -07:00
lawrencecchen 8ab5dafa50 Merge remote-tracking branch 'origin/main' into feat-cmux-tui-session-journal
# Conflicts:
#	.github/workflows/cmux-tui.yml
#	cmux-tui/Cargo.lock
#	cmux-tui/bindings/ERGONOMICS.md
#	cmux-tui/bindings/conformance/runner.py
#	cmux-tui/bindings/cpp/.cmux-resource-api.json
#	cmux-tui/bindings/cpp/.cmux-sdk-manifest.json
#	cmux-tui/bindings/cpp/include/cmux/raw/generated/models.hpp
#	cmux-tui/bindings/cpp/include/cmux/resource.hpp
#	cmux-tui/bindings/cpp/src/resource_models.cpp
#	cmux-tui/bindings/cpp/tests/test_resource.cpp
#	cmux-tui/bindings/examples/cpp-terminal-frontend/tests/frontend_test.cpp
#	cmux-tui/bindings/examples/java-ci-orchestrator/tests/com/cmux/examples/ci/FakeCmuxServer.java
#	cmux-tui/bindings/examples/python-agent-watchdog/tests/test_watchdog.py
#	cmux-tui/bindings/examples/python-dev-orchestrator/fake_cmux_server.py
#	cmux-tui/bindings/examples/rust-agent-dashboard/tests/fake_server.rs
#	cmux-tui/bindings/go/.cmux-resource-api.json
#	cmux-tui/bindings/go/operations.go
#	cmux-tui/bindings/go/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/go/raw/README.md
#	cmux-tui/bindings/go/raw/client_test.go
#	cmux-tui/bindings/go/raw/generated_commands.go
#	cmux-tui/bindings/go/raw/generated_events.go
#	cmux-tui/bindings/go/raw/generated_metadata.go
#	cmux-tui/bindings/go/raw/generated_presence_test.go
#	cmux-tui/bindings/go/raw/generated_types.go
#	cmux-tui/bindings/go/resource_api_test.go
#	cmux-tui/bindings/go/resources.go
#	cmux-tui/bindings/java/.cmux-resource-api.json
#	cmux-tui/bindings/java/src/com/cmux/Client.java
#	cmux-tui/bindings/java/src/com/cmux/Snapshots.java
#	cmux-tui/bindings/java/src/com/cmux/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/java/src/com/cmux/raw/Protocol.java
#	cmux-tui/bindings/java/tests/com/cmux/ResourceApiTest.java
#	cmux-tui/bindings/python/.cmux-resource-api.json
#	cmux-tui/bindings/python/cmux/models.py
#	cmux-tui/bindings/python/cmux/raw/_generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/python/cmux/raw/_generated/_schema.py
#	cmux-tui/bindings/python/cmux/raw/_generated/metadata.py
#	cmux-tui/bindings/python/cmux/resources.py
#	cmux-tui/bindings/python/tests/test_protocol.py
#	cmux-tui/bindings/python/tests/test_resource_api.py
#	cmux-tui/bindings/rust-sidebar/tests/runtime.rs
#	cmux-tui/bindings/rust/.cmux-resource-api.json
#	cmux-tui/bindings/rust/src/codec.rs
#	cmux-tui/bindings/rust/src/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/rust/src/generated/commands.rs
#	cmux-tui/bindings/rust/src/generated/events.rs
#	cmux-tui/bindings/rust/src/generated/metadata.rs
#	cmux-tui/bindings/rust/src/generated/mod.rs
#	cmux-tui/bindings/rust/src/generated/types.rs
#	cmux-tui/bindings/rust/src/resource/mod.rs
#	cmux-tui/bindings/rust/src/resource/model.rs
#	cmux-tui/bindings/rust/tests/mock_server.rs
#	cmux-tui/bindings/rust/tests/operation_reachability.rs
#	cmux-tui/bindings/typescript/.cmux-resource-api.json
#	cmux-tui/bindings/typescript/src/models.ts
#	cmux-tui/bindings/typescript/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/typescript/src/raw/generated/commands.ts
#	cmux-tui/bindings/typescript/src/raw/generated/events.ts
#	cmux-tui/bindings/typescript/src/raw/generated/index.ts
#	cmux-tui/bindings/typescript/src/raw/generated/metadata.ts
#	cmux-tui/bindings/typescript/src/raw/generated/types.ts
#	cmux-tui/bindings/typescript/src/resources.ts
#	cmux-tui/bindings/typescript/test/resource-api.test.ts
#	cmux-tui/bindings/zig/.cmux-resource-api.json
#	cmux-tui/bindings/zig/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/zig/src/raw/generated/protocol.zig
#	cmux-tui/bindings/zig/src/resource.zig
#	cmux-tui/crates/cmux-tui-core/src/mux.rs
#	cmux-tui/crates/cmux-tui-core/src/mux/resource_topology.rs
#	cmux-tui/crates/cmux-tui-core/src/resource.rs
#	cmux-tui/crates/cmux-tui-core/src/resource_api.rs
#	cmux-tui/crates/cmux-tui-core/src/resource_router.rs
#	cmux-tui/crates/cmux-tui-core/src/resource_router/auxiliary.rs
#	cmux-tui/crates/cmux-tui-core/src/resource_router/content.rs
#	cmux-tui/crates/cmux-tui-core/src/server.rs
#	cmux-tui/crates/cmux-tui-core/src/surface.rs
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_protocol.rs
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_runtime.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/resource_store.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/terminal_exit_store.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/tests.rs
#	cmux-tui/crates/cmux-tui-core/tests/browser_runtime.rs
#	cmux-tui/crates/cmux-tui/Cargo.toml
#	cmux-tui/crates/cmux-tui/src/app.rs
#	cmux-tui/crates/cmux-tui/src/cli/command.rs
#	cmux-tui/crates/cmux-tui/src/localization.rs
#	cmux-tui/crates/cmux-tui/src/main.rs
#	cmux-tui/crates/cmux-tui/src/pty_input.rs
#	cmux-tui/crates/cmux-tui/src/remote_runtime.rs
#	cmux-tui/crates/cmux-tui/src/session/mod.rs
#	cmux-tui/crates/cmux-tui/src/session/remote.rs
#	cmux-tui/crates/cmux-tui/src/session/tree.rs
#	cmux-tui/crates/cmux-tui/tests/cli.rs
#	cmux-tui/crates/cmux-tui/tests/terminal_host_recovery.rs
#	cmux-tui/docs/protocol.md
#	cmux-tui/scripts/test_check_resource_api_boundary.py
#	cmux-tui/spec/README.md
#	cmux-tui/spec/bindings.md
#	cmux-tui/spec/commands.md
#	cmux-tui/spec/frontends.md
#	cmux-tui/spec/inventory.json
#	cmux-tui/spec/native-frontend.md
#	cmux-tui/spec/resource-api-v2.md
#	cmux-tui/spec/resource-operations-v2.json
#	cmux-tui/spec/resource-operations-v2.md
#	cmux-tui/spec/terminal-host.md
2026-08-07 20:51:37 -07:00
Lawrence Chen 36c549ac3c Add coderouter team settings dashboard (#9835)
Rename the user-facing Subrouter dashboard to coderouter, add a chatmux-style account menu, and expose full Stack team settings under Billing.
2026-08-07 20:50:49 -07:00
lawrencecchen e101b726a8 Fix hosted cmux-tui Clippy failures 2026-08-07 20:50:44 -07:00
lawrencecchen 8e13261a3e test(tui): match quoted main reset command 2026-08-07 20:45:56 -07:00
lawrencecchen cf18a59d32 fix(tui): cancel connects and scope agent updates 2026-08-07 20:45:54 -07:00
lawrencecchen 01e274493d Use valid dry-run package versions 2026-08-07 20:43:53 -07:00
lawrencecchen 6f0da8e979 Isolate hosted verification runs 2026-08-07 20:43:43 -07:00
lawrencecchen aa9d5a9aa8 test(tui): cover main reset guidance 2026-08-07 20:42:29 -07:00
lawrencecchen 0a23fed15a Complete hosted TUI release verification 2026-08-07 20:41:45 -07:00
Abdulaziz AlbaharandClaude Fable 5 4dc1004766 Fix CLI compile break from classify() tuple access (#9834)
https://github.com/manaflow-ai/cmux/pull/9804 landed
`FeedEventClassifier.classify(...).0` while classify() already returned
the named FeedEventClassification struct, so CLI/cmux.swift no longer
compiles on main (every app-host and tests-build-and-lag CI job fails
with "value of type 'FeedEventClassification' has no member '0'").
Use .hookEventName, matching the other call site.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-07 20:40:50 -07:00
lawrencecchen dbff90070b fix(tui): satisfy hosted Clippy gates 2026-08-07 20:40:33 -07:00
Abdulaziz AlbaharandClaude Fable 5 8c766f7bfc Add iOS Move to Group context-menu picker for workspaces (#9779)
Long-pressing a workspace row now offers a Move to Group submenu (one
item per group on the workspace's Mac, current membership checked and
disabled) plus Remove from Group, so a workspace can join a group
without drag-and-drop. Selection routes through the same
MobileWorkspaceMovePolicy intent + optimistic joinGroupAtEnd path as
dropping a row onto a group, and reuses drag gating, so the picker can
never offer a move the drop path would reject. Covers both the UIKit
table pipeline and the SwiftUI row context menu.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-07 20:38:14 -07:00
lawrencecchen 307c33e27d Add hosted cmux-tui verification 2026-08-07 20:37:43 -07:00
lawrencecchen 2307470307 fix(tui): close reset recovery review findings 2026-08-07 20:33:02 -07:00
lawrencecchen dcf67be067 Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-07 20:22:18 -07:00
lawrencecchen 8154170461 fix(tui): close review feedback gaps 2026-08-07 20:22:09 -07:00
austinpower1258 f568ecb99b fix: bridge Hermes hooks into TUI gateway 2026-08-07 19:22:35 -07:00
austinpower1258 656823b0bd test: cover repeated Hermes TUI completions 2026-08-07 19:05:22 -07:00
Austin Wang 7cc1346904 Merge pull request #9760 from manaflow-ai/issue-9706-lease-gone-live-pty
Preserve live remote PTYs when relay lease disappears
2026-08-07 18:51:45 -07:00
austinpower1258 3e263ff6da fix: keep Hermes TUI watcher idle 2026-08-07 18:43:16 -07:00
austinpower1258 bbbc36769c test: bound Hermes TUI watcher CPU 2026-08-07 18:42:56 -07:00
austinpower1258 7d6ba1fc58 fix: bridge Hermes TUI lifecycle into cmux 2026-08-07 18:24:53 -07:00
Austin Wang 59c45b35e2 Merge pull request #9804 from manaflow-ai/issue-9592-codex-permission-notify
Notify on codex PermissionRequest: new nativeApprovalPrompt feed semantic raises the agentPermissionPrompt alert
2026-08-07 18:15:11 -07:00
Lawrence Chen 408ae46e35 Stop creating dev-artifact issues for E2E runs (#9832)
* Stop creating issues for E2E runs

* Handle unavailable E2E recordings
2026-08-07 18:01:34 -07:00
lawrencecchen 9328fbe51d feat(cli): add local server lifecycle grammar 2026-08-07 17:59:29 -07:00
lawrencecchen 7e5d89e943 test(cli): cover local server lifecycle grammar 2026-08-07 17:59:04 -07:00
lawrencecchen 7dfb4c1c1e fix(tui): avoid eager machine connections 2026-08-07 17:52:30 -07:00
lawrencecchen caba0cca6a fix(tui): address resource rail review feedback 2026-08-07 17:30:33 -07:00
austinpower1258 3d3a4e16ac test: cover Hermes TUI lifecycle bridge 2026-08-07 17:25:59 -07:00
lawrencecchen 672c2a1e84 fix(tui): keep smoke protocol in sync 2026-08-07 17:21:48 -07:00
lawrencecchen 4c372e152d fix(tui): stabilize resource rail focus 2026-08-07 17:20:21 -07:00
lawrencecchen 5e33738147 fix(tui): return from active resource rails 2026-08-07 17:04:19 -07:00
lawrencecchen eaa60e497d Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns
# Conflicts:
#	cmux-tui/Cargo.lock
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_protocol.rs
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_runtime.rs
#	cmux-tui/crates/cmux-tui/src/app.rs
#	cmux-tui/crates/cmux-tui/src/config.rs
#	cmux-tui/docs/machines.md
#	cmux-tui/spec/inventory.json
#	cmux-tui/spec/terminal-host.md
2026-08-07 17:03:40 -07:00
austinpower1258 0ab2fe6ec7 Merge remote-tracking branch 'origin/main' into issue-9520-hermes-first-class 2026-08-07 16:47:03 -07:00
austinpower1258 83e69db83b fix: close Hermes live alias and quit watchdog regressions 2026-08-07 16:46:46 -07:00
lawrencecchen d6dee4aed2 fix: harden reset recovery edge cases 2026-08-07 04:37:38 -07:00
austinpower1258andClaude Fable 5 f232177ace Give the CLI queued-send test CI-load-tolerant timeouts
The tolerant full-suite app-host pass runs this suite alongside dozens
of parallel suites; a 5s subprocess bound killed the CLI mid-startup
(status 15, empty stdout) while the idle focused gate passed. 30s keeps
the test deadline-bounded without racing runner load.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 04:16:19 -07:00
austinpower1258 82df26a12d test(ci): cover published app-host temp aliases 2026-08-07 04:10:05 -07:00
lawrencecchen b128fd2946 fix: document apple reset stat fields 2026-08-07 04:04:47 -07:00
lawrencecchen cd2e3ca5ab fix: detect reset path recreation 2026-08-07 03:57:52 -07:00
austinpower1258 c558c52ec3 Merge remote-tracking branch 'origin/main' into issue-9550-invisible-ax-windows 2026-08-07 03:57:21 -07:00
austinpower1258andClaude Fable 5 fdc8a22c2f Reserve deadline budget for the essential attention send
Review flagged that the optional live-target probe received the entire
remaining attention deadline: a stalled probe could consume the whole
budget and starve the notify/clear send it exists to serve. The probe is
now capped (1s) and always leaves a send reserve (0.75s) of the shared
deadline; when the remaining budget cannot fund both, the probe is
skipped and the command falls back to ambient addressing.

New behavior test: with the fake stalling agent.resolve_delivery_target
for 3s (past the whole deadline), the notification is still written,
addressed to the ambient identities. FakeCmuxSocket now keeps draining
buffered request lines when its replies hit a closed peer — matching the
real app's per-connection worker, which reads written lines after the
hook process exits (verified pi suite unaffected).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 03:47:57 -07:00
austinpower1258 d43a6260b6 fix: inherit simulator modules from test host 2026-08-07 03:47:36 -07:00
lawrencecchen 35df85e278 fix: harden reset ownership checks 2026-08-07 03:46:23 -07:00
austinpower1258 143902c821 Canonicalize isolated app-host config paths
Treat macOS /tmp and /private/tmp spellings as the same validated app-host home.
2026-08-07 03:37:11 -07:00
austinpower1258 3e185e1dfa fix: let app host own static simulator linkage 2026-08-07 03:36:49 -07:00
austinpower1258andClaude Fable 5 e5b68203e3 Resolve the live pane before addressing the attention command
Review flagged that the attention notify/clear was a plain V1 command
built from ambient env identities: on a restored remote pane those are
snapshot aliases, and the relay remaps IDs only inside JSON requests, so
the command would target a stale pane and the blocked agent stayed
silent on restored remote terminals.

The attention delivery now resolves the live identity first through the
alias-safe `agent.resolve_delivery_target` {surface_id} re-home probe —
the same contract Claude's hooks use; the probe's JSON request IS
relay-remapped, so the app answers with live identities — and addresses
the V1 command to the answer, falling back to the ambient identities
when the probe is unsupported or fails (correct for local panes). The
probe, connect, auth, and acknowledged send all share the one absolute
2s deadline.

New behavior test: with the fake resolving the ambient surface to a
re-homed (workspace, surface) pair, the notification must target the
resolved pair and never the ambient identities.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 03:35:47 -07:00
lawrencecchen ed857c34ee fix: fail closed for unsupported reset deletion 2026-08-07 03:31:29 -07:00
austinpower1258 6442db3d3d fix: keep simulator products statically linked 2026-08-07 03:26:35 -07:00
austinpower1258andClaude Fable 5 e98c53f8b5 Give the attention transport a real deadline across connect/auth/send
Review correctly flagged that the essential notify/clear connection
reused the telemetry lane's 50ms fast-fail bounds: a relay-backed
socket's multi-round-trip HMAC handshake (or a busy local socket) could
never finish inside them, so remote terminals silently lost the
permission notification. The attention transport now runs under one
absolute deadline (feedAttentionAcknowledgeTimeoutSeconds) spanning
connect, authentication, and the acknowledged send; the telemetry lane
keeps its deliberate fast-fail bounds.

New behavior test: with the fake socket delaying every reply (including
auth) by 0.5s under a socket password, the notification still delivers —
a fast-fail transport drops it.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 03:24:30 -07:00
lawrencecchen 9c9602eab4 fix: rewind reset directory scans 2026-08-07 03:21:01 -07:00
austinpower1258 872e31fd48 test: cover Hermes live alias and quit watchdog regressions 2026-08-07 03:18:24 -07:00
austinpower1258andClaude Fable 5 5fcda4e59a Add CLI-level regression tests for the permission-prompt delivery path
Review noted the added coverage stopped at classification and pure command
construction — a misrouted promptLine dispatch would restore the silent
agent while every unit test stayed green. Add a focused behavior suite
that spawns the real CLI against the existing FakeCmuxSocket harness and
asserts, on the actual socket transport:

- codex PermissionRequest emits the exact gated notify_target_async line
  and it precedes the feed.push telemetry frame;
- codex PostToolUse emits the exact pane-scoped clear_notifications line
  before its telemetry frame;
- codex PreToolUse emits neither (no premature clear, no over-notify);
- the hook AWAITS the app's acknowledgement: with the fake delaying its
  OK by 0.5s, a fire-and-forget regression would return instantly.

Verified red/green: the suite fails against the pre-fix release CLI
0.64.22 ("missing gated permission notification") and passes against this
branch's build. Wired into ci.yml beside the other CLI hook suites.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 03:15:56 -07:00
Lawrence Chen 6089fa04d3 Fix cmux-tui startup config latency (#9738)
* test: cover cmux-tui startup config resolver

* fix: avoid Ghostty resolver during cmux-tui startup

* test: isolate cmux-tui config env in startup guard

* fix: preserve Ghostty resource theme startup defaults

* fix: resolve Ghostty config includes without startup resolver

* fix: preserve Ghostty theme fallback semantics

* fix: honor macOS light theme detection

* fix: keep Ghostty explicit overrides above themes

* fix: defer Ghostty theme resolution until includes load

* fix: preserve Ghostty config path semantics

* fix: bound Ghostty config fallback traversal

* fix: bound Ghostty config startup work

* fix: bound Ghostty config helper process

* fix: preserve Ghostty helper cursor styles

* fix: preserve Ghostty window theme modes

* fix: detect non-macOS Ghostty system theme

* fix: keep Ghostty file fallback after helper failure

* fix: keep Ghostty helper timeout bounded

* fix: bound Ghostty helper cleanup and reads

* fix: keep ghostty helper budget total

* fix: keep ghostty fallback on helper miss

* fix: isolate desktop theme probe cleanup

* fix: harden ghostty helper entrypoint

* fix: kill ghostty helper descendant groups

* fix: bound ghostty helper process scan

* fix: reap timed out ghostty process scans

* fix: bound ghostty helper output drains

* fix: select ghostty terminal themes from system appearance

* fix: skip appearance probes for fixed ghostty themes

* fix: cache ghostty conditional theme mode

* fix: check ghostty theme env before platform probe
2026-08-07 03:12:53 -07:00
austinpower1258andClaude Fable 5 869da76ed3 Address review: reopen shim gate after lifecycle cancel, harden CLI test, reword queued marker
- A deadline-released spawn marks the shim install completed without a
  shim; cancelling the in-flight install (teardown, agent-hibernation
  suspend) now reopens the gate so the next runtime creation attempts a
  fresh install instead of running shim-less forever (Bugbot finding).
  Covered in the hung-shim regression test.
- CLI queued-send test: pin the child CLI to an English locale and fail
  loudly when the mock socket server does not complete (CodeRabbit).
- Reword the queued marker to "queued (terminal starting; input will be
  sent when its PTY is ready)" in the catalog and the CLI default
  (CodeRabbit grammar note).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 03:10:21 -07:00
austinpower1258 4864e19d64 Fix inherited Xcode 26.3 warnings
Clear two warnings introduced on main without changing the warning budget.
2026-08-07 03:09:58 -07:00
lawrencecchen 6fecd3a5fd fix: bound reset manifests and legacy markers 2026-08-07 03:09:01 -07:00
austinpower1258andClaude Fable 5 80c815480c Cover missing surface ID and newline sanitization in attention tests
Two review-suggested test additions: a nil surface ID must yield no
command (both UUID targets required), and a newline in a
payload-controlled tool name must not split the single socket command
line.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 03:05:02 -07:00
austinpower1258andClaude Fable 5 6540195741 Document the accepted prompt-submit clear race
Codex's fire-and-forget prompt-submit worker clears the pane at turn
start from a detached process; in a narrow window (worker slower than the
model's first approval-needing tool call) its late clear can remove the
new permission notification. This is the same pre-existing exposure the
shipped wrapper-path notification has always had — this change does not
widen the class — and eliminating it requires origin-time-fenced clears,
a cross-layer notification-store protocol change out of scope here.
Record the invariant at the send site.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 03:01:31 -07:00
lawrencecchen ebb49af198 fix: stage reset child deletes 2026-08-07 02:57:02 -07:00
lawrencecchen 17e6db8f9c test(tui): update C++ raw command count 2026-08-07 02:56:50 -07:00
austinpower1258 7602181e2a fix: address dock title review findings 2026-08-07 02:53:26 -07:00
austinpower1258 6f627a74fe fix: close remaining Hermes review gaps 2026-08-07 02:52:25 -07:00
austinpower1258andClaude Fable 5 a19d51714c Bound relay telemetry, share and unit-test the attention command builder
Two review findings:

- On relay-backed sockets, the acknowledged attention send closes its
  connection, so the follow-up one-way feed write reconnected implicitly
  with the default (unbounded-by-write-timeout) relay challenge — able to
  outlive the agent's hook budget. The feed frame now travels on its own
  explicitly bounded best-effort connection whenever an attention command
  was sent; no implicit reconnect remains.

- The attention command construction (UUID gating, payload shape, tool
  name sanitization, needs-permission meta) moves into the shared-compiled
  FeedEventClassifier as a pure builder, and new unit tests assert the
  exact notify_target_async / clear_notifications wire lines plus the nil
  cases. Transport ordering (awaited acknowledge before the hook returns)
  remains verified by the mock-socket harness documented in the PR — the
  app-hosted unit target cannot spawn the CLI against a live socket.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 02:49:05 -07:00
lawrencecchen c9262db0bd fix: fail closed on reset root races 2026-08-07 02:45:07 -07:00
austinpower1258andClaude Fable 5 2957e161ad Await the app's acknowledgement of the approval notify/clear
Review correctly noted that one-way writes return before the app's
detached per-connection worker enqueues the mutation, so a completed hook
process was no proof its clear had been applied — a delayed clear could
still erase a newer request's live notification. The notify/clear line is
now sent request/response and awaited (bounded at 2s) before the
synchronous feed hook returns, the same contract Claude's and Hermes'
hooks use for clear_notifications/notify_target_async. Codex runs these
hooks synchronously, so the next hook's process starts only after this
mutation is in the app's ordered lane. The feed frame stays one-way:
nonessential telemetry whose failure must never swallow the notification.

Verified against an acknowledging mock socket: PermissionRequest emits
awaited notify then feed.push, PostToolUse emits awaited clear then
feed.push, PreToolUse emits only feed.push; warm hook latency ~0.15s.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 02:36:06 -07:00
Austin Wang 1058cd2120 Merge branch 'main' into issue-9588-lightmode-text-contrast 2026-08-07 02:34:16 -07:00
austinpower1258 a9d8d5006a test: cover remaining Hermes review regressions 2026-08-07 02:31:06 -07:00
austinpower1258 067c185208 fix: statically link simulator support into bundled CLI 2026-08-07 02:30:25 -07:00
lawrencecchen addf008d37 fix: delete only confirmed reset entries 2026-08-07 02:27:18 -07:00
austinpower1258andClaude Fable 5 c8b9c91be2 Keep the approval clear off the unordered wrapper telemetry lane
The previous commit routed the native-approval-prompt clear through
sendFeedTelemetry so wrapper-launched codex seats would clear on tool
completion. Review correctly flagged that the wrapper-injected hooks run
as fire-and-forget nohup workers with no ordering guarantee: a delayed
PostToolUse worker's pane clear could erase a NEWER request's live
permission notification — silencing a blocked agent, the exact failure
this PR fixes. Remove the wrapper-lane clear and document why; wrapper
staleness is pre-existing shipped behavior that self-heals at the next
prompt-submit pane clear. The synchronous feed-hook path keeps the clear:
its events arrive in codex's own order.

Verified against a mock socket: `codex-hook post-tool-use` emits no clear;
`hooks feed --source codex --event PostToolUse` emits exactly one.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 02:26:43 -07:00
austinpower1258 af49efbd34 Merge remote-tracking branch 'origin/main' into issue-9520-hermes-first-class 2026-08-07 02:13:48 -07:00
austinpower1258andClaude Fable 5 e6a000c0c8 Send approval signal before telemetry; clear on the wrapper hook path too
Two review findings on the delivery lanes:

- The notify/clear line now precedes the feed frame in both send branches:
  the feed frame can be large and its best-effort 50ms write can fail
  under backpressure, and a failed telemetry write must never swallow the
  permission notification (that would recreate #9592's silence).

- The wrapper-injected codex hooks route tool telemetry through
  `hooks codex post-tool-use` → sendFeedTelemetry, which bypassed the
  feed-hook clear: wrapper-launched seats posted the permission
  notification via `hooks codex notification` but never cleared it on
  tool completion. sendFeedTelemetry now derives the same
  FeedEventClassifier decision and prepends the pane-scoped clear, giving
  both ingress paths one shared classification/side-effect path. The
  target helper falls back to the pane env (CMUX_WORKSPACE_ID /
  CMUX_SURFACE_ID) when the event lacks identities.

Verified against a mock socket on both paths:
`hooks feed --source codex --event PermissionRequest` emits
notify_target_async then feed.push; `--event PostToolUse` and
`codex-hook post-tool-use` emit clear_notifications then feed.push.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 02:13:43 -07:00
austinpower1258 37677f6ce2 fix: address Hermes restore review findings 2026-08-07 02:12:24 -07:00
lawrencecchen 1dd80f8c7a fix: delete reset state by directory handle 2026-08-07 02:07:28 -07:00
austinpower1258andClaude Fable 5 6cd92a0735 Clear codex approval prompts only on tool completion; batch one-way sends
Address review findings on the clear semantics and delivery:

- Codex gives no ordering guarantee between its PermissionRequest and
  pre-tool hooks, so a start-time clear could race and erase the
  just-raised prompt while the agent is still blocked — reintroducing the
  silence behind #9592. Clears now fire only on tool COMPLETION
  (PostToolUse/post_tool_use), which strictly follows any approval.
  beforeShellExecution and PreToolUse are covered as non-clearing in tests.

- wireMapping is now the single owner of clearsNativeApprovalPrompt;
  classify no longer rewraps the classification.

- The socketPath telemetry lane sends the feed frame and the
  notify/clear line over ONE connection (batched
  sendBestEffortFeedTelemetry(lines:)) instead of paying a second
  connect + auth per tool event.

Verified against a mock socket: PermissionRequest emits feed.push +
notify_target_async (redacted body), PreToolUse emits only feed.push, and
PostToolUse emits feed.push + a pane-scoped clear_notifications.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:58:51 -07:00
lawrencecchen 5108fe5d0a fix: harden reset staged deletion 2026-08-07 01:57:22 -07:00
austinpower1258 63c6832221 test: compare isolated app-host paths canonically 2026-08-07 01:55:16 -07:00
austinpower1258andClaude Fable 5 4df3d416bf chore: pin GhosttyKit artifact and record Hangul fork change
Adds the reviewed SHA-256 for the GhosttyKit.xcframework release built
from ghostty 3fbdd078d (fork main merge of manaflow-ai/ghostty#185),
and updates docs/ghostty-fork.md with the new pin, the Hangul NFC/NFD
canonical font resolution summary, its upstream-merge conflict notes
(ghostty-org/ghostty discussion #4163), and the reapplied VT
stream-boundary commit on fork main.

For #9583

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:54:42 -07:00
austinpower1258 4a64c5caeb test: cover Hermes review regressions 2026-08-07 01:50:28 -07:00
austinpower1258andClaude Fable 5 5e2ee3af4d Document the deliberate pane-wide notification-clear contract
Review asked for per-request keyed notification clears. Rejected:
notifications carry no request identity anywhere in cmux, and pane-wide
uncorrelated clears on progress signals are the shipped contract for every
agent integration (Claude session-start/prompt-submit/pre-tool-use, the
generic approvalResponse action for Hermes' resolved native approvals, and
codex's own prompt-submit hook — which also self-heals denied-approval
residue at the next turn). Record that invariant on the flag so future
reviewers see the ownership decision.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:45:26 -07:00
lawrencecchen 48e2c96400 fix: avoid reset preflight mutations 2026-08-07 01:44:19 -07:00
austinpower1258andClaude Fable 5 1066f9b914 fix: resolve NFD Hangul clusters via canonical composition in ghostty
Bumps the ghostty submodule to the fix: font selection now resolves
a decomposed Hangul grapheme cluster through its algorithmically
composed precomposed syllable, so canonically equivalent NFC and NFD
text produces the identical resolver query and selects the same
fallback face (and honors the same font-codepoint-map entries).
Terminal cell contents are unchanged, preserving copy/paste of the
original NFD codepoints. Both commits are reachable from
manaflow-ai/ghostty main via its merged PR #185.

Fixes #9583

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:43:08 -07:00
austinpower1258andClaude Fable 5 9977c7ecb5 test: ghostty regression test for NFC/NFD Hangul font divergence
Bumps the ghostty submodule to the commit that adds the failing
run-iterator test: canonically equivalent NFC and NFD Hangul must
resolve the same font face. Parent CI does not execute the submodule
Zig test suite, so this pointer exists to keep the test-first
structure visible; the red run is documented in the PR.

For #9583

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:43:07 -07:00
austinpower1258andClaude Fable 5 1b3aaa83ac Redact approval-prompt notification body and self-heal stale codex alerts
Review findings on the previous commit:

- The notification body carried the full tool summary (complete shell
  command). Commands can embed credentials, and notification banners reach
  lock screens, paired phones, and the recorded notification history. The
  body now names only the tool — the same "<tool> needs approval" string
  the in-app Feed approval banner uses — never the tool input.

- Codex fires PermissionRequest before its own "Approve for me" reviewer
  (#5507), so an auto-approved request would leave a stale or false
  "Permission" alert with nothing pending. Codex tool lifecycle progress
  (PreToolUse/PostToolUse feed events) now clears the pane's notifications,
  mirroring Claude's pre-tool-use clear_notifications contract. The clear is
  registry-scoped to sources that raise native approval prompts, so other
  agents' tool telemetry never touches the notification queue.

The immediate notify on PermissionRequest is retained deliberately: codex
has no post-reviewer hook, and the wrapper-injected schema already posts
this same immediate needs-permission notification via `hooks codex
notification`; suppressing until authoritative proof would recreate the
silence reported in #9592.

Verified against a mock socket: PermissionRequest now emits
`notify_target_async <ws> <sf> Codex|Permission|shell needs approval|c=needs-permission;p=0`
and PreToolUse/PostToolUse emit
`clear_notifications --tab=<ws> --panel=<sf>`.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:38:35 -07:00
austinpower1258 46b95df9f6 fix: complete Hermes profile hook routing 2026-08-07 01:35:56 -07:00
lawrencecchen 35469090d6 fix: fail closed on busy live markers 2026-08-07 01:33:56 -07:00
austinpower1258andClaude Fable 5 1ae6c7334c Notify on codex PermissionRequest via new nativeApprovalPrompt feed semantic
Codex blocks in its own approval reviewer when its PermissionRequest hook
fires, and that hook is wired only to the feed bridge — which deliberately
normalizes it to non-actionable PreToolUse telemetry so cmux Feed never
competes with Codex's native prompt ("Approve for me" depends on this). That
normalization also silently dropped the only signal Codex emits while
blocked, so notifications.agentPermissionPrompt never fired for codex seats.

Separate the two concerns in the classifier registry: a new
.nativeApprovalPrompt semantic keeps the exact telemetry wire behavior
(PreToolUse, non-actionable, no blocking wait) but marks the classification
notifiesNativeApprovalPrompt. Codex's PermissionRequest/permission_request
register with it; any future native-approval agent opts in with one registry
line. On that flag, the feed hook sends a fire-and-forget notify_target_async
built through the shared AgentHookNotificationClassifier, so the alert
carries the same "Permission"/"Approval needed" strings and the
c=needs-permission meta the generic notification hook and Claude's
permission_prompt path use — gated app-side by the existing
"Agent Needs Permission" setting. No new user-facing strings.

Verified against a mock socket: `cmux hooks feed --source codex --event
PermissionRequest` previously emitted only the feed.push frame (release
0.64.22); it now also emits
`notify_target_async <ws> <sf> Codex|Permission|<command>|c=needs-permission;p=0`,
while codex PreToolUse still emits no notification.

Fixes https://github.com/manaflow-ai/cmux/issues/9592

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:25:59 -07:00
austinpower1258 adf86b1395 fix: clear merged Swift warning regressions 2026-08-07 01:24:59 -07:00
austinpower1258 6dac4418be fix: canonicalize app-host config log paths 2026-08-07 01:24:58 -07:00
austinpower1258 9a9ecf2764 test: cover app-host config path aliases 2026-08-07 01:24:57 -07:00
lawrencecchen 34e6441515 fix: guard reset deletes with confirmed fingerprints 2026-08-07 01:24:22 -07:00
austinpower1258 985bd0ed49 Accept validated macOS temp aliases in app-host CI 2026-08-07 01:21:45 -07:00
lawrencecchen 1ac69cd3cd fix: allow symlinked reset state roots 2026-08-07 01:13:43 -07:00
austinpower1258andClaude Fable 5 63f0ea429e fix: ignore same-session session_switch re-emissions
OMP's reload() delegates to switchSession() on the unchanged session
file and re-emits session_switch with the same session id. Guard the
ownership handler so a same-id "switch" emits nothing: a spurious
session-start would mark an idle pane running with no agent_end coming,
leaving it non-hibernatable and shown active indefinitely.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:11:13 -07:00
austinpower1258andClaude Fable 5 d8ee541ee6 test: same-session session_switch must not rebind the pane
OMP's reload() delegates to switchSession() on the unchanged session
file and re-emits session_switch with the same session id. Treating that
as an ownership transition emits a spurious session-start hook that
flips an idle pane's record back to running with no agent_end coming,
leaving the pane non-hibernatable and shown active indefinitely. Drive a
same-id session_switch through the harness and require it to emit
nothing; the guard lands separately.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:11:12 -07:00
austinpower1258andClaude Fable 5 c723de4faf Make cold PTY spawn unstarvable and surface queued send delivery
Fixes the permanent app-wide PTY-spawn wedge from issue #9769 by
removing the class of silent one-shot drops in the cold-start path:

- Bootstrap window custody: `ensureHeadlessStartupWindowIfNeeded` now
  reclaims a pane host that window-portal churn parked outside any
  window (detachHostedView ends in removeFromSuperview), and discards a
  stale bootstrap window once the pane host lives in a real window.
  Previously a recorded-but-empty bootstrap window early-returned every
  future cold start while the follow-up attach deferred on the missing
  window — permanently, for input-demand starts (`cmux send`,
  `--command`) and background priming alike.

- Claude command-shim deadline: the optional wrapper-shim install no
  longer gates `createSurface` indefinitely. A bounded, cancellable
  deadline (injected clock, default 5s) lets spawn proceed without the
  shim when the install hangs; a late install result still serves future
  runtime creations.

- Background-prime slot pinning: a surface whose lifecycle forbids
  runtime creation (closing/closed panel, agent-hibernation suspension)
  no longer counts as background-prime work, so the prime coordinator
  releases the workspace's hidden mount slot instead of retaining one of
  the two global slots forever on its timeout path.

- Queued send visibility: `cmux send`/`send-key`/`send-panel`/
  `send-key-panel` human output now appends a localized "queued (...)"
  marker when the reply carries `queued: true`, so a send waiting on PTY
  spawn is distinguishable from a delivered one. JSON output already
  carried the flag. The v1 socket lane's bare-"OK" contract predates the
  queued flag and is left unchanged for legacy parsers; delivery itself
  is guaranteed by the spawn fixes above for every entrypoint.

The regression tests from the previous commit now pass.

Fixes #9769

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 01:02:39 -07:00
lawrencecchen b41b3df542 fix: report retried terminal-host reset 2026-08-07 01:01:26 -07:00
austinpower1258andClaude Fable 5 ae4c079650 fix: keep OMP pane ownership on globalThis across module instances
OMP loads a fresh copy of the cmux extension for every session in the
process, so the pane-ownership pin cannot live in module scope: each
subagent's module copy would start with no owner, adopt the subagent's
own session id, and keep emitting the lifecycle hooks the ownership
guard is meant to suppress. Store the owning session id on globalThis,
which is shared by all module instances in the process, so the
top-level session's claim is visible to every subagent's copy.

Verified with the reworked dual-instance harnesses: the module-scope
implementation fails test_omp_subagent_lifecycle.py and this version
passes both it and test_omp_extension_install.py.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 00:56:57 -07:00
austinpower1258andClaude Fable 5 6b09adc58e test: model OMP's per-session extension module loading
Structured review caught that the ownership guard relied on module-scope
state, but OMP loads a fresh copy of the extension module for every
session in the process: each extension import goes through a unique
?mtime= cache-busting URL (the loader's mtime token is a monotonically
increasing counter, not the file mtime), so the top-level session and
every task subagent get separate module instances.

The bun harnesses previously imported the extension once and routed all
sessions through that single instance, which made a module-scope
ownership pin look correct. Both harnesses now load a separate
cache-busted module instance per simulated session, so any cross-session
state the extension relies on must genuinely survive separate module
scopes. The subagent lifecycle test fails against the current
module-scope implementation; the fix lands separately.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 00:56:55 -07:00
austinpower1258 c39c8bc9be Merge remote-tracking branch 'origin/main' into issue-9550-invisible-ax-windows
# Conflicts:
#	web/tests/install-analytics.test.tsx
2026-08-07 00:54:28 -07:00
austinpower1258 e9a988f6cf Gate isolated sharing delegate conformance 2026-08-07 00:53:33 -07:00
austinpower1258 ff60b0b27e Merge remote-tracking branch 'origin/main' into issue-9337-dock-terminal-live-title 2026-08-07 00:53:25 -07:00
lawrencecchen 9b0a698a1c fix: render safe reset selectors 2026-08-07 00:51:41 -07:00
austinpower1258 be48d2568d test: cover Hermes profile launch review gaps 2026-08-07 00:51:39 -07:00
Lawrence Chen 3faf79585c Fix install analytics determinism gate (#9799) 2026-08-07 00:50:30 -07:00
austinpower1258andClaude Fable 5 c3472d67a4 Pre-size the rename field before its focus grab and clear the editor box
The attach-time focus grab sizes the shared field editor from the
field's current frame, and a zero-frame grab mis-sizes the editor's
dark box over the row — the same lifecycle SidebarRowChecklistItemLine
already documents and handles. Run the row layout pass before adding
the field so it enters the window with its title-slot frame, and clear
the field-editor background after attach, reusing the checklist's
helper.

Codex review finding on #9798.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 00:49:57 -07:00
austinpower1258andClaude Fable 5 98ba895364 fix: scope OMP lifecycle hooks to the pane-owning top-level session
Fixes #9591 (the omp instance of #9523).

OMP task-tool subagents run in-process, inherit CMUX_SURFACE_ID, and
each has its own session id, so every subagent's agent_end fired a stop
hook against the shared surface. The lifecycle sink is last-write-wins
per surface, so the last background subagent to finish marked the pane
idle while the main agent was still mid-turn, and the Agent Hibernation
sweep SIGHUPed the live pane ~65s later.

The v2 extension pins the owning session at the first session_start
(the top-level runtime always bootstraps before any subagent exists)
and drops every lifecycle hook from a non-owner context. Ownership
follows session_switch/session_branch - the top-level-only events OMP
emits for /new, fork, resume, and handoff - and re-emits session-start
so cmux rebinds the surface to the new session id (previously
post-/new sessions were never rebound). A subagent's session_shutdown
no longer drains or evicts the owner's queued hooks.

agent_end with willContinue (a scheduled automatic continuation:
auto-retry, queued messages, session_stop continuations, background
jobs) no longer emits a stop hook, the omp analog of Pi's agent_end ->
agent_settled fix (#8729). No version gating is needed: older OMP
builds simply omit the field, and OMP's session_stop settle event is
unsuitable because it never fires after an abort.

test_omp_extension_install.py moves to the ownership contract:
in-process session-id changes flow through session_switch, queue
pressure comes from rapid switches (the queued Stop still survives
eviction), and a foreign session_shutdown is verified not to drain the
owner's queue.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 00:44:08 -07:00
austinpower1258 ec8b69920c Merge remote-tracking branch 'origin/main' into issue-9550-invisible-ax-windows 2026-08-07 00:41:07 -07:00
austinpower1258 930ba2ecac Fix current-main CI guard regressions 2026-08-07 00:40:36 -07:00
austinpower1258andClaude Fable 5 ffd5b853c9 Share the sidebar inline-rename engine with the AppKit list (#9495)
Double-clicking a workspace name on the AppKit sidebar list committed
the untouched title ~1ms after the rename field appeared: after
makeFirstResponder began the editing session, the follow-up
selectText(nil) re-entered the field-editor machinery, synchronously
fired controlTextDidEndEditing, and the row's forked
SidebarRowInlineRenameField honored it by committing stringValue.

Delete the fork and route the AppKit list through the SwiftUI sidebar's
engine, one rename session per edit (SidebarRowInlineRenameSession):

- SidebarInlineRenameTextField focuses and selects once, when the field
  enters the window; the selectText restart is gone by construction.
- SidebarInlineRenameCoordinator resolves Enter, double-Escape, and
  focus loss at most once, passes IME composition through, and commits
  the live field-editor text instead of a stale stringValue.
- SidebarInlineRenameCommit gives the AppKit path the same commit
  policy as SwiftUI: empty drafts and unchanged auto-titles resolve to
  no write, so a stray commit can never freeze auto-naming.
  SidebarWorkspaceRowModel gains hasUserCustomTitle (plumbed from
  SidebarWorkspaceRowInput) as the policy baseline.

Cell suspension resolves the session before teardown, so end-editing
during teardown can no longer re-enter commit while the write itself
stays deferred past the table mutation. isEditing is now derived from
the session instead of a mutable flag. Existing suspension tests that
poked the old field's internals now drive the real session through the
field editor.

Fixes #9495

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 00:39:54 -07:00
lawrencecchen 72a666344a fix: harden reset confirmation ownership 2026-08-07 00:39:37 -07:00
austinpower1258 c4d418bba2 Unify surface configuration reload ordering 2026-08-07 00:35:41 -07:00
austinpower1258 d0422cfcc2 Test surface theme reload ordering 2026-08-07 00:35:22 -07:00
austinpower1258 42683b9424 fix: hide internal shell-state fallback 2026-08-07 00:27:16 -07:00
austinpower1258 af2bc23a64 fix: close Hermes approval and quit-time review gaps 2026-08-07 00:25:50 -07:00
Lawrence Chen 7daa9c6094 Fix copied surface link live identity (#9786)
* Add surface link live identity regression

* Use live surface identity for copied links
2026-08-07 00:25:11 -07:00
lawrencecchen 063ac855fd fix: harden reset deletion boundary 2026-08-07 00:20:35 -07:00
austinpower1258andClaude Fable 5 523089bf46 fix: run web-content undo/redo when focus mode sees a declined chord
In browser focus mode, CmuxWebView.performKeyEquivalent forwards the chord
to the page once and then consumes every Command equivalent so cmux and the
main menu never see it. WebKit's resend of a page-unhandled Cmd+Z /
Cmd+Shift+Z therefore died in the focus-mode branch before the web-content
undo/redo fallback in keyDown could run. Perform the web view's own editing
undo/redo when WebKit declines the chord in that branch, matching the
non-focus-mode routing.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 00:10:49 -07:00
austinpower1258andClaude Fable 5 48ed024dc2 test: focus-mode Cmd+Z must perform web-content undo when the page declines
The browser focus-mode branch of CmuxWebView.performKeyEquivalent consumes
every Command chord once the page has seen it, so WebKit's resend of a
page-unhandled Cmd+Z is swallowed there before the web-content undo/redo
fallback can run, leaving issue #9677 reproducible in focus mode.

Committed before the fix so CI proves the test catches the bug.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-07 00:10:23 -07:00
lawrencecchen c74405e1fc fix: block terminal-host publication during reset 2026-08-07 00:07:32 -07:00
austinpower1258 08333bd35c test: assert synchronous dock config seed directly 2026-08-06 23:59:13 -07:00
austinpower1258 623a829478 fix: authenticate workspace shell relay source 2026-08-06 23:59:06 -07:00
austinpower1258andClaude Fable 5 538fc6967c fix: perform browser undo/redo on a per-web-view undo manager
WebKit registers every web-content edit command on the web view's
undoManager (WebViewImpl::registerEditCommand calls [m_view undoManager]).
NSResponder resolved that to the window's shared undo manager, mixing every
web view's edit commands into one stack whose registered targets can
outlive their web view — the stale-target crash behind #7272. The fix for
that crash routed Cmd+Z / Cmd+Shift+Z away from the AppKit Edit menu when a
browser web view is focused, which also silenced in-page undo/redo because
nothing performed the command anymore.

CmuxWebView now owns webContentUndoManager and overrides undoManager, so
each page's undo stack is scoped to its web view's lifetime: stale-target
entries are impossible by construction and the window's undo manager never
sees web content. An undo/redo chord reaching CmuxWebView.keyDown has
already been offered to the page via performKeyEquivalent and declined
(WebKit resends unhandled keys), so keyDown now performs the web view's own
editing undo/redo instead of re-forwarding the chord into WebKit, matching
Safari's Edit-menu behavior. Pages that handle Cmd+Z themselves (e.g.
Google Docs) still consume the first pass and never reach this path.

Fixes https://github.com/manaflow-ai/cmux/issues/9677

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-06 23:57:48 -07:00
austinpower1258 dedd6a1c2b Merge remote-tracking branch 'origin/main' into issue-9520-hermes-first-class
# Conflicts:
#	CLI/cmux.swift
2026-08-06 23:57:12 -07:00
lawrencecchen 44077efe7e fix: classify reset fingerprint races 2026-08-06 23:53:28 -07:00
austinpower1258 2331088534 fix: address Hermes deterministic restore review findings 2026-08-06 23:50:23 -07:00
austinpower1258andClaude Fable 5 d475067122 Add failing regression test: codex PermissionRequest must raise a permission-prompt notification
FeedEventClassifier.classify now returns a FeedEventClassification struct
carrying a notifiesNativeApprovalPrompt flag alongside the wire event name
and actionability. The flag is false for every current semantic, so runtime
behavior is unchanged in this commit; the new test asserting that codex
PermissionRequest events set it is expected to FAIL, demonstrating
https://github.com/manaflow-ai/cmux/issues/9592 (the event is normalized to
non-actionable PreToolUse telemetry at ingest and no agentPermissionPrompt
notification is ever raised).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-06 23:50:22 -07:00
lawrencecchen 0591142d3e fix: fingerprint reset file contents 2026-08-06 23:42:08 -07:00
austinpower1258andClaude Fable 5 51c37efee5 test: OMP lifecycle hooks must be owned by the top-level session
Regression test for #9591: OMP task-tool subagents run in the same
process as the top-level session and inherit CMUX_SURFACE_ID, so every
subagent's agent_end was reported as the pane's idle transition. The
last background subagent to finish marked the whole pane idle while the
main agent was still mid-turn, and the Agent Hibernation sweep then
SIGHUPed the live pane.

The new test drives the generated OMP extension through the issue's
exact repro shape: a main session mid-turn, a background subagent that
boots/runs/finishes, an agent_end with willContinue (a scheduled
automatic continuation), the real terminal settle, and a session_switch
(/new). It requires that only the owning top-level session ever reaches
cmux lifecycle hooks, that willContinue defers the stop hook, and that
session_switch re-pins ownership and rebinds the new session id.

Fails against the current v1 extension; the fix lands separately so CI
proves the test catches the bug.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-06 23:36:52 -07:00
Lawrence Chen 99b7b370fe Harden install analytics privacy (#9791) 2026-08-06 23:33:18 -07:00
lawrencecchen 36ed4ae8f0 fix: hold terminal-host reset leases 2026-08-06 23:32:57 -07:00
Lawrence Chen 0676a4cdac Serve coderouter landing on cmux.com (#9790) 2026-08-06 23:29:30 -07:00
austinpower1258andClaude Fable 5 3956c75e4a test: browser Cmd+Z/Cmd+Shift+Z must perform web-content undo/redo
Regression coverage for https://github.com/manaflow-ai/cmux/issues/9677:
with a browser web view focused and the page declining the chord (WebKit's
resend of an unhandled key), the routed Cmd+Z / Cmd+Shift+Z must execute
undo/redo on the web view's own undo manager instead of being swallowed,
and each web view's undo manager must be scoped to that view rather than
the window's shared undo manager (the stale-target crash class from
https://github.com/manaflow-ai/cmux/issues/7272).

Committed before the fix so CI proves the tests catch the bug.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-06 23:21:28 -07:00
austinpower1258 fe7f7236d2 test: cover Hermes deterministic restore review findings 2026-08-06 23:12:34 -07:00
austinpower1258 3038288f81 fix: import dock shell activity state 2026-08-06 23:07:24 -07:00
austinpower1258andClaude Fable 5 6d4a2933a5 Add failing regression tests for burst-wedged PTY spawn and silent queued sends
Issue #9769: after a CLI dispatch burst, new terminal surfaces never
acquire a PTY and `cmux send` silently queues or drops input while
printing OK. These tests reproduce the mechanism:

- TerminalSurfaceBootstrapCustodyTests: window-portal churn parks the
  pane host outside any window while a bootstrap startup window is still
  recorded; every later cold start (input demand, queued send,
  background prime) early-returns and defers on the missing window, so
  the surface never spawns. A hung Claude command-shim install likewise
  starves createSurface forever.
- BackgroundPrimeStartableSurfaceTests: a surface whose lifecycle
  forbids runtime creation still counts as background-prime work, so the
  prime coordinator's timeout path pins one of the two global hidden
  mount slots forever.
- CLISendQueuedOutputTests: a queued send reply prints a bare
  "OK surface:N workspace:M", indistinguishable from a delivered send.

CI is expected red on this commit; the fix follows separately.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-06 23:05:15 -07:00
Austin Wang c5bf3ca465 Merge pull request #9765 from manaflow-ai/issue-9746-horizontal-tab-insert
Fix horizontal tab strip insertion positions
2026-08-06 23:03:00 -07:00
austinpower1258andClaude Fable 5 fc1c48db2f Add failing regression tests for AppKit sidebar inline rename (#9495)
Double-clicking a workspace name on the AppKit sidebar list creates the
inline rename field but tears the editing session down ~1ms later,
committing the untouched title. These tests drive the real AppKit
editing path (cell in a window, shared field editor, commands dispatched
through the field editor) and assert the intended behavior: begin keeps
the session alive without a write, Enter commits the live editor text
once, an unchanged title is a no-op, Escape cancels, and focus loss
commits the typed draft.

Test-only commit: CI is expected to go red until the fix lands.

Issue: https://github.com/manaflow-ai/cmux/issues/9495

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-06 23:00:25 -07:00
lawrencecchen cd07d75e77 fix: harden saved-state reset confirmation 2026-08-06 22:59:47 -07:00
austinpower1258 7a78c50351 fix: import terminal surface for shell activity routing 2026-08-06 22:52:24 -07:00
austinpower1258 a38c0a0cc8 Stop AX snapshots retaining transient windows 2026-08-06 22:51:56 -07:00
austinpower1258 eae5bb5985 Add #9550 AX window retention regression test 2026-08-06 22:51:56 -07:00
Lawrence Chen 21675cc273 Stream raw PTY bytes to smart terminal clients (#9634)
* Merge main into raw PTY streaming

* Test smart stream byte-budget overflow

* test(tui): require raw protocol 11 compatibility boundary

* Bump private mux protocol to 11

* test(mac): cover selection and resize recovery regressions

* Recover terminal selections and resize delivery

* test(mac): bound resize acknowledgement retries

* fix(mac): bound resize acknowledgement retries

* test(sdk): cover protocol 11 inventory

* test(cpp): cover protocol 11 terminal placement

* Fix demo launcher and input readiness

* Bound C string snapshot retries

* Harden terminal snapshot and retry state

* Bound terminal snapshot consumers and retry state

* Address terminal review findings

* Render terminal frames from dirty rows

* Harden dirty frame lifecycle and retry pacing

* Apply terminal row deltas without full frame copies

* Add authoritative viewport row counts

* Initialize dirty renderer cache on empty frames
2026-08-06 22:51:52 -07:00
Lawrence Chen 371edf3c32 Add tracked website installers and coderouter landing (#9784) 2026-08-06 22:50:37 -07:00
lawrencecchen 021a839dbe fix: own saved-state reset API 2026-08-06 22:48:03 -07:00
Austin Wang c0b8391865 Merge pull request #9780 from manaflow-ai/issue-9768-codex-hook-dedup
Fix duplicate Codex hook channels and watchdog leaks
2026-08-06 22:43:20 -07:00
austinpower1258 6b79b62d9a fix: reconcile Codex hook producers 2026-08-06 22:39:47 -07:00
austinpower1258 728dfec369 test: cover implicit remote shell lifecycle routing 2026-08-06 22:33:00 -07:00
lawrencecchen e326110ba6 fix: rename reset state before deletion 2026-08-06 22:30:31 -07:00
austinpower1258 b36be1d285 fix: detect installed Hermes runtime and restore its official icon 2026-08-06 22:24:14 -07:00
austinpower1258 e539849606 Merge remote-tracking branch 'origin/main' into issue-9706-lease-gone-live-pty 2026-08-06 22:23:46 -07:00
Lawrence Chen 42715b7f9e Isolate app-host tests from runner user config (#9716)
* test: require isolated app-host user state

* ci: isolate app-host user configuration

* test(ci): scope app-host isolation guard to jobs

* test(ci): reject malformed isolation workflow

* test: require app-host launch home isolation

* test: reject app-host config path leaks

* fix: isolate launched app-host user state

* test: require nonempty app-host XDG default

* fix: default app-host XDG path safely

* test: keep app-host redirects off xcodebuild

* fix: scope redirects to launched app host

* test: require XCTest runner home isolation

* fix: isolate XCTest app host through runner environment

* test: close app-host isolation validation gaps

* fix: harden app-host isolation validation

* test: cover optional app-host isolation validation

* fix: keep app-host isolation assertions opt in

* test: reject app-host XDG and scheme overrides

* fix: bind app-host XDG to isolated home

* test: cover non-isolated macOS Bash wrapper

* fix: support optional isolation on macOS Bash

* test: cover app-host child isolation boundaries

* fix: isolate app-host child process state

* test: require fail-closed app-host cleanup

* fix: fail closed and clean app-host homes

* test: require cleanup after XDG removal

* fix: clean homes after XDG removal

* style: restore process helper indentation

* test: require robust app-host teardown discovery

* fix: make app-host teardown robust

* test: preserve long-path app-host fixture

* test: keep app-host isolation out of driver environment

* fix: isolate app-host redirects at launch boundary

* test: reject ambient Ghostty configuration reads

* fix: validate every Ghostty config path

* test: clean mutated app-host XDG entries

* fix: clean mutated app-host XDG entries

* test: require durable app-host isolation evidence

* fix: require durable app-host isolation evidence

* test: enforce short symlink-safe app-host homes

* fix: use short symlink-safe app-host homes

* test: exercise app-host home symlink guard

* test: require app-host identity receipts

* fix: bind app-host cleanup to run identity

* test: keep app-host receipts across CI jobs

* fix: persist app-host process authority

* test: inject lsof into app-host retry guard

* test: preserve existing app-host authority scope

* fix: preserve existing app-host authority scope

* test: model dead-pid lsof diagnostics

* fix: classify dead-pid lsof diagnostics as stale

* test: authenticate cleanup before ownership transfer

* fix: authenticate app-host scope before ownership transfer

* test: bind app-host receipts to process incarnation

* fix: bind app-host receipts to process incarnation

* test: make app-host owner fixture portable

* test: require atomic app-host receipt publication

* fix: publish app-host receipts atomically

* test: cover app-host recovery review gaps

* fix: harden app-host recovery

* test: cover app-host ownership lifecycle gaps

* fix: scope app-host recovery to one run

* test: recover authenticated prior-run app hosts

* test: cover multiple current app-host owners

* test: bound prior app-host recovery eligibility

* test: model lsof machine descriptor fields

* test: recover deleted prior app-host owners

* fix: recover authenticated prior app hosts

* test: model lsof access mode fields

* fix: verify lsof receipt access mode

* test: include lsof access mode in cleanup fixture

* test: reject untrusted newest confirmations

* fix: authenticate newest app-host confirmations

* test: cover app-host machine ownership boundaries

* fix: bind app-host recovery to machine ownership
2026-08-06 22:21:47 -07:00
lawrencecchen 3a2f4b79d3 fix: cover session sidecars in reset token 2026-08-06 22:20:54 -07:00
lawrencecchen 42d96b53cb fix: harden session guard coordinator 2026-08-06 22:12:23 -07:00
austinpower1258 e76a4b0bdd test: cover drag-to-split cursor focus 2026-08-06 22:11:38 -07:00
austinpower1258 1dcf57676d test: cover real Hermes restore and Vault presentation 2026-08-06 22:10:02 -07:00
Austin Wang ce41f152de Merge pull request #9066 from manaflow-ai/issue-9065-remove-cgwindowlistcreateimage
Replace legacy window screenshot capture
2026-08-06 22:05:19 -07:00
austinpower1258 05f3e1a0a9 fix: wait for PTY teardown before daemon exit 2026-08-06 22:03:51 -07:00
lawrencecchen 70ac77691d fix: clean exact reset guards safely 2026-08-06 22:03:41 -07:00
austinpower1258 1d7626f5a0 fix: keep background attention inside cmux 2026-08-06 22:01:34 -07:00
austinpower1258 a26cf6d570 test: cover Codex hook producer deduplication 2026-08-06 22:01:13 -07:00
austinpower1258 598dbd2da3 fix: address dock title lifecycle review findings 2026-08-06 22:00:10 -07:00
lawrencecchen 187dc8d16b fix: bound reset guard storage 2026-08-06 21:57:44 -07:00
austinpower1258 f6d690d0d1 test: keep daemon alive during PTY teardown 2026-08-06 21:54:07 -07:00
lawrencecchen 42e1fd1d52 fix: order reset confirmation lifecycle 2026-08-06 21:49:16 -07:00
Austin Wang 82454e9bdb Merge pull request #9736 from manaflow-ai/issue-9128-pdf-share-button
fix: restore embedded PDF preview sharing
2026-08-06 21:48:18 -07:00
austinpower1258 722cae8562 fix: harden horizontal tab drop fallback 2026-08-06 21:47:32 -07:00
Austin Wang fecf416d50 Merge pull request #9744 from manaflow-ai/issue-9341-downloads-popover-contrast
Fix browser popover appearance contrast
2026-08-06 21:45:48 -07:00
austinpower1258 c2e9a50145 test: prevent background attention from reaching AppKit 2026-08-06 21:45:18 -07:00
lawrencecchen cafa67af0e fix: avoid unsupported windows directory flush 2026-08-06 21:41:50 -07:00
austinpower1258 068e855f77 fix: refine persistent daemon exit checks 2026-08-06 21:38:13 -07:00
austinpower1258 3d3b2642f4 fix: import workspace shell activity state 2026-08-06 21:37:39 -07:00
Abdulaziz Albahar 9d8d66b10e Show iOS terminal send progress and failures (#9723)
* test(ios): cover terminal send status

* feat(ios): show terminal send status

* fix(ios): bind send status to queued input

* test(ios): reject persistent send success glyph

* fix(ios): clear send progress after delivery

* test(ios): preserve failed send across draft restore

* fix(ios): retain failed send after draft restore
2026-08-06 21:35:57 -07:00
austinpower1258 0e359517f1 fix: enable horizontal tab insertion positions 2026-08-06 21:35:20 -07:00
lawrencecchen 7469555336 fix: bind reset confirmation to state identity 2026-08-06 21:34:26 -07:00
austinpower1258 a503a36e87 Merge remote-tracking branch 'origin/main' into issue-9706-lease-gone-live-pty 2026-08-06 21:29:50 -07:00
Lawrence Chen 5fcdb66a61 Revoke team route tokens when billing lapses (#9761)
* Honor Team subscriptions for hosted coderouter

* Revoke team route tokens when billing lapses
2026-08-06 21:29:45 -07:00
austinpower1258 4fedac14cb test: reproduce diff viewer scheme deadlock 2026-08-06 21:28:36 -07:00
Abdulaziz Albahar 29d809d7d5 Replace iOS Mac hide swipes with row toggles (#9727)
* Replace iOS computer hide swipes with toggles

* Avoid storing visibility mutation tasks

* Keep visibility toggle actions synchronous

* Keep toggle binding actor-local

* Test visibility mutations preserve the latest intent

* Serialize iOS computer visibility mutations

* Animate iOS computer visibility toggles

* Test visibility mutations across account boundaries

* Cancel visibility mutations at scope boundaries

* Test cancelled visibility marker rollback

* Roll back cancelled visibility markers

* Test visibility queue tails across boundaries

* Preserve visibility serialization across scope changes
2026-08-06 21:27:55 -07:00
Abdulaziz Albahar 3347fd9a90 iOS browser stream: focus editables under replayed taps, suppress stray backspace, verbose diagnostics (#9729)
* test: a replayed phone click on a text field must focus it

Replayed clicks reach the streamed page as DOM events, but WebKit
refuses to move field focus for clicks in a window that is never key
(the offscreen render host). A tapped text field never focuses, the
phone keyboard never rises, and backspace falls through as page-level
history back-navigation instead of deleting. Red test plus the bare
panel replay seam the fix will hang off.

* Focus editables under replayed phone clicks; verbose browser-stream diagnostics

Programmatic JS focus is exempt from WebKit's key-window rule, so a
replayed click now hit-tests the tap point (descending one shadow-root
level) and focuses the editable it finds. The phone keyboard rises via
the existing editable_focused beacon, and typing lands in the field. A
bare backspace with no focused editable is suppressed instead of
falling through as WebKit history back-navigation, which lost page
state when users tried to delete text.

Verbose browser-stream diagnostics for debugging user reports: the Mac
host ring (Sentry-attached) records stream lifecycle (start, replace,
stop, first frame), input replay outcomes (kind, click count,
suppressed backspace, text length), focus-assist results, beacon
editable transitions, and create resolutions; the phone debug log
gains browser.create and browser.stream lifecycle lines.

* Address review: semantic diagnostic decodes, first-frame after delivery, shadow-aware focus checks

The four browser diagnostic codes now decode into named fields (stage,
input kind, count, focus outcome, panel correlation) instead of falling
through to detail_1/2/3, with a suppressed backspace becoming its own
input kind so counts stay unambiguous. The first-frame lifecycle stage
records once per session after the first successful browser.frame
delivery, never for a capture whose send failed. Editable-focus
detection descends shadow roots in the suppression check, the focus
assist's already-focused check, and the beacon, so widget-wrapped
inputs receive backspace and raise the keyboard; regression test
covers backspace delivery to a shadow-root input.
2026-08-06 21:26:12 -07:00
austinpower1258 06e27e445b fix: preserve live PTYs after relay lease loss 2026-08-06 21:26:02 -07:00
lawrencecchen 0403ad22bb fix: harden guarded state reset 2026-08-06 21:24:41 -07:00
Abdulaziz Albahar 6277fdb408 Filter Iroh discovery by runtime scope (#9535)
* Add scoped connectivity discovery

* Harden scoped discovery fallbacks

* Validate discovery tags before hashing

* Keep discovery tag mapping off MainActor

* Test empty-route host registration renewal

* Renew empty-route Iroh host bindings

* Update host renewal deadline expectation

* Test scoped discovery review regressions

* Fix scoped discovery review regressions

* Test canonical peer tags and renewal deadlines

* Canonicalize scoped peer tag matching

* Satisfy scoped discovery review policy
2026-08-06 21:19:14 -07:00
Lawrence Chen 80054fcaca Honor Team subscriptions for hosted coderouter (#9753) 2026-08-06 21:18:04 -07:00
austinpower1258 b74ce85df9 fix: rotate hibernated terminal process generations 2026-08-06 21:17:28 -07:00
lawrencecchen bc83387350 fix: require reset preview confirmation 2026-08-06 21:15:56 -07:00
austinpower1258 e018d5f071 test: bound shell dedupe across process generations 2026-08-06 21:08:57 -07:00
austinpower1258 2dd592e8ee test: preserve live PTY after relay lease loss 2026-08-06 21:08:29 -07:00
austinpower1258 f0db8f9315 test: cover hibernated terminal process generations 2026-08-06 21:08:17 -07:00
Austin Wang 10013687b0 Merge pull request #8563 from mcorcelle/feat/configurable-pane-flash-color
Make pane flash and attention ring color configurable
2026-08-06 21:08:04 -07:00
lawrencecchen 53c3a67ac3 fix: avoid mutating missing reset targets 2026-08-06 21:05:59 -07:00
lawrencecchen 5319386733 fix: require durable reset sync 2026-08-06 21:01:05 -07:00
austinpower1258 3a205be7a2 fix: require attached PDF share anchor 2026-08-06 20:59:24 -07:00
lawrencecchen c68b5ac8bf fix: reuse terminal host reset snapshot 2026-08-06 20:56:08 -07:00
Abdulaziz Albahar 8fa42e5e23 Open Notifications as a pane tab (#9721)
Notifications now opens as a normal workspace pane tab instead of a window-level overlay. Legacy overlay snapshots migrate to tabs, and background notification panes cannot steal terminal focus.

Verified with tagged cloud build npbg and focused hosted UI coverage.
2026-08-06 20:55:14 -07:00
austinpower1258 4f965d05ae test: mount canvas attention harness in window 2026-08-06 20:53:40 -07:00
austinpower1258 9b06ef32fb fix: guard restored Dock terminal titles 2026-08-06 20:48:50 -07:00
lawrencecchen 4aa79e32b2 test: gate terminal host reset coverage to unix 2026-08-06 20:48:03 -07:00
austinpower1258 3a914b106d test: cover detached PDF share controls 2026-08-06 20:44:59 -07:00
lawrencecchen 30055fa7e8 fix: report reset safety failures safely 2026-08-06 20:44:21 -07:00
lawrencecchen e38a863376 fix: make session reset preview first 2026-08-06 20:39:20 -07:00
austinpower1258 65e56c4f54 fix: address remaining Hermes review findings 2026-08-06 20:38:20 -07:00
Austin Wang 981d4d7816 Refresh appearance probe after SwiftUI updates 2026-08-06 20:36:04 -07:00
austinpower1258 bfe48260b6 fix: keep pane attention color owner-controlled 2026-08-06 20:35:18 -07:00
lawrencecchen 3aaa605bdb fix: harden session state reset gate 2026-08-06 20:31:57 -07:00
Austin Wang 6239b29a98 Make downloads appearance probe lifecycle-driven 2026-08-06 20:31:24 -07:00
Austin Wang 2c7bc82428 Move downloads appearance probe to debug support 2026-08-06 20:24:27 -07:00
austinpower1258 27a5086147 fix: update PDF share debug closure 2026-08-06 20:23:19 -07:00
austinpower1258 52c5f92251 fix: route PDF share activation explicitly 2026-08-06 20:16:34 -07:00
cmux reload-cloud 93298252ad fix: preserve efficient screenshot composition 2026-08-06 20:13:53 -07:00
Austin Wang 7857841837 Fix browser popover appearance resolution 2026-08-06 20:12:29 -07:00
austinpower1258 636ae73463 test: mount terminal attention ownership harness 2026-08-06 20:10:38 -07:00
austinpower1258 20fe7baa8a test: cover remaining Hermes review regressions 2026-08-06 20:05:56 -07:00
austinpower1258 24d864951f test: mount PDF share controls before activation 2026-08-06 20:04:19 -07:00
cmux reload-cloud 76a1d08c0b test: align OMP restore environment contract 2026-08-06 19:55:48 -07:00
austinpower1258 11efc10f54 test: protect terminal attention color ownership 2026-08-06 19:52:04 -07:00
Austin Wang 189ac5b27b test: cover downloads popover contrast mismatch 2026-08-06 19:49:48 -07:00
austinpower1258 3a8f1156a6 Merge remote-tracking branch 'origin/main' into issue-9128-pdf-share-button 2026-08-06 19:40:50 -07:00
austinpower1258 1c4bd8c319 fix: complete PDF share translations 2026-08-06 19:40:33 -07:00
austinpower1258 4540f16c5b test: avoid PDF share helper shadowing 2026-08-06 19:39:01 -07:00
austinpower1258 9117d49b91 refactor: keep canvas color wiring at existing owners 2026-08-06 19:32:10 -07:00
austinpower1258 59dcd20a86 fix: support non-pointer PDF sharing 2026-08-06 19:21:58 -07:00
austinpower1258 3821e90345 fix: propagate pane attention color into canvas hosts 2026-08-06 19:16:37 -07:00
austinpower1258 b49eb593aa fix: present PDF share picker from chrome 2026-08-06 19:07:51 -07:00
cmux reload-cloud bbbf26c5e6 Merge remote-tracking branch 'origin/main' into issue-9065-remove-cgwindowlistcreateimage 2026-08-06 19:07:36 -07:00
cmux reload-cloud 8d4788a898 fix: isolate screenshot backend liveness 2026-08-06 19:07:16 -07:00
austinpower1258 86b33c919d perf: update pane color without rebuilding geometry 2026-08-06 18:54:42 -07:00
cmux reload-cloud ea43159b2d refactor: give screenshot capture one lifetime lease 2026-08-06 18:48:05 -07:00
Lawrence Chen f4263ae8b9 Restrict coderouter sign-in to passwordless email (#9740) 2026-08-06 18:43:53 -07:00
austinpower1258 098b4ad007 fix: make pane attention copy color-neutral 2026-08-06 18:40:31 -07:00
Lawrence Chen 87fb5db1b7 Delete completed reconciliation migration endpoint (#9739) 2026-08-06 18:40:26 -07:00
austinpower1258 8f8eb05a58 fix: continue parsing after invalid notification sound 2026-08-06 18:40:24 -07:00
austinpower1258 d6e8d30ef6 test: reproduce PDF preview share routing gap 2026-08-06 18:30:39 -07:00
cmux reload-cloud ac8082c81d fix: recover and clip screenshot compositing 2026-08-06 18:21:12 -07:00
Lawrence Chen 07322a4648 Remove completed reconciliation migration route (#9737)
* Add one-shot reconciliation migration fallback

* Remove completed reconciliation migration route
2026-08-06 18:17:56 -07:00
austinpower1258 97c8a7cadf Merge remote-tracking branch 'origin/main' into issue-9128-pdf-share-button 2026-08-06 18:09:51 -07:00
austinpower1258 f0bc80ab60 test: exercise mounted Quick Look share routing 2026-08-06 18:09:31 -07:00
cmux reload-cloud 0330d7a186 fix: include native chrome in window captures 2026-08-06 18:02:23 -07:00
Lawrence Chen 27dfa4a1ef Add one-shot reconciliation migration fallback (#9735) 2026-08-06 18:02:23 -07:00
austinpower1258 3584879fce fix: present embedded Quick Look share picker 2026-08-06 18:02:14 -07:00
austinpower1258 a2e7a77b62 test: preserve flash color after invalid sound 2026-08-06 17:55:36 -07:00
cmux reload-cloud 894b4509a8 fix: preserve screenshot command contracts 2026-08-06 17:52:18 -07:00
Lawrence Chen 9ee56b5f34 Finish coderouter billing operations and telemetry (#9732)
* Finish coderouter billing operations and telemetry

* Harden reconciliation and operator tooling

* Serialize and harden billing reconciliation
2026-08-06 17:51:07 -07:00
austinpower1258 ebdf0381ec fix: share pane attention color across renderers 2026-08-06 17:47:17 -07:00
austinpower1258 42c3cfedbd fix: centralize Dock terminal title ownership 2026-08-06 17:43:57 -07:00
cmux reload-cloud 471b0acb00 fix: close screenshot capture review gaps 2026-08-06 17:39:15 -07:00
austinpower1258 c83e67941a test: reproduce Quick Look share action gap 2026-08-06 17:32:54 -07:00
lawrencecchen 8227438ae8 fix: offer scoped session state reset recovery 2026-08-06 17:31:46 -07:00
lawrencecchen b0d71f0a3b test: cover incompatible session state reset recovery 2026-08-06 17:29:50 -07:00
austinpower1258 33424f95bc Merge remote-tracking branch 'origin/main' into issue-9337-dock-terminal-live-title 2026-08-06 17:23:36 -07:00
austinpower1258 a5b8826678 fix: propagate live titles to Dock terminals 2026-08-06 17:19:55 -07:00
Austin Wang a0a4dfdd75 Merge branch 'main' into feat/configurable-pane-flash-color 2026-08-06 17:15:02 -07:00
cmux reload-cloud 45bbf1fc51 Merge remote-tracking branch 'origin/main' into issue-9065-remove-cgwindowlistcreateimage
# Conflicts:
#	Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandExecutionPolicyTests.swift
#	Sources/TerminalController.swift
2026-08-06 17:12:43 -07:00
Austin Wang 9f0c47f8e4 Merge pull request #9604 from haung921209/comments-list-cli
Add cmux comments list — read-only CLI surface for diff review comments
2026-08-06 16:54:40 -07:00
Lawrence Chen d7a5b5ac0b Clarify coderouter model errors (#9728) 2026-08-06 16:21:38 -07:00
Abdulaziz AlbaharandClaude Fable 5 86f8875240 Sidebar: request an authoritative apply when a deferred row click parks (#9691)
* Add failing test: parked reveal-time sidebar click must request an apply

A click landing while row actions are detached is deferred (#9225) but the
replay only runs from the next authoritative apply, and nothing requests
one: the park mutates no SwiftUI-tracked state, the sidebar body is
Equatable-gated, and an idle app never re-arms the rows. The click stays
parked until unrelated invalidation, historically an app deactivate/
reactivate cycle.

The test drives the reveal gap (suspend + reveal without an apply), sends
the row click, and asserts the controller fires the new
onDeferredRowClickAwaitingApply seam exactly once; the seam is inert in
this commit so CI shows the test red.

Refs https://github.com/manaflow-ai/cmux/issues/9690

Co-Authored-By: Claude Fable 5 <[email protected]>

* Request an authoritative apply when a sidebar click parks awaiting actions

Fixes https://github.com/manaflow-ai/cmux/issues/9690: infrequently,
sidebar row taps did nothing until an app deactivate/reactivate cycle.

A click landing on a presentation-snapshot row (live action captures
released) is deferred and replayed from the next authoritative apply
(#9225). But the replay was passive: applies only happen when the
Equatable-gated sidebar body re-evaluates, and the park itself mutates no
SwiftUI-tracked state, so an idle app never re-armed the rows. The parked
click waited for unrelated invalidation, which an app focus cycle
eventually provided via window-key row repaints.

The controller now fires onDeferredRowClickAwaitingApply when it parks a
click; SidebarWorkspaceTableView forwards it to VerticalTabsSidebar, which
bumps a @State token read by appKitWorkspaceScrollArea (same pattern as
appKitPostResizeRefreshToken). The body re-evaluates, updateNSView
re-applies fresh action-carrying rows, and the parked click replays
immediately. The request fires only from a physical click, never from a
replay re-park, so a request per click is the ceiling and it cannot loop.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-06 16:07:34 -07:00
Austin Wang a4b76aac36 Merge pull request #9630 from manaflow-ai/fix-4133-un-center-main-thread-deadlock
Fix beachball: bound every UNUserNotificationCenter call so a wedged usernotificationsd can't deadlock the main thread
2026-08-06 16:04:21 -07:00
Lawrence Chen b9a8e20d22 Instrument coderouter usage and failures (#9725) 2026-08-06 16:00:22 -07:00
Abdulaziz Albahar 81c20891b9 iOS: unify phone browsers on the streamed Mac surface (create, blank-page state, Mac click counts, discarded-tab restore) (#9577)
* iOS: New Browser creates and streams a Mac browser pane

The New Browser picker action opened the phone-local WKWebView pane,
which no longer matches the streamed Mac-browser surface used by the
Mac Browsers rows. Add a mobile.browser.create RPC (mirroring
mobile.terminal.create) plus a browser.stream.create.v1 capability, and
route the button through the same activate-and-start stream path as
panel selection. The local pane remains as a fallback for Macs without
the capability, while disconnected, or when creation is rejected.

* iOS: purposeful blank-page state and Mac double-click taps for browser streams

A fresh New Browser pane mirrored an empty white capture, which read as a
glitch; the pane now shows an opaque new-page placeholder until the first
navigation gives the panel a URL. Double tap no longer zooms locally: taps
forward immediately with a rising Mac click count (double tap = double
click for word selection, triple = paragraph), and removing the
double-tap recognizer also removes the recognizer-failure delay from
every single click. Pinch keeps owning zoom.

* test: streaming a discarded background tab must restore its web view

A session-restored or memory-discarded background tab has only a blank
web shell; a phone stream started on it mirrors white frames until a
manual reload. Red test: mobile stream start must begin the
discard-restore navigation like revealing the tab on the Mac does.

* Restore discarded web views when a mobile browser stream starts

Streaming counts as a visibility touch: kick the discard-restore
navigation before the first capture, so a phone opening a preexisting
background tab after Mac launch streams real content instead of a
white blank that only a manual reload fixed.

* Address review: owned create request, reconcile uncertain create, generic error copy

A late mobile.browser.create result no longer activates its panel over a
selection the user made in the meantime: completion applies only while
its request ID is still current, and every competing picker action
invalidates it. An uncertain create outcome (timeout, decode failure,
client swap) now refreshes panel discovery so a committed Mac panel
surfaces in the picker instead of becoming an orphan. The create RPC's
encode-failure body uses product-level copy instead of serialization
detail.
2026-08-06 15:58:58 -07:00
Abdulaziz AlbaharandClaude Fable 5 fe18ae95fa Refresh same-account Iroh discovery on iOS startup (#9430)
* Update App Review Mac reviewer instructions

* Stabilize iOS Iroh event stream recovery

* Force live Iroh discovery for startup auto-connect

* Revert "Stabilize iOS Iroh event stream recovery"

This reverts commit 1a6cf89388.

* Make reviewer-setup.md notes block the canonical ASC template

CodeRabbit flagged conflicting instructions: the checklist said to paste
all of review-notes.md into App Store Connect while reviewer-setup.md had
its own pasteable block with the manual pairing fallback. The
reviewer-setup.md block is now the single canonical template and
review-notes.md is marked reference-only.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-06 15:37:21 -07:00
Abdulaziz Albahar e1a59bdb03 Fix grouped workspaces in All Computers (#9509)
* test(ios): preserve groups in all-computer workspace lists

* fix(ios): preserve groups in all-computer workspace lists

* test(ios): reject ambiguous group collapse migration

* fix(ios): harden multi-Mac group derivation

* test(ios): cover grouped multi-Mac workspace list
2026-08-06 15:37:03 -07:00
2280f9cc1e Fit iOS onboarding within every viewport (#9489)
* test(ios): keep onboarding content fixed in viewport

* fix(ios): fit onboarding within every viewport

* test(ios): harden onboarding viewport checks

* test(ios): cover compact onboarding layouts

* test(ios): baseline fallback footer geometry

* fix(ios): fit compact onboarding connection layout

* test(ios): require real agent onboarding capture

* fix(ios): use real agent onboarding capture

* test(ios): preserve original onboarding capture

* fix(ios): keep original onboarding captures

* test(ios): describe restored onboarding capture

* Pin GhosttyKit checksum for iOS startup fix (#9487)

* test(ios): bound onboarding chrome

* Retake onboarding captures from the current app with real workspace content

The onboarding tour framed captures were taken in July against the old
home chrome (top search bar) and placeholder fixture rows (iOS avatar
tuning / Docs / Notes with no activity). Retaken all eight variants
(workspaces + notifications x en/ja x light/dark) at 1320x2868 from the
current UI: bottom-aligned minimized search next to the floating tabs,
six workspace rows with agent-activity previews, same-day timestamps,
and unread state, so the tour shows what the shipped list really looks
like.

The workspace fixture rows now carry realistic use cases (agent fixed a
crash and opened a PR, build green, agent waiting on approval) instead
of bare terminal-name subtitles; ids workspace-main/workspace-docs and
the Docs-vs-main search disjointness the bottom-search UI tests depend
on are preserved. Five notification-feed preview bodies drop their
self-referential test-speak (wrapping-verification sentence, 'ready to
open in the iOS app') for agent-report copy, in both English and
Japanese; the one UI test asserting the approval body verbatim is
updated with it.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Top-align onboarding pages and enlarge the framed iPhone

The fit pass left each page's visual centered in the leftover space, so
the pairing card floated mid-page and the framed iPhone shrank into a
480pt cap with dead space above and below. Scene content now pins to the
top (copy, then visual directly beneath, spare space at the bottom), and
the iPhone frame cap rises to 560pt on phones / 700pt on iPad; small
viewports are unaffected because the frame layout still fits itself to
the proposed height.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Use the Deep Blue bezel for dark-mode onboarding frames

The silver product frame glows against the dark tour backdrop. Dark
appearance now loads frameit's Deep Blue iPhone 17 Pro Max artwork
(same 1470x3000 screen geometry, so the existing frame-derived mask
serves both), selected alongside the appearance-specific capture; the
artwork-resolution test now covers both colorways.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Austin Wang <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-06 15:26:33 -07:00
Lawrence Chen 2c3f2fb5ca Warm up billing success page (#9718) 2026-08-06 13:36:59 -07:00
Lawrence Chen 95ef75b4fa Add client-local terminal multiview architecture (#9387)
* Implement client-local terminal multiview architecture

* Harden terminal multiview lifecycle

* Complete terminal multiview lifecycle contracts

* Fix projected terminal response and close

* Document terminal project in CLI help

* Document terminal projection resource path

* Align lifecycle docs with terminal projections

* Test enhanced prefix split routing regression

* Implement client-local terminal projections

* test(tui): cover legacy workspace selection recovery

* fix(tui): seed legacy compatibility workspace

* test(tui): reproduce slow-client output failures

* fix(tui): keep slow client projections current

* test(tui): move backpressure fixtures into workers

* test(tui): reproduce modifier prefix cancellation

* fix(tui): keep modifiers outside semantic input

* test(tui): reproduce Ctrl-D exit topology leak

* fix(tui): atomically detach exited terminal views

* test(tui): reproduce input and resize lifecycle races

* test(tui): reproduce semantic destination races

* fix(tui): make terminal projections client-local and receipt ordered

* fix(tui): preserve queued creation through terminal exit

* test(tui): reproduce zero-size split crash

* fix(tui): make degenerate split layout total

* style(tui): format degenerate split layout

* test(tui): reproduce empty startup input route

* fix(tui): establish the startup input route

* fix(tui): keep smoke terminals process-owned

* test(tui): reproduce burst input loss

* test(tui): reproduce missing size lease release loop

* fix(tui): make view size release idempotent

* test(tui): reproduce stale attachment resize failure

* fix(tui): supersede stale attachment resizes

* fix(tui): retain host input under mutation bursts

* test(tui): reproduce multiview projection leaks

* fix(tui): isolate backend view projections

* test(tui): reproduce retired attach lease leak

* fix(tui): release retired view attachments

* fix(tui): restore projection viewport state

* test(tui): reproduce boxed request schema omission

* fix(tui): publish multiview control contracts

* test(tui): isolate CLI fixtures from user config

* test(tui): reproduce sidebar host leak on shutdown

* fix(tui): bind auxiliary PTYs to daemon lifetime

* test(tui): make attach smoke lifecycle-aware

* test(tui): reproduce stale surface attach race

* fix(tui): retire stale surface attach races

* test(tui): reproduce mirror retirement attach race

* fix(tui): classify superseded surface attaches

* test(tui): keep attach smoke ephemeral

* test(tui): reproduce hidden terminal host launch failure

* fix(tui): report terminal host launch failures

* test(tui): allow raced fanout completion

* test(tui): require protocol bump for multiview

* fix(tui): version the multiview resource contract

* fix(tui): keep one prelaunch resource protocol

* test(tui): reproduce concurrent PTY descriptor leak

* fix(tui): make host disconnect teardown race-free

* test(tui): reject internal session recovery errors

* fix(tui): keep recovery errors product-facing

* test(sdk): reproduce legacy terminal snapshot rejection

* fix(sdk): decode legacy terminal snapshots

* fix(sdk): derive Rust catalog hash in test

* test(sdk): lock terminal tab identity validation

* test(tui): reproduce public projection review failures

* fix(tui): make public projections self-consistent

* fix(tui): satisfy projection lints

* fix(tui): restore cross-platform validation

* test(web): isolate hosted Subrouter environment

* fix(web): inject hosted Subrouter client factory

* fix(tui): isolate Zig SDK toolchain

* fix(tui): make recovery validation deterministic

* fix(tui): preserve terminal exit stream ownership

* test(tui): cover terminal projection recovery gaps

* test(tui): require close cleanup outside creation fence

* fix(tui): harden terminal multiview lifecycle

* test(tui): make SDK cancellation check deterministic

* test(tui): make teardown checks deterministic

* test(sdk): synchronize idle stream deadline

* test(sidebar): synchronize idle stream snapshot

* test(sdk): synchronize stream close ownership

* Classify fanout completions by deadline

* fix(tui): guard late cell pixel completion

* test(tui): synchronize clear permit assertion

* test(tui): synchronize shutdown drain assertion

* test(tui): gate ordered write drain checks

* test(tui): synchronize navigation supersession

* test(tui): bound browser event fixtures

* Make selection input fixture output-free

* Make animated selection fixture output-free

* test(tui): scale loaded browser verification

* ci(tui): serialize instrumented runtime tests

* test(remote): require instrumented latency budget

* ci(remote): budget latency under instrumentation

* test(tui): synchronize deferred host ack

* ci(tui): isolate host backpressure ordering

* ci(tui): scope valgrind origin tracking

* ci(tui): scope TLS valgrind undefined checks

* test(tui): require ordered workspace publication fence

* fix(tui): publish workspace deltas in commit order

* test(tui): reject expired fanout admission

* fix(tui): stop fanout admission at shared deadline

* style(tui): format concurrency regression

* fix(tui): fence fanout deadline admission

* test(tui): keep publication probe formatter-stable

* style(tui): stabilize deadline job formatting

* refactor(tui): name resource close plan inputs

* ci(tui): isolate valgrind runtime instrumentation

* test(tui): expose hidden host frame queue limit

* fix(tui): make host backpressure byte-authoritative

* test(tui): stop leaking SDK stream metadata

* docs(tui): describe unbounded host wakeup accurately

* ci(tui): shard valgrind leak checks

* chore: keep terminal multiview change scoped

* test(tui): make scheduler fixtures cooperative

* ci(tui): isolate application valgrind tests

* test(web): cover hosted coderouter production gate

* test(zig): assert the remaining stream deadline

* docs(tui): define all-view exit detachment

* test(tui): scale remote fixtures under instrumentation

* test(go): cover omitted terminal tab identities

* refactor(go): localize terminal alias presence check

* docs(go): clarify terminal projection validation

* ci(tui): keep normal test deadlines strict

* test(tui): isolate shutdown cancellation timing

* test(tui): reject legacy zero-view snapshots

* fix(tui): restore resource protocol v2

* test(tui): await committed terminal before teardown

* test(tui): isolate render scan instrumentation

* test(tui): make queued attach deadlines deterministic

* test(tui): accept completed browser resize
2026-08-06 08:37:11 -07:00
cmux-lawrence 98abef6232 Merge remote-tracking branch 'origin/main' into codex/cmux-browser-provider 2026-08-06 08:11:29 -07:00
cmux-lawrence eb036e978c Merge remote-tracking branch 'origin/feat-terminal-multiview' into codex/cmux-browser-provider
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/server.rs
2026-08-06 08:11:25 -07:00
Lawrence Chen fb6d4c1ae5 docs(cli): sync restore help contract (#9714) 2026-08-06 07:45:14 -07:00
cmux-lawrence a97628581a Merge terminal multiview into browser provider 2026-08-06 07:38:12 -07:00
lawrencecchen 02ad3eb862 test(tui): accept completed browser resize 2026-08-06 07:00:18 -07:00
lawrencecchen 056127d9b7 test(tui): make queued attach deadlines deterministic 2026-08-06 06:20:17 -07:00
lawrencecchen 0060dc3189 test(tui): isolate render scan instrumentation 2026-08-06 05:42:21 -07:00
Lawrence Chen f5cd28364f Connect Hexclave and Stripe billing analytics to PostHog (#9702)
* Connect Stack and Stripe identities to PostHog

* Harden billing analytics delivery and sign-out

* Backfill paid plan identity on sign-in

* Clarify analytics retention disclosures

* Prevent cross-account analytics attribution

* Fail closed on unresolved analytics identity

* Gate analytics until auth identity resolves

* Preserve paid identity joins after auth gating

* Use Stack's typed cookie token store

* Observe Hexclave auth state for analytics

* Bound and coalesce analytics identity refresh

* Keep billing analytics events order independent

* Support analytics identity timeouts across browsers

* Separate auth changes from passive analytics refresh

* Fail passive identity refreshes closed

* Buffer analytics during identity revalidation

* Sanitize buffered analytics across identity changes

* Store identity marker with PostHog state

* Drop buffered events across account changes
2026-08-06 05:33:44 -07:00
lawrencecchen 0d55ba54ad test(tui): await committed terminal before teardown 2026-08-06 05:21:06 -07:00
lawrencecchen a5c24d79bf fix(tui): fail closed on browser view indexes 2026-08-06 05:07:21 -07:00
lawrencecchen 6c9519e681 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-06 04:53:50 -07:00
lawrencecchen 6fdd0dbd40 fix(tui): restore resource protocol v2 2026-08-06 04:47:52 -07:00
cmux-lawrence 03fe214ac5 Merge latest terminal multiview changes 2026-08-06 04:45:54 -07:00
cmux-lawrence 0a2f53f593 feat(tui): revision-fence viewport bootstrap creation 2026-08-06 04:45:26 -07:00
lawrencecchen f0bb535e31 test(tui): reject legacy zero-view snapshots 2026-08-06 04:42:56 -07:00
Lawrence Chen a0126d11d1 Test Stripe entitlement lifecycle states (#9707) 2026-08-06 04:34:42 -07:00
lawrencecchen 2bb6acf2a1 test(tui): harden browser index normalization 2026-08-06 04:08:37 -07:00
lawrencecchen 48a5c97652 test(tui): isolate shutdown cancellation timing 2026-08-06 04:01:32 -07:00
cmux-lawrence b885c71dec feat(tui): separate creation receipts from retry attempts 2026-08-06 03:46:06 -07:00
lawrencecchen 006fb168d0 ci(tui): keep normal test deadlines strict 2026-08-06 03:40:27 -07:00
lawrencecchen c43ee337ab fix(tui): validate multiview browser index shape 2026-08-06 03:38:51 -07:00
lawrencecchen de66eda89c test(tui): reconcile journal multiview regressions 2026-08-06 03:38:45 -07:00
cmux-lawrence dcee6081b1 Merge remote-tracking branch 'origin/feat-terminal-multiview' into codex/cmux-browser-provider
# Conflicts:
#	cmux-tui/bindings/go/operations.go
#	cmux-tui/bindings/go/resource_api_test.go
2026-08-06 03:31:32 -07:00
lawrencecchen cc92564ea8 docs(go): clarify terminal projection validation 2026-08-06 03:31:05 -07:00
cmux-lawrence 66b359e4f5 Merge remote-tracking branch 'origin/feat-cmux-tui-session-journal' into codex/cmux-browser-provider 2026-08-06 03:30:02 -07:00
lawrencecchen b1117d8edd refactor(go): localize terminal alias presence check 2026-08-06 03:23:59 -07:00
lawrencecchen 8bb3cc0394 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-06 03:18:50 -07:00
lawrencecchen 45f3856624 test(go): cover omitted terminal tab identities 2026-08-06 03:18:43 -07:00
Lawrence Chen 4c3325dad3 Merge pull request #9678 from manaflow-ai/codex/site-macos-nightly-cta
Add discoverable cmux Browser download page
2026-08-06 03:15:18 -07:00
lawrencecchen dac52bde2a Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal 2026-08-06 03:14:22 -07:00
lawrencecchen a783c2a7e3 test(tui): scale remote fixtures under instrumentation 2026-08-06 03:08:01 -07:00
cmux-lawrence 404f0108f1 fix(web): track Browser landing downloads 2026-08-06 02:59:02 -07:00
cmux-lawrence 43677144d1 test(web): cover Browser landing download analytics 2026-08-06 02:57:30 -07:00
cmux-lawrence af118fcd14 Merge remote-tracking branch 'origin/feat-terminal-multiview' into codex/cmux-browser-provider 2026-08-06 02:55:01 -07:00
cmux-lawrence 5e3dd51b85 Merge remote-tracking branch 'origin/feat-cmux-tui-session-journal' into codex/cmux-browser-provider 2026-08-06 02:55:00 -07:00
cmux-lawrence bbb720aaf6 test(tui): reconcile journal and multiview fixtures 2026-08-06 02:46:07 -07:00
cmux-lawrence de83baf7f0 test(tui): cover workspace-scoped browser receipts 2026-08-06 02:42:29 -07:00
cmux-lawrence ef13792ab1 fix(tui): repair journal test compilation 2026-08-06 02:42:29 -07:00
lawrencecchen 7870f50df0 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-06 02:42:07 -07:00
lawrencecchen c95ed1e1e8 test(tui): compare legacy terminal placement without import 2026-08-06 02:38:17 -07:00
Lawrence Chen 9bf23bcf01 Polish pricing CTAs and annual default (#9701)
* test pricing page annual default and compact CTAs

* Default pricing to annual and compact paid CTAs
2026-08-06 02:37:08 -07:00
lawrencecchen 4e5ad659c1 docs(tui): define all-view exit detachment 2026-08-06 02:35:09 -07:00
lawrencecchen 4bb7093091 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-06 02:32:01 -07:00
lawrencecchen fb7dfa3bb0 fix(tui): preserve protocol-one snapshot compatibility 2026-08-06 02:31:14 -07:00
lawrencecchen eb43345d8d test(zig): assert the remaining stream deadline 2026-08-06 02:31:11 -07:00
Lawrence Chen 8527de7a4f Fix npm bootstrap latest-tag verification (#9700)
* test: cover npm-required bootstrap latest tag

* fix: honor npm bootstrap latest invariant

* fix: redact npm tag validation failures

* fix: constrain npm bootstrap latest exception

* fix: parse npm prerelease tag safely
2026-08-06 02:28:38 -07:00
lawrencecchen b1506d2a2a test(web): cover hosted coderouter production gate 2026-08-06 02:25:48 -07:00
cmux-lawrence bcfb024ab5 Merge remote-tracking branch 'origin/feat-terminal-multiview' into codex/cmux-browser-provider 2026-08-06 02:24:00 -07:00
cmux-lawrence b15ec4e79c Merge remote-tracking branch 'origin/feat-cmux-tui-session-journal' into codex/cmux-browser-provider 2026-08-06 02:23:58 -07:00
lawrencecchen c5c6dc3f1d fix(tui): harden journal replay and SDK streams 2026-08-06 02:19:55 -07:00
lawrencecchen 6631c6451a Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-06 02:19:52 -07:00
lawrencecchen 3a76870899 ci(tui): isolate application valgrind tests 2026-08-06 02:19:24 -07:00
lawrencecchen de03a228e3 test(tui): make scheduler fixtures cooperative 2026-08-06 02:19:23 -07:00
Lawrence Chen 4569dbbaf0 Gate hosted coderouter on Pro (#9699) 2026-08-06 02:04:48 -07:00
lawrencecchen 7443d3f24b chore: keep terminal multiview change scoped 2026-08-06 01:52:15 -07:00
lawrencecchen f1225c629f ci(tui): shard valgrind leak checks 2026-08-06 01:40:38 -07:00
lawrencecchen 2ea2c5f898 test(tui): expose journal merge blockers 2026-08-06 01:28:56 -07:00
cmux-lawrence ec12d89252 Merge latest terminal multiview 2026-08-06 00:51:30 -07:00
cmux-lawrence 71998628d0 Merge latest TUI session journal 2026-08-06 00:51:30 -07:00
lawrencecchen c8c50a3d1d docs(tui): describe unbounded host wakeup accurately 2026-08-06 00:35:02 -07:00
lawrencecchen 64e70cdf92 Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_runtime.rs
2026-08-06 00:34:34 -07:00
lawrencecchen 379ea2132a test(tui): stop leaking SDK stream metadata 2026-08-06 00:32:02 -07:00
lawrencecchen 89e72c2001 fix(tui): make host backpressure byte-authoritative 2026-08-06 00:25:45 -07:00
lawrencecchen 97ec95b386 perf(tui): compact installed hook commands 2026-08-06 00:23:55 -07:00
lawrencecchen f00be78538 test(tui): bound installed hook commands 2026-08-06 00:22:35 -07:00
lawrencecchen 6aaaffc673 test(tui): expose hidden host frame queue limit 2026-08-06 00:21:00 -07:00
lawrencecchen f6a3900550 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-06 00:20:09 -07:00
lawrencecchen 8f08195915 fix(sdk): drain final stream frames on macOS 2026-08-05 23:59:04 -07:00
lawrencecchen ba10e4a156 test(sdk): expose sidebar terminal failures 2026-08-05 23:58:07 -07:00
Lawrence Chen ce79b78632 Support private Pi route authentication (#9695) 2026-08-05 23:55:39 -07:00
lawrencecchen cc4e8e61fd style(tui): satisfy stream acknowledgement lint 2026-08-05 23:54:47 -07:00
lawrencecchen 5bc1857565 fix(tui): reconcile canonical consumer fixtures 2026-08-05 23:48:45 -07:00
lawrencecchen 62f5244f97 test(tui): reject legacy terminal aliases in consumers 2026-08-05 23:43:24 -07:00
cmux-lawrence 16cb714407 Merge latest TUI session journal
# Conflicts:
#	cmux-tui/bindings/cpp/.cmux-sdk-manifest.json
#	cmux-tui/bindings/cpp/include/cmux/raw/generated/models.hpp
#	cmux-tui/bindings/go/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/go/raw/generated_commands.go
#	cmux-tui/bindings/go/raw/generated_events.go
#	cmux-tui/bindings/go/raw/generated_metadata.go
#	cmux-tui/bindings/go/raw/generated_presence_test.go
#	cmux-tui/bindings/go/raw/generated_types.go
#	cmux-tui/bindings/java/src/com/cmux/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/java/src/com/cmux/raw/Protocol.java
#	cmux-tui/bindings/python/cmux/raw/_generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/python/cmux/raw/_generated/_schema.py
#	cmux-tui/bindings/python/cmux/raw/_generated/metadata.py
#	cmux-tui/bindings/rust-sidebar/tests/runtime.rs
#	cmux-tui/bindings/rust/src/codec.rs
#	cmux-tui/bindings/rust/src/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/rust/src/generated/commands.rs
#	cmux-tui/bindings/rust/src/generated/events.rs
#	cmux-tui/bindings/rust/src/generated/metadata.rs
#	cmux-tui/bindings/rust/src/generated/mod.rs
#	cmux-tui/bindings/rust/src/generated/types.rs
#	cmux-tui/bindings/typescript/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/typescript/src/raw/generated/commands.ts
#	cmux-tui/bindings/typescript/src/raw/generated/events.ts
#	cmux-tui/bindings/typescript/src/raw/generated/index.ts
#	cmux-tui/bindings/typescript/src/raw/generated/metadata.ts
#	cmux-tui/bindings/typescript/src/raw/generated/types.ts
#	cmux-tui/bindings/zig/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/zig/src/raw/generated/protocol.zig
#	cmux-tui/crates/cmux-tui/src/main.rs
2026-08-05 23:27:51 -07:00
lawrencecchen 5562335384 ci(tui): isolate valgrind runtime instrumentation 2026-08-05 23:27:35 -07:00
cmux-lawrence 6bb27209da Merge latest terminal multiview 2026-08-05 23:12:11 -07:00
lawrencecchen 4db9904bee refactor(tui): name resource close plan inputs 2026-08-05 22:47:44 -07:00
austinpower1258 15dfbfca59 fix: route every notification center call through a bounded service
UserNotifications serializes add, settings, authorization, category, and removal operations onto one internal connection queue. A removal blocked in synchronous XPC can therefore make a later main-actor add dispatch-sync behind it and beachball the app.

Route the complete macOS notification-center surface through one dedicated serial background queue. Start an independent two-second deadline at submission time so calls queued behind a permanently wedged entry still resolve as timed out; prevent expired queued work from entering the framework later, and preserve local-feedback degradation at user-visible delivery paths.

Keep the launch-time delegate assignment synchronous because Apple requires it before didFinishLaunching returns, while category installation and every XPC-touching method use the bounded service. Add deterministic service tests for wedged entry, never-completing callbacks, queue starvation, healthy completion, authorization, and removals, and run the package suite in CI.
2026-08-05 22:44:41 -07:00
austinpower1258 699c27b2d6 test: reproduce notification center caller stall 2026-08-05 22:44:41 -07:00
lawrencecchen 771f43a6e2 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-05 22:42:50 -07:00
lawrencecchen db69ebb54e style(tui): format inactive hook regression 2026-08-05 22:41:17 -07:00
Lawrence Chen 79a3f646a9 Stop caching coderouter quota usage (#9694) 2026-08-05 22:39:44 -07:00
lawrencecchen da8cb163fd Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	.github/workflows/cmux-tui.yml
2026-08-05 22:23:13 -07:00
Lawrence Chen 2ac8401a37 Complete coderouter private beta routing reliability (#9692) 2026-08-05 22:22:24 -07:00
lawrencecchen d8be804af0 perf(tui): fail inactive agent hooks immediately 2026-08-05 22:22:07 -07:00
lawrencecchen 5e403546c2 test(tui): require stale hooks to fail fast 2026-08-05 22:10:03 -07:00
lawrencecchen d019394de3 style(tui): stabilize deadline job formatting 2026-08-05 22:04:56 -07:00
lawrencecchen 9e8ff119a0 fix(tui): scope agent trees by session identity 2026-08-05 22:04:33 -07:00
lawrencecchen 744fa50f9b test(tui): keep publication probe formatter-stable 2026-08-05 22:01:13 -07:00
lawrencecchen 594993f854 test(tui): keep progressive agent roots together 2026-08-05 21:56:21 -07:00
lawrencecchen ae7b38683f fix(tui): fence fanout deadline admission 2026-08-05 21:54:43 -07:00
lawrencecchen a4ca53d28c style(tui): format concurrency regression 2026-08-05 21:51:24 -07:00
lawrencecchen 11c0362fbf Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-05 21:50:14 -07:00
lawrencecchen a9f3672402 fix(tui): stop fanout admission at shared deadline 2026-08-05 21:50:06 -07:00
lawrencecchen 8f2c9a30c3 test(tui): reject expired fanout admission 2026-08-05 21:49:25 -07:00
lawrencecchen cedfd80ea6 fix(tui): publish workspace deltas in commit order 2026-08-05 21:41:35 -07:00
lawrencecchen 91f8e47ad5 perf(tui): delegate hook activation to helper 2026-08-05 21:34:48 -07:00
lawrencecchen ff257deec4 test(tui): require ordered workspace publication fence 2026-08-05 21:24:59 -07:00
cmux-lawrence a45c9b5e6f Merge latest cmux main 2026-08-05 21:22:12 -07:00
cmux-lawrence 0c1a436944 Merge latest terminal multiview fixes 2026-08-05 21:22:12 -07:00
Abdulaziz AlbaharandClaude Fable 5 e01238959d iOS: Make Tailscale connection method strict (#9497)
* test(ios): require truthful Tailscale-only selection

* fix(ios): make Tailscale selection authoritative

* test(ios): verify strict transport choice copy

* fix(ios): stop Iroh discovery in strict mode

* test(ios): align strict transport fixtures

* test(ios): await failed route teardown

* test(ios): assert transport selection behavior

* fix(ios): bind onboarding preview transport choice

* test(ios): pin exclusive method selection and physical teardown

Review follow-ups from PR 9497: the onboarding UI test now asserts the
untapped method is deselected after each tap, the strict-switch test
polls the boxed live Iroh transport for physical close instead of only
the store's logical route, and the connectionMethodStore declaration
documents why nil is unreachable in the shipping app.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-05 21:06:07 -07:00
lawrencecchen 9d840c07d6 test(tui): require minimal delegated hook commands 2026-08-05 21:05:41 -07:00
cmux-lawrence 390291c28b feat(tui): attach browser surfaces to native provider 2026-08-05 21:04:27 -07:00
Lawrence Chen 4665c6ea8b Allow coderouter legacy cleanup after new accounts (#9689)
* Allow cleanup after new encrypted accounts are added

* Make legacy credential migration monotonic
2026-08-05 20:59:49 -07:00
lawrencecchen 2a42acbc8f Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	.github/workflows/cmux-tui.yml
#	cmux-tui/bindings/cpp/.cmux-sdk-manifest.json
#	cmux-tui/bindings/cpp/include/cmux/raw/generated/models.hpp
#	cmux-tui/bindings/go/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/go/raw/generated_commands.go
#	cmux-tui/bindings/go/raw/generated_events.go
#	cmux-tui/bindings/go/raw/generated_metadata.go
#	cmux-tui/bindings/go/raw/generated_presence_test.go
#	cmux-tui/bindings/go/raw/generated_types.go
#	cmux-tui/bindings/java/src/com/cmux/raw/.cmux-sdk-manifest.json
#	cmux-tui/bindings/java/src/com/cmux/raw/Protocol.java
#	cmux-tui/bindings/python/cmux/raw/_generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/python/cmux/raw/_generated/_schema.py
#	cmux-tui/bindings/python/cmux/raw/_generated/metadata.py
#	cmux-tui/bindings/rust-sidebar/tests/runtime.rs
#	cmux-tui/bindings/rust/src/client.rs
#	cmux-tui/bindings/rust/src/codec.rs
#	cmux-tui/bindings/rust/src/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/rust/src/generated/commands.rs
#	cmux-tui/bindings/rust/src/generated/events.rs
#	cmux-tui/bindings/rust/src/generated/metadata.rs
#	cmux-tui/bindings/rust/src/generated/mod.rs
#	cmux-tui/bindings/rust/src/generated/types.rs
#	cmux-tui/bindings/rust/src/resource/client.rs
#	cmux-tui/bindings/rust/tests/mock_server.rs
#	cmux-tui/bindings/typescript/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/typescript/src/raw/generated/commands.ts
#	cmux-tui/bindings/typescript/src/raw/generated/events.ts
#	cmux-tui/bindings/typescript/src/raw/generated/index.ts
#	cmux-tui/bindings/typescript/src/raw/generated/metadata.ts
#	cmux-tui/bindings/typescript/src/raw/generated/types.ts
#	cmux-tui/bindings/zig/src/raw/generated/.cmux-sdk-manifest.json
#	cmux-tui/bindings/zig/src/raw/generated/protocol.zig
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_protocol.rs
#	cmux-tui/crates/cmux-tui-core/tests/browser_runtime.rs
#	cmux-tui/crates/cmux-tui/src/session/mod.rs
#	cmux-tui/crates/cmux-tui/src/session/remote.rs
2026-08-05 20:53:51 -07:00
Lawrence Chen caf19d82fd Move coderouter credentials to KMS-encrypted RDS (#9686) 2026-08-05 20:51:08 -07:00
Lawrence Chenandcmux-lawrence 7edde50e21 Limit transient Actions artifact retention (#9687)
Co-authored-by: cmux-lawrence <[email protected]>
2026-08-05 20:40:36 -07:00
lawrencecchen adf24ae22c ci(tui): scope TLS valgrind undefined checks 2026-08-05 20:37:24 -07:00
austinpower1258 edce9f5b68 Merge remote-tracking branch 'origin/main' into issue-9520-hermes-first-class 2026-08-05 20:28:57 -07:00
austinpower1258 5252b324b5 fix: address Hermes lifecycle review findings 2026-08-05 20:28:12 -07:00
lawrencecchen be17bc0c1e perf(tui): keep agent hooks off critical paths 2026-08-05 20:25:41 -07:00
austinpower1258 9f9872e564 test: cover Hermes review regressions 2026-08-05 20:24:15 -07:00
lawrencecchen c4a31d85f7 ci(tui): scope valgrind origin tracking 2026-08-05 20:20:17 -07:00
lawrencecchen 919a164250 ci(tui): isolate host backpressure ordering 2026-08-05 20:08:08 -07:00
lawrencecchen 633375d3ed test(tui): synchronize deferred host ack 2026-08-05 20:08:08 -07:00
lawrencecchen 3872207c66 test(tui): require nonblocking portable agent hooks 2026-08-05 20:03:36 -07:00
cmux-lawrence 029b8c0ec4 Merge remote-tracking branch 'origin/feat-terminal-multiview' into codex/cmux-browser-provider
# Conflicts:
#	.github/workflows/cmux-tui.yml
#	cmux-tui/crates/cmux-tui-core/tests/browser_runtime.rs
#	cmux-tui/crates/cmux-tui/src/session/remote.rs
2026-08-05 20:02:44 -07:00
lawrencecchen d73fe9a56a ci(remote): budget latency under instrumentation 2026-08-05 19:40:36 -07:00
lawrencecchen b54d74441a test(remote): require instrumented latency budget 2026-08-05 19:39:26 -07:00
lawrencecchen 4fbbcca4bc ci(tui): serialize instrumented runtime tests 2026-08-05 19:12:04 -07:00
lawrencecchen 15de9dd8b6 test(tui): scale loaded browser verification 2026-08-05 18:55:23 -07:00
lawrencecchen ffea43e97b Make animated selection fixture output-free 2026-08-05 18:46:50 -07:00
lawrencecchen ccf2165cd4 Make selection input fixture output-free 2026-08-05 18:46:50 -07:00
lawrencecchen 0a8d864e25 test(tui): bound browser event fixtures 2026-08-05 18:45:24 -07:00
lawrencecchen 545d767a28 test(tui): synchronize navigation supersession 2026-08-05 18:40:06 -07:00
lawrencecchen 80a34af8a4 Merge latest terminal multiview into journal integration base 2026-08-05 18:24:40 -07:00
lawrencecchen ec4fae94f5 test(tui): gate ordered write drain checks 2026-08-05 18:20:27 -07:00
lawrencecchen f30078837f test(tui): synchronize shutdown drain assertion 2026-08-05 18:10:42 -07:00
lawrencecchen 09701ba422 fix(tui): interrupt journal streams on signal 2026-08-05 17:54:33 -07:00
lawrencecchen e9f4829ccb Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-05 17:49:56 -07:00
lawrencecchen 05fda1290a test(tui): synchronize clear permit assertion 2026-08-05 17:49:12 -07:00
lawrencecchen 7f7bc01057 test(tui): cover prompt journal stream interrupt 2026-08-05 17:45:17 -07:00
Lawrence Chen 6bb1d7b5f7 Share coderouter usage cache across Vercel instances (#9679) 2026-08-05 17:37:53 -07:00
lawrencecchen 4eeb357a94 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-05 17:27:10 -07:00
lawrencecchen 418e41b356 fix(tui): guard late cell pixel completion 2026-08-05 17:26:57 -07:00
lawrencecchen af8912444d Classify fanout completions by deadline 2026-08-05 17:25:38 -07:00
Lawrence Chen ad214598a7 Cache coderouter usage and collapse token auth query (#9676) 2026-08-05 17:25:08 -07:00
cmux-lawrence 592f98bdaf web: remove untranslated Browser detail 2026-08-05 17:23:59 -07:00
cmux-lawrence 49aca0ddc7 web: localize Browser availability copy 2026-08-05 17:23:02 -07:00
cmux-lawrence 01ac0386c4 web: label Browser channel explicitly 2026-08-05 17:22:05 -07:00
cmux-lawrence 288315003c web: add cmux Browser download landing 2026-08-05 17:17:14 -07:00
lawrencecchen 2f4986ace3 docs(tui): clarify agent hook activation 2026-08-05 17:16:07 -07:00
Lawrence Chen 7fbd9d2ff7 Parallelize coderouter usage and add dedicated landing (#9675) 2026-08-05 17:13:21 -07:00
lawrencecchen 2c564d2577 test(sdk): synchronize stream close ownership 2026-08-05 17:11:09 -07:00
lawrencecchen cc11ec940c test(sidebar): synchronize idle stream snapshot 2026-08-05 16:45:59 -07:00
lawrencecchen 6caecb452c test(sdk): synchronize idle stream deadline 2026-08-05 16:32:07 -07:00
lawrencecchen 73c61239ba test(tui): make teardown checks deterministic 2026-08-05 16:13:48 -07:00
lawrencecchen f7c1cf7dc3 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-05 15:47:34 -07:00
lawrencecchen 0108db569f test(tui): make SDK cancellation check deterministic 2026-08-05 15:46:59 -07:00
Abdulaziz Albahar 4b7796a960 Fix end-to-end push notification reliability (#9319)
* test push notification delivery failures

* test remaining push reliability failures

* test tighten push reliability contracts

* test prevent duplicate alerts across push retries

* test persist partial push delivery retries

* test serialize persisted push retries

* test preserve unregisters across account switches

* test migrate unregister tombstones to queue

* test retry transient registration statuses

* test close push delivery retry gaps

* test bind push retries to logical payloads

* fix reliable push delivery backend

* test push registration ownership and rotation

* test fix phone admission boundary and race

* test cover push registration lifecycle races

* test reuse push expiration across retries

* test compile push lifecycle assertions

* test push readiness and responsive toggles

* test reuse expiration after stale push lease

* test isolate async URL protocol callbacks

* test align APNs retries with provider guidance

* test require APNs correlation UUIDs

* test detach scripted push transport

* fix APNs retry and request identity policy

* test isolate APNs retry classes and expiration validation

* test preserve logical push expiration across retries

* test make push registration transport deterministic

* test freeze push recipients and crash recovery state

* fix isolate durable push delivery workflow

* test enforce APNs provider boundaries

* test reuse APNs provider connections

* fix bound and reuse APNs delivery sessions

* test preserve push correlation on internal failure

* test cover push readiness recovery and live admission

* fix retain push correlation on internal failures

* fix make push registration account safe

* test reject originless push redirects

* fix reject non-http push redirects

* fix expose live push readiness

* fix fail closed on TestFlight push entitlements

* test reject false push readiness

* fix fail closed on push readiness

* test specify durable phone push queue

* test cover push queue recovery boundaries

* test expose phone forwarding controls

* fix expose phone forwarding controls

* test wire phone push queue coverage

* test preserve provider push retry delay

* test reject stale notification policy completion

* test publish push auth identity transitions

* test expose phone push queue health

* test expose truthful push test stage

* fix make phone push delivery durable

* test report degraded push retry storage

* test retry transient push authentication

* fix retry transient push authentication

* fix expose end-to-end push readiness

* fix serialize push settings mutations

* fix emulate signing entitlement booleans

* fix compile phone push RPC handlers

* fix route all push popover entrypoints

* test stabilize push database stress cases

* fix ios push readiness module dependency

* fix audit missing push credentials

* test bound device token stress cases

* test expose unconfigured push provider

* fix report unconfigured push provider

* fix import push status in ios coordinator

* test: cover push review regressions

* fix: harden phone push delivery end to end

* fix return restored agent argument

* fix: address push reliability review findings

Server: delivery no longer aborts with the client request (partial APNs
outcomes survive disconnects), provider backoffs are clamped to the event
TTL or finalized as expired, expired records stop blocking reclaims,
account deletion waits for active device delivery leases, and lease
release uses a partial index instead of a table scan.

Mac: queue restore adopts the observed identity so the identity stream's
first yield no longer clears the persisted queue, the queue store
scavenges abandoned tmp snapshots, and in-flight policy discard uses the
delivery-identity index.

iOS: push-readiness preview env key routed through UITestConfig, device
limit count localized via localizedStringWithFormat.

Tests: TTL clamp unit + route coverage, aborted-signal regression,
unknown-admission fail-closed, negative Retry-After clamp, retainOnly
generation scope, account-deletion lease block.

* fix notifications phone push settings bindings

* test clamp provider push retry to event ttl

* test allow retry-after rounding drift

* fix: route phone_push.status.get auth failures to reauth disconnect

A revoked, expired, or account-mismatched token during the authenticated
status probe previously left the shell connected and only cleared push
readiness. Definitive authorization errors now route through
disconnectForAuthorizationFailureIfNeeded like every other authenticated
RPC; transient failures still just clear the cached Mac status.

* fix: lockfile guard false positive, IPA gate messages, test isolation

The Package.resolved policy flagged this PR for adding a path dependency
(CmuxMobileRPC into CmuxMobileShellUI) that was already reachable through
CmuxMobileShell: Xcode's originHash covers resolved remote inputs, so no
honest resolution changes any lockfile byte (verified against Xcode 26.6,
which rewrites the swift-CLI hash back). The guard now requires lockfile
diffs only for dependency edits that can move the pinned set: changed
remote requirements, or path edits pulling previously unreachable remote
pins in or out. Verified both negative cases still fail.

upload-testflight entitlement-gate failures now name the durable IPA
instead of the deleted temp-extracted .app path.

Two push-coordinator lifecycle tests now use isolated UserDefaults suites
instead of leaking through .standard.

* test: cover deferred push edge cases

* fix: close deferred push reliability gaps

* test: make injected push clock assertion causal

* fix iOS push status text returns

* fix: make push timeout sendable

* fix: baseline inherited iOS convention debt

* fix: route iOS convention baseline lint

* fix: make live push clock sendable

* test: cover remaining push delivery races

* test: harden push verification boundaries

* fix: close final push delivery races

* test: cover final push edge cases

* fix: preserve truthful push readiness results

* test: synchronize push queue lock coverage

* fix: call the Darwin file lock function

* fix: preserve push preview initializer order

* test: fix workspace drop fixture initializer

* test: synchronize push settings mutations

* test: tap push switch controls reliably

* test: require push opt-in at workspace list

* test: correct push lifecycle assertions

* test: make push settings regression observable

* test: keep push settings assertions alive headlessly

* fix: enable reliable mobile push notifications
2026-08-05 15:41:33 -07:00
Abdulaziz AlbaharandClaude Fable 5 6d0d313622 Read hosted tenant delete token lazily from process.env (#9669)
0eecd5afea (#9607) switched SUBROUTER_STACK_TENANT_DELETE_TOKEN to the
validated env object, but t3-env freezes values at first import, so
tenant-control configuration became unobservable after boot and the
unconfigured paths broke: the exchange route returns 200 instead of 503
and account deletion fires hosted tenant deletes for accounts that never
enabled Subrouter. web tests have been red on main since (CI paused).
env.ts still validates presence on Vercel non-preview deployments.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-05 15:40:58 -07:00
lawrencecchen 940e8481af fix(tui): harden terminal multiview lifecycle 2026-08-05 15:25:12 -07:00
lawrencecchen 21cfbda64f test(tui): require close cleanup outside creation fence 2026-08-05 14:52:58 -07:00
lawrencecchen 327cbe6253 test(tui): cover terminal projection recovery gaps 2026-08-05 14:14:21 -07:00
lawrencecchen cd7ce7af84 Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/browser.rs
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_runtime.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/public_projection_store.rs
#	cmux-tui/crates/cmux-tui-core/tests/browser_runtime.rs
2026-08-05 09:38:58 -07:00
lawrencecchen 23bb4a9b5a perf(tui): trim journal queue allocations 2026-08-05 08:28:44 -07:00
lawrencecchen f00423e716 Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	.github/workflows/cmux-tui.yml
#	cmux-tui/bindings/rust-sidebar/tests/runtime.rs
#	cmux-tui/crates/cmux-tui/src/ui/graphics_writer.rs
2026-08-05 08:18:43 -07:00
lawrencecchen 7490693e51 fix(tui): preserve terminal exit stream ownership 2026-08-05 08:10:58 -07:00
lawrencecchen b9faca0d87 feat(tui): harden session journal hooks 2026-08-05 08:09:37 -07:00
lawrencecchen c94324fb86 fix(tui): make recovery validation deterministic 2026-08-05 06:33:08 -07:00
lawrencecchen c7a8940974 fix(tui): inventory sidebar menu actions 2026-08-05 06:21:40 -07:00
lawrencecchen a2d744109e fix(tui): isolate Zig SDK toolchain 2026-08-05 05:46:39 -07:00
lawrencecchen 88646922d4 feat(tui): add sidebar profiles and SSH connection flow 2026-08-05 05:44:01 -07:00
lawrencecchen fa06e524cc fix(tui): import projection id in app tests 2026-08-05 05:19:48 -07:00
lawrencecchen a2b4b6817b Merge remote-tracking branch 'origin/pr-9387' into feat-cmux-tui-session-journal
# Conflicts:
#	cmux-tui/bindings/rust/src/codec.rs
#	cmux-tui/bindings/rust/src/resource/client.rs
2026-08-05 05:16:46 -07:00
lawrencecchen be78737a4a fix(web): inject hosted Subrouter client factory 2026-08-05 05:07:21 -07:00
lawrencecchen b3ea7e4415 test(web): isolate hosted Subrouter environment 2026-08-05 04:57:30 -07:00
lawrencecchen 78474245e3 fix(tui): restore cross-platform validation 2026-08-05 04:57:19 -07:00
lawrencecchen d087539bb9 test(tui): cover machine connection and sidebar profiles 2026-08-05 04:35:21 -07:00
lawrencecchen 885f440076 Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal 2026-08-05 04:28:08 -07:00
lawrencecchen 9d12698a4a fix(tui): make terminal placements canonical 2026-08-05 04:27:57 -07:00
lawrencecchen cc6a64ab58 test(tui): reject redundant terminal placement alias 2026-08-05 04:27:38 -07:00
lawrencecchen 7131fc976d Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-05 02:45:26 -07:00
lawrencecchen 0dc88abd92 fix(tui): satisfy projection lints 2026-08-05 02:44:22 -07:00
lawrencecchen f48773bea5 Merge remote-tracking branch 'origin/main' into feat-cmux-tui-session-journal 2026-08-05 02:39:56 -07:00
lawrencecchen 69e6423d99 Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	cmux-tui/bindings/rust/tests/catalog_manifest.rs
#	cmux-tui/crates/cmux-tui-core/src/resource_api.rs
2026-08-05 02:39:49 -07:00
lawrencecchen 92094c2e24 perf(tui): batch journal topology enrichment 2026-08-05 02:39:14 -07:00
lawrencecchen 78fcd06dfd feat(tui): add bounded journal replay 2026-08-05 02:39:09 -07:00
lawrencecchen 1c5ff143d0 fix(tui): derive agent edges from session ancestry 2026-08-05 02:39:01 -07:00
lawrencecchen 62827450b4 fix(tui): make public projections self-consistent 2026-08-05 02:37:29 -07:00
lawrencecchen d8fa39962f test(tui): reproduce public projection review failures 2026-08-05 02:32:26 -07:00
lawrencecchen 03db16136a Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-05 02:27:59 -07:00
Lawrence Chen 2d9ba4b090 Proxy Codex model discovery through CodeRouter (#9641)
* test: require CodeRouter model discovery proxy

* Proxy Codex model discovery through CodeRouter
2026-08-05 02:22:46 -07:00
Lawrence Chen fe198fb88d Restore hosted Subrouter CLI configuration (#9638)
* test: preserve hosted Subrouter CLI config

* fix: restore hosted Subrouter CLI config
2026-08-05 02:22:02 -07:00
lawrencecchen cb31615a1b Close SSH carriers gracefully before reaping 2026-08-05 02:20:00 -07:00
Lawrence Chen 81ae632de6 Fix SDK registry bootstrap verification (#9640)
* test: cover omitted PyPI publisher claims

* fix: repair SDK bootstrap verification
2026-08-05 02:14:09 -07:00
lawrencecchen 073a25c591 test: require graceful SSH carrier close 2026-08-05 02:01:02 -07:00
lawrencecchen 708bae372d test(tui): require session-derived agent ancestry 2026-08-05 02:00:50 -07:00
lawrencecchen 0198a9942e fix(tui): preserve typed terminal launch failures 2026-08-05 01:59:53 -07:00
lawrencecchen bb7a9c20d4 fix(tui): remove unshipped snapshot compatibility 2026-08-05 01:59:46 -07:00
lawrencecchen 5d05b2acf9 test(sdk): lock terminal tab identity validation 2026-08-05 01:52:12 -07:00
lawrencecchen 6bfe30255a Reap pooled SSH runtimes on exit 2026-08-05 01:51:34 -07:00
lawrencecchen b11b008810 fix(sdk): derive Rust catalog hash in test 2026-08-05 01:49:05 -07:00
lawrencecchen 24eb1bb86f Merge remote-tracking branch 'origin/main' into feat-cmux-tui-session-journal 2026-08-05 01:40:39 -07:00
lawrencecchen 9e370b9368 Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	cmux-tui/bindings/cpp/.cmux-resource-api.json
#	cmux-tui/bindings/go/.cmux-resource-api.json
#	cmux-tui/bindings/java/.cmux-resource-api.json
#	cmux-tui/bindings/python/.cmux-resource-api.json
#	cmux-tui/bindings/rust/.cmux-resource-api.json
#	cmux-tui/bindings/typescript/.cmux-resource-api.json
#	cmux-tui/bindings/zig/.cmux-resource-api.json
#	cmux-tui/crates/cmux-pty/src/macos.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/tests.rs
2026-08-05 01:40:29 -07:00
lawrencecchen c43d891622 fix(tui): migrate owned frontend projections 2026-08-05 01:35:39 -07:00
lawrencecchen fde3749a79 fix(sdk): decode legacy terminal snapshots 2026-08-05 01:31:20 -07:00
lawrencecchen 7a609cfe4f test(tui): require owned projection migration 2026-08-05 01:28:53 -07:00
Lawrence Chen 40ff1c1667 Restore Vercel OIDC credentials for RDS access (#9637)
* test: require Vercel OIDC for RDS pools

* Restore Vercel OIDC credentials for runtime RDS access
2026-08-05 01:28:33 -07:00
lawrencecchen 4bb6fdcd0d Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-05 01:28:19 -07:00
lawrencecchen 9aabb2aaf1 Keep machine connections warm across switches 2026-08-05 01:26:55 -07:00
lawrencecchen a565a3042f feat(tui): scope frontend views with durable leases 2026-08-05 01:24:27 -07:00
lawrencecchen 29f1d3ee86 test(sdk): reproduce legacy terminal snapshot rejection 2026-08-05 01:16:27 -07:00
Lawrence Chen 0447b3cb45 Fix CodeRouter responses middleware routing (#9636)
* test: preserve CodeRouter data-plane route through middleware

* Bypass localization for CodeRouter responses
2026-08-05 01:08:19 -07:00
lawrencecchen 2871a0a50d fix(tui): keep recovery errors product-facing 2026-08-05 01:07:35 -07:00
lawrencecchen 258b208e82 test(tui): reject internal session recovery errors 2026-08-05 01:06:21 -07:00
Lawrence Chen 6e9ca01fc4 Move CodeRouter data plane to Vercel (#9633)
* test: require Vercel-native CodeRouter data plane

* Move CodeRouter data plane to Vercel

* Allow authenticated operators to run RDS migrations

* Serialize refreshes and fail over cooked accounts
2026-08-05 00:56:28 -07:00
Austin Wang c9e66e8dc9 Merge pull request #9631 from manaflow-ai/fix-9624-restore-caller-identification
Fix restore caller surface resolution
2026-08-05 00:55:35 -07:00
lawrencecchen 806dc6e608 fix(tui): make host disconnect teardown race-free 2026-08-05 00:47:00 -07:00
lawrencecchen 1587afc334 test(tui): require window-scoped frontend ownership 2026-08-05 00:15:03 -07:00
lawrencecchen 83497be0f6 feat(tui): journal resource effect outcomes 2026-08-05 00:10:22 -07:00
lawrencecchen 568b0297bd test(tui): reproduce concurrent PTY descriptor leak 2026-08-05 00:08:16 -07:00
lawrencecchen 817da44a07 test(tui): require journaled resource effect outcomes 2026-08-05 00:03:40 -07:00
lawrencecchen f9d7c8af24 Merge branch 'feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	cmux-tui/spec/terminal-host.md
2026-08-04 23:55:49 -07:00
lawrencecchen 501e287cb6 docs(tui): define persistent frontend ownership 2026-08-04 23:55:09 -07:00
lawrencecchen 097c586f2c fix(tui): keep one prelaunch resource protocol 2026-08-04 23:54:50 -07:00
austinpower1258 7ee8f64f9d fix(cli): restore caller surface resolution 2026-08-04 23:50:09 -07:00
lawrencecchen 310e99aa57 test: require immediate persistent machine selection 2026-08-04 23:49:49 -07:00
Lawrence Chen d228709009 Merge pull request #9632 from manaflow-ai/codex/linux-auto-update-cta
web: prioritize auto-updating Linux installer
2026-08-04 23:21:15 -07:00
cmux-lawrence 0c80c2896c web: prioritize auto-updating Linux installer 2026-08-04 23:16:06 -07:00
lawrencecchen 28da4e69e0 fix(tui): version the multiview resource contract 2026-08-04 23:09:40 -07:00
cmux-lawrence ff147074bd test(web): require auto-updating Linux installer CTA 2026-08-04 23:06:30 -07:00
cmux reload-cloud 5d286eda8b feat: activate lifecycle-accurate Hermes hooks 2026-08-04 23:03:30 -07:00
cmux reload-cloud 25dbdc9a2b test: reject premature Hermes smart-approval notifications 2026-08-04 22:40:50 -07:00
Austin Wang ec0cd3308e Merge pull request #9324 from mykmelez/myk/fix-stale-port-badges
Retire stale sidebar port badges despite vanished TTYs, privileged owners, and zombies
2026-08-04 22:30:30 -07:00
lawrencecchen 80b57d3e17 test(tui): require protocol bump for multiview 2026-08-04 22:17:21 -07:00
lawrencecchen 64e3969f0e Merge remote-tracking branch 'origin/main' into feat-cmux-tui-session-journal 2026-08-04 22:16:55 -07:00
lawrencecchen 4106ee805f Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_protocol.rs
#	cmux-tui/crates/cmux-tui-core/src/terminal_host_runtime.rs
2026-08-04 22:16:49 -07:00
lawrencecchen cf7474edc5 Document terminal host launch failures 2026-08-04 22:15:24 -07:00
Austin Wang ca1e087edf Merge pull request #9621 from manaflow-ai/fix-9619-restore-preserves-workspace-title
Preserve workspace titles during session restore
2026-08-04 22:08:03 -07:00
Lawrence Chen 0ff3fdc2f2 Merge pull request #9628 from manaflow-ai/codex/website-download-routes
Publish verified cmux Browser nightly downloads
2026-08-04 22:06:02 -07:00
lawrencecchen 5fae1279d2 Make animated selection fixture output-free 2026-08-04 22:04:30 -07:00
lawrencecchen 8698ef6fc0 Render status for empty machine sessions 2026-08-04 22:02:06 -07:00
cmux-lawrence 3060a4054e web: align mac downloads with universal artifacts 2026-08-04 22:00:48 -07:00
lawrencecchen 7bd9a69b8b test: render status without a workspace 2026-08-04 21:59:07 -07:00
austinpower1258 929dee81ef fix: reconcile title after focused surface transfer 2026-08-04 21:59:03 -07:00
austinpower1258 f9ecd31fdc Fix diff comment payload integration 2026-08-04 21:58:28 -07:00
austinpower1258 fca797fdd8 Add coverage for shared diff comment lifecycle JSON 2026-08-04 21:56:03 -07:00
lawrencecchen bff27eacd3 Give serialized Valgrind coverage headroom 2026-08-04 21:50:59 -07:00
cmux-lawrence 17709529d0 web: add verified browser nightly downloads 2026-08-04 21:50:48 -07:00
austinpower1258 10eb95cbf6 test(cli): reproduce restore caller resolution regression 2026-08-04 21:38:30 -07:00
lawrencecchen f5f9bd0f93 Keep machine sidebar usable when terminal launch fails 2026-08-04 21:28:41 -07:00
lawrencecchen be464f3a67 test(tui): allow raced fanout completion 2026-08-04 21:28:27 -07:00
lawrencecchen dfafe87935 fix(tui): report terminal host launch failures 2026-08-04 21:26:11 -07:00
lawrencecchen 080ee02201 test: preserve terminal host launch errors 2026-08-04 21:22:43 -07:00
lawrencecchen 45ba66fdee test(tui): reproduce hidden terminal host launch failure 2026-08-04 21:13:28 -07:00
lawrencecchen d2c12ca2ce Serialize core tests under Valgrind 2026-08-04 21:08:36 -07:00
lawrencecchen 38741b4041 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview
# Conflicts:
#	cmux-tui/dist/scripts/test_linux_packages.py
2026-08-04 21:01:01 -07:00
lawrencecchen f27db14e3a Make selection input fixture output-free 2026-08-04 20:47:40 -07:00
lawrencecchen c1da806141 Make terminal detach the final stream marker 2026-08-04 20:41:46 -07:00
lawrencecchen 70ddd4e3ab Test self-detach terminal ordering 2026-08-04 20:38:06 -07:00
haung921209 0d93f18f85 Resolve comments CLI strings from the enclosing app bundle
The internationalization check is right that cmux-cli ships no string catalog,
so String(localized:) in the CLI renders its default value — measured earlier
when a plural variation failed to select. Rather than add a resource phase to a
command-line tool, this routes the command's strings through
CMUXDiffViewerLocalization, which already resolves
CLIExecutableLocator.enclosingAppBundle() and honors AppleLanguages.

Verified per locale: help renders 使い方 under ja and 사용법 under ko, the list
header renders "Комментарии ревью: 3" under ru, state labels render 未対応 and
в ожидании, and the guards render their Russian and Japanese messages.

Also documents DiffComment.init and the payload tests.
2026-08-05 12:24:53 +09:00
lawrencecchen ac8cca6a49 Stabilize deadline and drain verification 2026-08-04 20:20:27 -07:00
haung921209 0391f22bda Keep count selection in code, drop numeral-governed nouns from plural strings
The Slavic plural finding was right, but CLDR variations do not fire here: the
CLI resolves strings against its own bundle, which carries no string catalog, so
every localized CLI string renders its defaultValue. Measured — with the header
moved to variations, count=1 printed "1 review comments".

So selection stays in code (correct singular today) and the plural strings avoid
letting the numeral govern the noun, which keeps one form grammatical for every
count above one: ru "Комментарии ревью: %1$lld", uk "Коментарі рецензування:
%1$lld", pl "Komentarze przeglądu: %1$lld", ar "تعليقات المراجعة: %1$lld".
Bosnian already used a form valid for every count above one.
2026-08-05 12:08:52 +09:00
lawrencecchen d538be2cb1 Synchronize browser pointer test with frame authority 2026-08-04 20:06:44 -07:00
austinpower1258 b5a6cb640f fix: harden restored title boundary lifecycle 2026-08-04 20:04:07 -07:00
lawrencecchen 22203f0168 Derive browser retry test budget 2026-08-04 19:51:52 -07:00
lawrencecchen 958936f126 Classify fanout completions by deadline 2026-08-04 19:51:52 -07:00
haung921209 10a04aeede Make DiffCommentPayload a constructable mapper instead of a static namespace
The ambient-global-state rule flags a caseless enum whose whole API is static
helpers, and it was the right call here: an instance can own the formatter, so
"one formatter per reply" is now a property of the type rather than something
each caller has to remember. Tests can inject a formatter too.

TerminalController, DiffCommentsBridge, and the tests construct a mapper; the
bridge's list reply collapses to comments.map(payload.json).
2026-08-05 11:44:38 +09:00
haung921209 9b493b8aba Point the CmuxDiffComments product at its own package reference
The product dependency I added carried CMUXDebugLog's package UUID with a
CmuxDiffComments comment, so a clean resolve could have associated the product
with the wrong package. My local builds hid it behind an already-resolved
SourcePackages cache.

Audited every XCSwiftPackageProductDependency in the project against its
referenced object: no other mismatch. Verified with SourcePackages deleted so
resolution ran from scratch.

Also documents the `ls` alias in the comments contract row.
2026-08-05 11:34:19 +09:00
austinpower1258 b3c16f3478 fix: preserve restored workspace titles 2026-08-04 19:33:33 -07:00
lawrencecchen 6975c4f646 Stabilize Rust stream close test 2026-08-04 19:25:55 -07:00
austinpower1258 24f6e13fc1 test: preserve titles across restore bootstrap 2026-08-04 19:17:25 -07:00
haung921209 e4f26e572a Extract DiffComment and the payload mapping into CmuxDiffComments
Per the package-boundary review: the comment model and its wire mapping are
reusable domain logic shared by the WebKit bridge, the socket method, and the
tests, so they move behind a SwiftPM boundary.

- Packages/macOS/CmuxDiffComments exposes public DiffComment and
  DiffCommentPayload and depends on Foundation only.
- Persistence stays app-side: DiffCommentStore keeps its directory resolution
  and the SessionRestorePolicy test check, so no app-wide dependency crosses
  into the package.
- The app target, bridge, socket handler, and both test files import the
  package; no behavior changes.
2026-08-05 11:12:16 +09:00
lawrencecchen 1d407b5da9 Align C++ protocol coverage count 2026-08-04 19:10:21 -07:00
lawrencecchen 0a401b2b5b Align SDK coverage with protocol inventory 2026-08-04 19:05:18 -07:00
Austin Wang 2f3d92281f Merge pull request #9566 from manaflow-ai/issue-9518-dock-focus-cmd-l-cmd-shift-t
Route all surface shortcuts through focused Dock
2026-08-04 19:02:07 -07:00
lawrencecchen 75115171f1 Record terminal host activation message 2026-08-04 19:01:30 -07:00
lawrencecchen a3a99c487d Stabilize hosted terminal verification 2026-08-04 18:57:21 -07:00
haung921209 fc5ef0d714 Move comment wire mapping into DiffCommentPayload
Both surfaces that serialize review comments now share one type that touches
neither AppKit nor controller state: TerminalController keeps only the socket
dispatch, DiffCommentsBridge keeps only the WebKit glue, and the mapping and
consumed-filtering live in DiffCommentPayload with the tests pointed at it.

This also makes the two response paths structurally identical, so the webview
reply cannot drift back to allocating a formatter per comment. If the project
wants this behind a SwiftPM package, it is now a file move rather than a
refactor.
2026-08-05 10:37:43 +09:00
lawrencecchen fe6fedbe23 Satisfy strict journal router lint 2026-08-04 18:18:39 -07:00
haung921209 05419f4eba Translate the new catalog keys, generalize the socket error, add docstrings
- Replace the English fallbacks with real translations for all 18 remaining
  locales across the 13 keys this PR adds. The internationalization rule lists
  copied English as an unacceptable way to fill a locale slot, so matching the
  catalog's existing fallback habit was not enough for new keys. CLI tokens and
  format specifiers are preserved verbatim.
- socket.comments.missingRepoRoot now reads "A repository path is required."
  The pre-merge privacy check wants API identifiers out of user-facing text;
  the CLI already fails with its own message first, so a direct socket caller
  loses nothing it cannot get from the invalid_params code.
- Document the four comments CLI helpers.
2026-08-05 10:15:58 +09:00
Lawrence Chen a2b3c10f11 Fix cmux TUI release packaging (#9608)
* Fix cmux TUI release packaging

* Allow TUI release branch dry runs

* Update TUI package smoke command

* Revert "Allow TUI release branch dry runs"

This reverts commit 0ccb643704.
2026-08-04 18:15:32 -07:00
lawrencecchen a2b3b37f16 Merge latest terminal multiview base 2026-08-04 18:13:26 -07:00
Lawrence Chen 0eecd5afea Fix CodeRouter trusted tenant exchange (#9607)
* Make hosted tenant exchange self-contained

* Route CLI auth through versioned exchange endpoint

* Use a semantic tenant exchange endpoint

* Use CodeRouter control header for hosted exchange

* Read hosted credentials through validated runtime env
2026-08-04 18:12:23 -07:00
haung921209 3a723ea7f1 Reject option tokens as --repo values and split the list header by count
- parseOption takes the token after --repo verbatim, so `--repo --all` would
  resolve a repository named "--all" and hand it to git as a path. Reject a
  value starting with --, pointing at ./-name for dash-prefixed paths.
- Replace the literal "comment(s)" header with cli.comments.list.header.one
  and .other selected by count, matching how cli.memory.output.processCount is
  written. Both keys cover all 20 locales.
2026-08-05 10:06:09 +09:00
lawrencecchen 13fa80e756 Activate sidebar resources on mouse down 2026-08-04 17:56:52 -07:00
haung921209 4b7e990b3e Localize the comments CLI strings, reject stray arguments, reuse the bridge formatter
- comments list now rejects every unrecognized remainder token, not only ones
  starting with --, so a stray positional cannot be silently ignored.
- The webview bridge's comments.list response builds one ISO8601DateFormatter
  and passes it to commentJSON, matching the socket response.
- Route the command's user-facing text through the catalog: nine new
  cli.comments.* keys for the errors and list output, each covering all 20
  locales with the reviewed ja value and the accepted English fallback.
2026-08-05 09:40:37 +09:00
lawrencecchen 1bebaa05cc test: require sidebar activation on mouse down 2026-08-04 17:39:13 -07:00
lawrencecchen a23c328f58 Order initial terminal output after topology 2026-08-04 17:28:41 -07:00
haung921209 86311bc8ab Cover every catalog locale for the two new keys
cli.comments.usage and socket.comments.missingRepoRoot now carry entries for
all 20 locales the catalog supports. The reviewed en and ja values are
unchanged; the remaining 18 locales use the accepted English fallback, which
is how 229 of the catalog's other fully-covered keys are written.
2026-08-05 09:25:27 +09:00
haung921209 14fdf2a458 Address review: locale entries, one formatter per reply, reject unknown flags
- Localizable.xcstrings: add the ja entry for cli.comments.usage and add the
  socket.comments.missingRepoRoot key, which the handler referenced but the
  catalog never defined. Both keys now match the en+ja coverage that the
  other cli.*.usage and socket.* keys use.
- DiffCommentsBridge: add commentJSON(_:formatter:) so a caller mapping many
  comments allocates one ISO8601DateFormatter per reply instead of one per
  comment; the existing single-comment signature delegates to it.
- cmux comments list: reject unrecognized -- options instead of ignoring
  them, so a typo cannot read as a supported request.
2026-08-05 09:17:55 +09:00
lawrencecchen e0d5a42302 test(tui): keep attach smoke ephemeral 2026-08-04 16:59:21 -07:00
haung921209 f8299cd77e Add cmux comments list backed by a comments.list socket method
Review comments saved in the diff viewer already reach agents through the
TextBox pending pool (push). This adds the pull direction: a read-only CLI
that asks the running app for a repository's saved comments, so external
tools never depend on the store's key derivation or in-memory cache.

- comments.list v2 method: canonicalizes repo_root via DiffCommentStore and
  delegates to a pure commentsListPayload(comments:repoRoot:includeConsumed:)
  so the reply shape is testable without a socket
- cmux comments list [--repo <path>] [--all] [--json]: resolves the git
  toplevel (default: cwd), prints a human summary or JSON
- CommentsListPayloadTests: default listing omits consumed comments,
  include_consumed adds them with an ISO8601 consumedAt, anchor fields are
  preserved, an empty store reports zero
- cli-contract.md: command table row + no-socket help probe
2026-08-05 08:51:21 +09:00
lawrencecchen db93a463e3 fix(tui): classify superseded surface attaches 2026-08-04 16:46:43 -07:00
lawrencecchen ae830c381b Test initial terminal output journaling 2026-08-04 16:44:34 -07:00
lawrencecchen ea1242e0c1 test(tui): reproduce mirror retirement attach race 2026-08-04 16:37:46 -07:00
lawrencecchen 9127dfe9e7 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-04 16:28:39 -07:00
lawrencecchen 06575bb203 fix(tui): retire stale surface attach races 2026-08-04 16:28:15 -07:00
lawrencecchen f58e55ecc0 test(tui): reproduce stale surface attach race 2026-08-04 16:13:24 -07:00
lawrencecchen 372d8afec0 Harden loaded TUI verification 2026-08-04 15:53:53 -07:00
lawrencecchen 28428710d4 Test fragmented terminal host delivery 2026-08-04 15:53:26 -07:00
Lawrence Chen 4cf7cc39a6 Add copy-paste CLI authorization page (#9597)
* Add copy-paste CLI authorization page

* Exclude device authorization from locale routing
2026-08-04 15:50:50 -07:00
lawrencecchen 7a79c03fcf Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-04 15:38:05 -07:00
lawrencecchen d02e6517a7 test(tui): make attach smoke lifecycle-aware 2026-08-04 15:30:44 -07:00
cmux reload-cloud ed841e0890 test: require automatic Hermes hook activation 2026-08-04 15:19:44 -07:00
austinpower1258 314224f4ed fix: route surface shortcuts to focused Dock 2026-08-04 15:17:48 -07:00
austinpower1258 c40b650276 test: cover Dock shortcut routing gaps 2026-08-04 15:17:33 -07:00
lawrencecchen c49c06944e fix(tui): bind auxiliary PTYs to daemon lifetime 2026-08-04 14:03:06 -07:00
lawrencecchen c910b38fcc Stabilize journal CI under load 2026-08-04 13:03:02 -07:00
lawrencecchen a1dbabd3cb test(tui): reproduce sidebar host leak on shutdown 2026-08-04 12:05:25 -07:00
Austin Wang e48438c4ef Merge pull request #9536 from manaflow-ai/issue-9462-google-sheets-browser-pane-cpu-hang
Use current Safari identity for Google Sheets
2026-08-04 12:03:37 -07:00
Austin Wang 91b195c1d8 Merge pull request #8612 from manaflow-ai/fix/atd-sidebar-link-click-8596
Fix AppKit sidebar description link clicks
2026-08-04 12:02:28 -07:00
lawrencecchen 9378f548d5 test(tui): isolate CLI fixtures from user config 2026-08-04 11:56:11 -07:00
lawrencecchen cd1c02a0c2 Stabilize cross-platform TUI verification 2026-08-04 11:13:55 -07:00
lawrencecchen 0066bb5a58 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-04 10:16:50 -07:00
lawrencecchen 6e6a292ebd fix(tui): publish multiview control contracts 2026-08-04 08:40:08 -07:00
lawrencecchen 2842bc11bd test(tui): reproduce boxed request schema omission 2026-08-04 08:15:19 -07:00
Austin Wang 2c26320ab8 Merge pull request #9580 from manaflow-ai/fix/pr-8614-merge-carrier
Land PR #8614 after resolving current main conflicts
2026-08-04 08:00:58 -07:00
lawrencecchen 0ccae477cf Optimize indexed journal subject catch-up 2026-08-04 07:53:18 -07:00
austinpower1258 c685309bba Merge active orphan fixture repair into PR #8614 carrier 2026-08-04 07:48:20 -07:00
austinpower1258 f155b74809 cmuxTests: model active orphan shortcut route 2026-08-04 07:48:04 -07:00
austinpower1258 d927a96d63 Merge orphan route pruning repair into PR #8614 carrier 2026-08-04 07:35:26 -07:00
austinpower1258 b99b5c2b28 cmuxTests: await orphan shortcut route pruning 2026-08-04 07:20:04 -07:00
lawrencecchen 50b7dc7ee5 fix(tui): restore projection viewport state 2026-08-04 07:01:10 -07:00
austinpower1258 00784263f0 Merge rect fixture compile repair into PR #8614 carrier 2026-08-04 06:42:23 -07:00
austinpower1258 470552b843 cmuxTests: keep FIFO diagnostic in one string literal 2026-08-04 06:42:16 -07:00
lawrencecchen cd66b9211a fix(tui): release retired view attachments 2026-08-04 06:35:11 -07:00
austinpower1258 b3b6c65c2f Merge rect publication fixture repair into PR #8614 carrier 2026-08-04 06:31:22 -07:00
austinpower1258 f5eae784e8 cmuxTests: route rect replies past setup commands 2026-08-04 06:31:12 -07:00
austinpower1258 3dfb475d52 Merge latest main into PR #8614 carrier 2026-08-04 06:02:31 -07:00
austinpower1258 2b5dc40667 Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 06:01:42 -07:00
Abdulaziz Albahar 28f88c819c Stagger Iroh relay credential refreshes (#9581)
* test: require staggered relay refresh slots

* fix: stagger relay credential refresh by endpoint role

* test: cover client relay refresh slot distribution

* test: assert stable bounded relay refresh slots
2026-08-04 07:57:13 -05:00
lawrencecchen db67e7b5ca test(tui): reproduce retired attach lease leak 2026-08-04 05:46:09 -07:00
austinpower1258 11cab6a727 Merge Sendable clock default into PR #8614 carrier 2026-08-04 05:45:56 -07:00
austinpower1258 1d309236c8 Use a Sendable focus-history clock default 2026-08-04 05:45:46 -07:00
austinpower1258 05e79d3252 Merge current main and final review fixes into PR #8614 carrier 2026-08-04 05:30:27 -07:00
austinpower1258 2e1427c499 Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 05:30:09 -07:00
Austin Wangandejc3 539a1b2ad5 cmuxTests: repair three red suites, one of which was killing its own test host (#9572)
* cmuxTests: derive the theme reload target from a dash-free socket suffix

The CLI derives a theme reload target from the socket file name, collapsing every run of
non-alphanumerics in the slug to a dot. #6452 made this fixture's socket path unique with a raw
UUID to stop two runs colliding in /tmp, which put the UUID's dashes into the derived identifier
as dots, so the expected literal could no longer match and the test waited out its five seconds.
The stdout assertion kept passing because the derived id still has the expected value as a
prefix, which is why this read as a timeout rather than a string mismatch.

Keeps the unique suffix hex-only so the expected identifier stays a plain template instead of a
call into the CLI's own helper, which would agree by construction.

* cmuxTests: drop two palette assertions for a gate that no longer exists

#8173 replaced the fork-probe reuse gate: `!cachedResultHadFallback` became
`cachedResultIsFresh`, and the fallback case is now re-verified against SharedLiveAgentIndex at
the call site instead of being refused outright. The parameter stayed in both signatures, so
these two assertions still compiled while asserting the opposite of what the product does, and
WorkspaceForkConversationContextMenuTests asserts the new contract in both directions a few
files away.

Removes the two assertions whose only purpose was the removed term, and renames the clear-side
test to say what it still covers.

* cmuxTests: stop the remote-connection suite killing its own test host

Three separate problems, in order of blast radius.

Two assertions indexed `operations` right after asserting its count. A count assertion does not
stop execution, so on failure the next line trapped with Index out of range and took the shared
test host down, and every remaining test in the shard never ran. Measured twice in one run.

Four @MainActor tests waited on a DispatchSemaphore. configureRemoteConnection enqueues its
session transition as a main-actor Task, so blocking the main actor stopped the very work being
waited on from ever being scheduled. They now use expectations, which pump the run loop.

Fifteen fixtures passed an unresolved %C control template. The broker deliberately refuses to
own a path it cannot resolve, so no lease was ever taken and cleanup could not run; six inverted
expectations were passing vacuously as a result. They now use the resolved form ssh -G produces,
and a new test pins the unowned-template policy so the fixtures cannot quietly regress to it.

Two more read activeRemoteSessionControllerID straight after configureRemoteConnection and now
await the transition instead.

* cmuxTests: point the daemon-upload tests at the transport that replaced scp

Two tests waited on an scp invocation that no longer happens. #8434 moved the daemon upload off scp
and onto the ssh exec channel, streaming the binary into `cat >`, and did not touch these tests.
Their stubs only fulfilled inside an `executable == "/usr/bin/scp"` branch, so the expectation
could never fire, the wait spent its whole budget, and the unwrap on the next line reported nil.

Both now capture the upload from the ssh branch. The property each one is about is unchanged: the
daemon still has to land on an absolute path under the remote HOME, that path just travels inside
the remote command instead of an scp destination, so the assertion moved with it.

The scp branch is kept and fails loudly. If the upload ever returns to scp, that should be a
sentence in the failure output rather than a silent timeout, which is precisely how these two broke.

The reinstall test also now records how many capability hellos preceded the upload and requires at
least one. Retargeting alone would have let it pass on a first install, which is not the
missing-pty-capability path it is named for.

Renamed the first test off "ScpDestination" since it no longer describes what is asserted.

* cmuxTests: fix three CLI tests that could not pass, and stop one hiding why

Three separate causes, all in the fixtures rather than the product.

Two socket-selection tests replied to the CLI with a bareword. SocketClient only treats OK, OK …,
PONG, ERROR: … or JSON as a complete single-line reply, so a bareword sends it into the multiline
drain pass, where reconfiguring the receive timeout on a socket whose peer already hung up fails with
EINVAL — and the CLI reports "Invalid argument" instead of the reply it already had buffered. The
replies are now OK-framed. These were the only two barewords in the suite, which is why eleven
near-identical siblings pass.

Both now also assert which responder received the request. That is the property they exist for —
the tagged socket is chosen and the stable one is not — and unlike the stdout comparison it cannot
be made vacuous by a future change to the reply.

A fork-diagnostics fixture passed agent "project-agent", which is not in the CLI's catalog, so the
command exited before emitting any JSON. The test has never passed; it went in already red alongside
the pi-family gate it is meant to cover. It now uses grok, a catalog agent that is neither pi-family
nor one of the transcript-walking agents, so the basename gate is still what is under test.

The shared helper turned all of that into a JSON decoding failure, because it only expected a zero
exit before parsing. It now requires the exit status and a completed run, so the next fixture mistake
reports the CLI's own error text instead of a parse error.

* cmuxTests: pair the pi-basename fixture with an agent that can actually fork

The pi-family basename test asked for fork_command_available, fork_supported and
fork_startup_input_available, but its fixture stored the record under a grok
launcher pair. A captured launch command is only used when its launcher describes
the requested agent, so the grok/omo pair was dropped as untrusted, no fork argv
was built for any agent, and all four assertions failed on
agent_has_no_fork_command without ever reaching the rule under test.

Store the record under opencode instead, whose wrapper launcher is omo. The
capture is now trusted, the fork argv resolves through the omo launcher, and the
executable basename stays /tmp/pi so the disagreement between the structured
identity and the basename is still what the test measures. The omo launcher also
answers fork support before the opencode executable probe, so the result does not
depend on a /tmp/pi existing on the machine running the test.

* cmuxTests: assert the stderr-closed CLI does not crash, instead of a CLI that no longer exists

This test asserted exit 1 and a "Usage:" banner on stdout. Neither has been true since #f48922aa94:
an unknown command exits 2 with a single line and no usage dump, and that line goes to stderr — which
the test closes with 2>&-. So it could not pass, and the crash it was written for was not what it
checked.

The regression is still worth guarding. cc4a6109d8 replaced FileHandle.standardError.write, which
raises and aborts when stderr is closed, with a raw Darwin.write that returns -1 on EBADF. The oracle
is therefore that the CLI exited on its own terms rather than dying from a signal, so ProcessRunResult
now carries terminationReason and both runners set it. Without that, a signalled process is
indistinguishable from an ordinary non-zero exit, because its terminationStatus is just the signal
number.

The command now runs under exec, so the process being waited on is the CLI rather than the shell. A
shell reports a signalled child as a normal exit with status 128+signal, which would have hidden
exactly the crash being tested.

It also pins CMUX_SOCKET_PATH and the home directory. Socket resolution otherwise consults a
machine-global marker file, and a spawn with a pristine temp home was measured reaching a real running
app — which would make the exit code depend on what is running on the machine. With the socket pinned
the unknown-command path is a single branch, so the test asserts exit 2 exactly rather than settling
for non-zero.

* cmuxTests: isolate the CLI regression suite from the machine's own cmux

A CLI spawned from this suite with a pristine temp home and a scrubbed
environment still reached a real running app. CFFIXED_USER_HOME moves the socket
directory but not socket discovery: the CLI also reads the machine-wide
/tmp/cmux-last-socket-path marker, and for an untagged debug build it scans /tmp
for cmux-debug-*.sock and connects to what it finds. Resolution runs before the
command dispatches, so even `claude-teams --help` did this. Every spawn site that
is not itself testing resolution now pins CMUX_SOCKET_PATH to a per-run path, the
three stable-variant tests write the marker inside their own temp home, and
runShell takes an explicit environment instead of handing the child everything
the test host was launched with.

Two tests bound a responder on /tmp/cmux.sock, the release app's socket path, and
UnixSocketResponder unlinks before it binds, so a run could take the control
socket away from a release app in use. The early returns meant to prevent that
raced the app, disagreed about whether a dangling symlink counts as present, and
turned the tests into silent passes. The symlink fallback case moves to the
user-scoped stable path inside its temp home. The legacy case keeps the part that
needs the real path, that /tmp/cmux.sock is classified as a stable implicit
default, and no longer creates, binds, or removes it. Three more guards tested
paths inside a freshly created temp home and could never fire, so they are gone.

stderr was pointed at the stdout pipe while about thirty tests parse stdout as
JSON or compare it to an exact reply, so one diagnostic line from the runtime
broke a content check instead of naming itself. stderr now has its own pipe,
failure messages carry both streams, and the negative checks that meant "the CLI
never said this anywhere" read both rather than silently narrowing to stdout.
Readers for both pipes start before the wait, because reading after
waitUntilExit deadlocks once a child fills a pipe buffer and that looks like a
hang inside the CLI. A launch failure is reported on stdout as well as stderr,
since five sibling suites share this runner and print only stdout.

Runs that assert nothing about latency no longer carry a 5s cap and take a 60s
guard instead, which still fails a stuck CLI rather than passing slowly. The two
browser-download tests keep their 3s and 16s caps, where the deadline is the
assertion. The two theme tests with fixed bundle identifiers now scope them per
run, since the reload notification goes out machine-wide; for the nightly one
that means scoping the socket file name too, because the identifier is derived
from it.

* cmuxTests: assert the exit code this fixture actually produces

The stderr-closed test asserted exit 2, the unknown-command code. Measured, it exits 1: the pinned
socket has no listener, so the CLI fails at connect and the top-level handler returns before the
unknown-command arm runs. That ordering makes the fixture a better exercise of what the test guards,
not a worse one, because the connect error is written to the stderr the test has closed. The run
confirmed the guard itself holds — termination reason was a normal exit, not a signal.

* cmuxTests: report stderr in sessions helper failures

* cmuxTests: preserve restore assertions after stream split

* cmuxTests: close review gaps in process and upload fixtures

* cmuxTests: align remote fixtures with streamed input and scoped identity

* cmuxTests: yield main actor while awaiting daemon upload

* cmuxTests: repair CLI regression fixtures and child lifetimes

* cmuxTests: isolate daemon bootstrap fixtures from ControlMaster

* cmuxTests: keep theme notification state nonisolated

* cmuxTests: detach live argv fixture from test host

* cmuxTests: own Go discovery in daemon reinstall fixture

* cmuxTests: make subprocess and bootstrap fixtures deterministic

* cmuxTests: remove detached fixture wall clock

* cmuxTests: use async-safe scoped locking

* cmuxTests: make off-host process work concurrent

* cmuxTests: keep blocking process wait off cooperative executor

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 05:29:17 -07:00
lawrencecchen 4f3be53952 fix(tui): make frontend journal ids cross-platform 2026-08-04 05:27:14 -07:00
austinpower1258 bb881787bd Merge focus-dismissal review fix into PR #8614 carrier 2026-08-04 05:22:01 -07:00
austinpower1258 dc64d16d3a tests: stop after focus-dismissal timeout 2026-08-04 05:21:48 -07:00
austinpower1258 ef49d76840 Merge warning-free focus-history clock into PR #8614 carrier 2026-08-04 05:18:05 -07:00
austinpower1258 e495092446 Mark focus-history clock closure Sendable 2026-08-04 05:17:49 -07:00
austinpower1258 aa5e7dc9f6 Merge final shortcut test repairs into PR #8614 carrier 2026-08-04 05:09:21 -07:00
lawrencecchen c2dc4ffb1f fix(tui): reconcile stacked multiview checks 2026-08-04 05:07:00 -07:00
austinpower1258 ef4c5de385 Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 04:54:23 -07:00
austinpower1258 96e515bad7 tests: align shortcut suite with current reload policy 2026-08-04 04:54:19 -07:00
lawrencecchen b1ed35008c Use managed SSH lifecycle for machine rail 2026-08-04 04:39:13 -07:00
lawrencecchen 64eb21554a fix(tui): specify multiview transport commands 2026-08-04 04:29:51 -07:00
lawrencecchen 83f024d3da fix(tui): isolate backend view projections 2026-08-04 04:27:54 -07:00
lawrencecchen 371151dfbc test(tui): reproduce multiview projection leaks 2026-08-04 04:22:00 -07:00
austinpower1258 c90a038699 Merge remote-tracking branch 'origin/main' into fix/atd-sidebar-link-click-8596 2026-08-04 04:20:28 -07:00
austinpower1258 af88c26294 Merge review fixes into PR #8614 carrier 2026-08-04 04:20:18 -07:00
austinpower1258 5e06c67e87 tests: prove managed shortcut writes are refused 2026-08-04 04:19:48 -07:00
Austin Wang a0680fd439 Merge pull request #9569 from manaflow-ai/cli-headless-fixes
cmuxTests: integrate headless CLI mock fixes against current main
2026-08-04 04:11:26 -07:00
lawrencecchen 0c1d45be75 fix(tui): retain host input under mutation bursts 2026-08-04 04:11:14 -07:00
lawrencecchen 29151156a9 fix(tui): supersede stale attachment resizes 2026-08-04 04:10:16 -07:00
lawrencecchen 266cfe77e1 Merge remote-tracking branch 'origin/feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/server.rs
#	cmux-tui/crates/cmux-tui-core/tests/pty.rs
#	cmux-tui/crates/cmux-tui/src/app.rs
#	cmux-tui/crates/cmux-tui/src/session/mod.rs
#	cmux-tui/crates/cmux-tui/src/session/tree.rs
2026-08-04 04:03:04 -07:00
lawrencecchen 9f01450d83 test: require managed SSH machine lifecycle 2026-08-04 03:58:45 -07:00
lawrencecchen 915608e83c test(tui): reproduce stale attachment resize failure 2026-08-04 03:58:23 -07:00
EJandejc3 2b6ea53707 cmuxTests: gate the keyDown-forward assertion on a live surface (#8504)
* cmuxTests: gate the keyDown-forward assertion on a live surface

testTypingRepairForwardsKeyDown asserted that the repaired keyDown reached
libghostty, but forwarding only happens once the runtime surface is live, and the
headless xctest host does not always spin one up. The assertion then fails for a
reason that has nothing to do with typing repair.

Gate the forward observation on a live surface, matching the constraint the
neighbouring tests in this file already respect.

This commit previously also carried a fix for remote-workspace restore handing
the terminal a local working directory. That fix is #8634, which restructures the
same logic behind a named predicate and gates both paths rather than one, so it
is dropped here to avoid two competing changes to the same function.

* cmuxTests: skip the keyDown-forward check visibly instead of silently dropping it

The forward assertion was wrapped in `if terminalPanel.surface.hasLiveSurface`, which means on a host
without a live libghostty surface the oracle simply disappears and the test still reports green. The
comment even said the headless host does not always provide one, so the coverage was not just
conditional, it was silently conditional, and nothing distinguished a run that checked the forwarding
from a run that did not.

XCTSkipUnless says it out loud. The repair-routing assertions above run first and are checked on every
host either way, so nothing that was verified before is verified less now.

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 03:57:33 -07:00
austinpower1258 1943a80b7a cmuxTests: align headless CLI lifecycle expectations 2026-08-04 03:50:33 -07:00
Austin Wangandejc3 99f7e1baf6 Recover one pane instead of restarting the session when its seed retention overflows (#9575)
* remote-tmux: cover a pane that retains past its own ceiling

Red on purpose. Two tests drive the two branches that can reach a pane's retention
ceiling and assert what should happen: the pane recovers and the transport keeps
running. Today both fail with the connection in `.reconnecting` and no pane marked
for a deferred reseed.

They need one change to be reachable at all. The per-pane comparison read the
hard-coded static while the seed tests inject a small mirror limit, so no fixture
could reach this branch and it has never had coverage. The comparison now goes
through `min(static, mirror budget)`, which is the same value at the shipped
default because the mirror-wide default is exactly twice the per-pane static.

That bound is also the honest one: without it a single pane may retain more than
the whole mirror is allowed, which is why one retaining pane always crosses the
per-pane line first and the mirror-wide check only becomes reachable with three
panes retaining at once.

* remote-tmux: recover one pane instead of restarting the session on seed overflow

A pane whose surface has not reached its remote size yet cannot accept a seed, so the
mirror retains it. When that retention crossed the pane's ceiling the mirror called
`beginReconnecting()`, which is the "this control stream is unusable" path. The stream
was fine; a renderer had run out of room.

What it cost: the state change wipes every pane's retained seed and every deferred
reseed, then the reattach reseeds all of them with `clearScrollback: true`. That emits
ESC[3J, so each pane loses its locally saved lines and gets back at most what
`capture-pane` returns. One slow pane truncated the scrollback of every other pane in
the session.

The remedy was already in this file, three lines below each of the three call sites,
and already used for the neighbouring condition: drop that pane's retained bytes, mark
it, and re-seed it from an authoritative `capture-pane` once its grid is ready. The
recapture is what makes dropping the bytes safe, and it touches one pane.

The condition is reachable in ordinary use — a large `cat` in a pane whose tab has not
been opened yet will do it on a fast link. It is close to unreachable on the
high-latency path that motivated #8436, so the harsh branch fired mainly in conditions
that PR was not about.

Of the eighteen `beginReconnecting()` calls in Sources, this was the only one outside
the connection itself and the only one that could fire while the stream was healthy.

* remote-tmux: a seed budget ceiling no longer restarts a healthy stream

Two sites in the connection's own seed accounting called `beginReconnecting()` under an
explicit `connectionState == .connected` guard, so a producer running out of room restarted a
stream that was working. The reattach then reseeds every pane with `clearScrollback`, emitting
ESC[3J, so one slow pane truncated every sibling pane's scrollback — the same blast radius the
mirror-side change removes, one layer down. Two independent design reviews ranked this the
worst remaining problem, and it makes the claim in this branch's description true rather than
nearly true.

`recoverPaneSeedBudget` discards that pane's retained bytes and re-seeds it authoritatively.
Freeing the bytes first is what makes room for the re-seed to be admitted, and the re-seed is
deferred to the next main-actor turn because this runs inside the reservation that just failed
— a synchronous call re-enters it and recurses until the stack overflows, which a fuzz run
measured.

The budget test follows: it pinned `.reconnecting` and an empty seed table, and now pins the
blast radius instead. The pane that crossed the budget is released, a pane that did not keeps
its seed, and only the offender's bytes return to the budget.

* remote-tmux: recover an overflowing pane seed once under total-budget backpressure

reservePendingPaneSeedBytes already recovers the pane (recording
pane-seed-total-backpressure) when the aggregate budget is exhausted, and
the caller's combined guard then recovered it a second time under
pane-seed-backpressure. Each recovery enqueues a clear-scrollback reseed,
so one overflow scheduled two. Split the guard so the per-pane ceiling
keeps its marker and a reserve failure returns without recovering again.

* remote-tmux: cover bounded pane seed recovery retries

* remote-tmux: bound deferred pane seed recovery

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 03:49:46 -07:00
9bdeca5d91 cmuxTests: settle async focus broadcasts before asserting their side effects (#8507)
* tests: settle focus broadcasts before asserting focus-history and notification dismissal

Five tests in the Workspace/TabManager suites called Workspace.focusPanel (or
addWorkspace) and immediately asserted the side effects that focus is supposed to
produce: a focus-history entry, and dismissal of the focused pane's unread
notification. Those side effects no longer happen synchronously.

Both of them run from the .ghosttyDidFocusSurface observer in TabManager, and that
notification is emitted through FocusSurfaceBroadcaster, which by contract never
delivers synchronously -- it coalesces onto a later main-queue turn so that emitting
mid-mutation cannot re-enter the focus/selection path. That indirection is what fixed
the unbounded focus cycle in issue #5100. Focus itself still lands synchronously,
which is why the surrounding focusedPanelId assertions kept passing and only the
side-effect assertions failed.

Drain the main queue before reading focus history or notification state, matching what
the passing tests in these same two files already do.

testFocusHistoryMenuSnapshotCarriesFocusedTimestamp needed one more correction: a
.back snapshot lists where focus would return to, so its first item is the focus record
stamped by TabManager()'s own initial workspace, not by the later addWorkspace call.
The lower bound of the causal interval now reads before TabManager() so it actually
brackets the record under assertion.

These suites are not in any CI -only-testing allowlist, so nothing caught the drift.

* tests: inject focus-history timestamps

* tests: settle active focus-history coverage

---------

Co-authored-by: ejc3 <[email protected]>
Co-authored-by: Austin Wang <[email protected]>
2026-08-04 03:46:57 -07:00
lawrencecchen 711c9392a7 fix(tui): type openpty window size per platform 2026-08-04 03:46:05 -07:00
austinpower1258 752f0f9eef Merge PR #8614 after resolving current main conflicts
Preserve the original PR head as ancestry while carrying the reviewed conflict resolution against current main.
2026-08-04 03:45:42 -07:00
austinpower1258 34ad688380 Merge remote-tracking branch 'origin/main' into cli-headless-fixes 2026-08-04 03:42:25 -07:00
austinpower1258 bfe921f9ec Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 03:41:33 -07:00
EJandejc3 0cc8445541 Open a browser at the end of the tab strip, not one slot short (#8705)
`openBrowser(insertAtEnd:)` passed a final position to `reorderTab`, which is
addressed in bonsplit insertion gaps. The end of the strip is `count`, not
`count - 1`, so the old value asked for the gap in front of the last tab and
left the new browser one slot short of the end.

It looked correct whenever exactly one tab followed the insertion point, since
the position and the gap agree there, which is why the existing two-tab test
did not catch it.

Before: TabManagerSurfaceCreationTests, 11 tests, 1 failure
After:  TabManagerSurfaceCreationTests, 11 tests, 0 failures

Co-authored-by: ejc3 <[email protected]>
2026-08-04 03:38:33 -07:00
lawrencecchen 188064ff34 fix(tui): make view size release idempotent 2026-08-04 03:33:27 -07:00
lawrencecchen 8c641eb814 fix(tui): sync journal SDK compatibility gates 2026-08-04 03:33:25 -07:00
lawrencecchen 417c27fb24 Document status copy menu route 2026-08-04 03:23:31 -07:00
austinpower1258 72aa0f0f73 Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs
# Conflicts:
#	cmuxTests/BrowserConfigTests.swift
#	cmuxTests/BrowserPanelTests.swift
#	cmuxTests/OmnibarAndToolsTests.swift
2026-08-04 03:19:26 -07:00
lawrencecchen 853707a627 test(tui): reproduce missing size lease release loop 2026-08-04 03:13:27 -07:00
Austin Wang 6d49edf927 Merge pull request #9576 from manaflow-ai/browser-headless-fixes
browser: integrate headless suite fixes against current main
2026-08-04 03:12:21 -07:00
austinpower1258 2d615e2b5b Merge remote-tracking branch 'origin/main' into fix/atd-sidebar-link-click-8596 2026-08-04 03:11:29 -07:00
austinpower1258 66798bf699 Avoid nested type for sidebar link layout cache 2026-08-04 03:07:16 -07:00
austinpower1258 7ac4a2520a Merge remote-tracking branch 'origin/main' into cli-headless-fixes 2026-08-04 03:06:30 -07:00
austinpower1258 4d3e14d90d Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 03:03:34 -07:00
austinpower1258 6639a2bd75 Merge remote-tracking branch 'origin/main' into cli-headless-fixes 2026-08-04 03:02:55 -07:00
Austin Wang c7b47c3e93 web: expose changelog versions to agent page variants (#9579) 2026-08-04 03:02:45 -07:00
austinpower1258 5dd6ea231f Revert "cmuxTests: avoid XCTest expectation host crashes"
This reverts commit b53005c56f.
2026-08-04 03:00:46 -07:00
austinpower1258 e7c4253c71 Retire ports when a terminal hibernates 2026-08-04 02:58:23 -07:00
8b8b1b0b87 Give the PR refresh run-loop test something real to observe (#8724)
* sidebar-git: give the PR refresh run-loop test something real to observe

testPullRequestRefreshRepositoryDiscoveryDoesNotBlockMainRunLoop counted calls to
a stubbed `git remote -v` subprocess as its proxy for "repository discovery ran".
The refresh stopped spawning that process in #2797, which replaced it with
in-process config parsing, so the counter sat at zero and the assertion failed.
The checks after it were worse than failing: with no discovery observed, they held
whether or not anything happened at all.

Repository discovery is the blocking filesystem work the refresh does before it
reaches the network, so that is what the test should watch. This adds
GitRepositoryDiscovering for the two calls PullRequestProbeService makes while
resolving candidate seeds, and lets a host inject it. GitMetadataService conforms
and stays the only implementation the app installs, so behavior is unchanged;
PullRequestPollService and the probe service accept the protocol instead of the
concrete type, which every existing call site already satisfies.

The test injects a discovery that counts and sleeps. It resolves no slugs, which
keeps the refresh off the GitHub transport and away from `gh auth token`.

The test now makes two claims rather than three. The invocation count is the one
that can fail for a product reason, and it is the one that was broken. The
run-loop tick gap stays as a coarse guard against a seconds-long stall.

The old "discovery did not run on the main thread" check is gone, along with the
observation box that fed it. `repositorySlugs` is a nonisolated async requirement,
so SE-0338 runs it off the caller's actor however the refresh schedules it: the
check passed no matter what the product did, including if discovery were rewritten
to be awaited inline. A test that cannot fail is not evidence, and keeping it
would have implied coverage the test does not have.

The run-loop tick gap stays as a coarse guard, and its comment now says why it is
loose: 45 seeds times 30ms of injected blocking is 1.35s against a 2.0s ceiling, so
this test's own work cannot trip it. It fires only if the product adds a
multi-second main-thread stall on top.

The counter is renamed to RepositoryDiscoveryInvocationCounter, since it counts
discovery calls rather than command-runner calls, and the new TabManager parameter
carries a note that it overrides discovery for the pull-request refresh only.

* chore: prepare PR 8724 origin transfer

* test: transfer deterministic PR refresh coverage

---------

Co-authored-by: ejc3 <[email protected]>
Co-authored-by: Austin Wang <[email protected]>
2026-08-04 02:53:53 -07:00
Abdulaziz Albahar f52578acb9 Preserve Iroh sessions across relay policy refresh (#9538)
* test: prove relay policy refresh mutates once

* fix: preserve sessions across relay policy refresh

* fix: clean up failed relay policy activation
2026-08-04 04:53:46 -05:00
austinpower1258 622654ae0b Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 02:52:30 -07:00
b959519136 cmuxTests: stop the shortcut routing suite from taking the test host down (#8635)
One test in this suite has been killing the xctest host, which is worse than a red suite:
the host dies with no verdict and every suite batched with it loses its results too.

The evidence names the test. scripts/ci/cmux-unit-test-timings.json was generated from a
green main run by scraping per-test completion lines, and it holds 247 entries for this
suite. testWelcomeWindowSidebarShortcutsUseSharedToggleCommands is the only declared test
absent from it. A test that neither passes nor fails nor skips is one the host died inside.

That test is also the only place in this 12,000-line file that calls performClose on a
window it constructed, and the only closed window here that leaves AppKit's close-time
release enabled; the other twenty disable it, and the product does the same for its own
windows. The test holds the window through ARC while the delegate's window context and the
focus-capture swizzle hold weak references to it, so the deferred close drops the last
retain a runloop turn later and the process aborts rather than failing a test.

Separately, the one test that constructs a second AppDelegate restored AppDelegate.shared
but not the surface registry's route retirer, which init had pointed at the temporary
delegate and which the registry holds weakly. That left the retirer nil for the remainder
of the host, so later tests ran against a registry that never sweeps retired routes.

A third latent host kill stays for its own change: a key-event helper calls fatalError
instead of failing, and converting it needs a throwing signature at fourteen call sites,
which does not belong in the same diff as the crash it would obscure.

Co-authored-by: ejc3 <[email protected]>
Co-authored-by: Austin Wang <[email protected]>
2026-08-04 02:47:40 -07:00
austinpower1258 fcc476c9ca Merge remote-tracking branch 'origin/main' into cli-headless-fixes 2026-08-04 02:47:04 -07:00
austinpower1258 a1726129e4 web: expose changelog versions to agent page variants 2026-08-04 02:46:26 -07:00
EJandejc3 e4bd9695d1 Show git status in the file explorer for repos reached through a symlink (#8577)
* file explorer: show git status for repos reached through a symlink

GitStatusProvider compared git's physical repo root (/private/var/...) against
the caller's explorer root spelled logically (/var, /tmp, or a symlinked project
dir) by raw string prefix, so every entry was dropped and the file explorer showed
no git status for any workspace behind a symlink. Resolve both roots to one spelling
for the containment check and emit keys under the caller's spelling so
FileExplorerStore lookups match. The ssh path keeps the caller's spelling on both
axes, so remote paths are never resolved against the local filesystem.

* file explorer: say when the root == "/" key branch is reached

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 02:44:50 -07:00
64f7726b4f tests: stop the portal first-reveal fixture from killing the test host (#8689)
BrowserPortalFirstRevealScrollTests declares 16 tests. Run alone it completed 10
of them and restarted the app host three times, so the suite had no verdict and
anything sharing its host lost one too.

makeWindowFixture builds an NSWindow and three tests close it. AppKit releases a
window on close unless the owner opts out, and ARC still holds a strong reference,
so each of those closes over-releases and takes the process down. The count lines
up: exactly three tests call close(), and there were exactly three restarts. The
one test that builds its own window already sets the flag, so this was an omission
in the shared fixture rather than a deliberate difference.

The product does this everywhere it owns a window (BrowserPanel, the prewarmed
pool, the popup controller, ReleasingWindowController); only this fixture missed it.

Before: 3 restarts, 10 of 16 tests ran, ** TEST FAILED **
After:  0 restarts, 16 of 16 tests ran, ** TEST SUCCEEDED **

Both arms ran on the same worktree and the same warm derived-data path, one suite
per app host, with only this change between them.

Co-authored-by: ejc3 <[email protected]>
Co-authored-by: Austin Wang <[email protected]>
2026-08-04 02:43:37 -07:00
lawrencecchen 5c20c8c5a7 Fix status copy menu activation 2026-08-04 02:43:14 -07:00
austinpower1258 3064ef4318 Revert "cmuxTests: retain markdown test windows across close"
This reverts commit 2ca500aa2f.
2026-08-04 02:42:06 -07:00
austinpower1258 97203addca Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 02:41:40 -07:00
EJandejc3 85fe23c44e CmuxAuthRuntime: wake the sign-in test waits on an event (#8644)
The HostBrowserSignInFlow harness waits spun on Task.yield() until their
condition held. Under CPU contention that is a bet on when the awaited task
gets scheduled, and the spinning loop competes with it for the same cores.
Running the package suites a few at a time was enough to lose the whole
target to

    HostBrowserSignInFlowTestSupport.swift:102: Fatal error: Timed out waiting
    for 1 host-browser session(s); got 0

since the timeout aborts the process and takes all 167 tests with it.

Raising the deadline does not fix that. With 48 busy loops on 16 cores, a
ten-second budget aborted the same way, only later. So each wait now suspends
until the fake it waits on resumes it: the session factory resumes session
waiters as it appends a session, the fake client resumes them as a currentUser
read parks on the closed user gate, and the gateable client resumes them once
an exchange has written its tokens or a clear has emptied the store. The
condition wait registers with the observation system instead, since the flow
and the coordinator are both @Observable. FlowFakeAuthClient's
storedAccessTokenDidPark() and ManualTestClock already worked this way.

The deadlines stay on as a net, so a genuine hang still reports by name rather
than suspending the run forever. They no longer bound a passing run.

Co-authored-by: ejc3 <[email protected]>
2026-08-04 02:41:00 -07:00
austinpower1258 516ac4b4cd Merge remote-tracking branch 'origin/main' into browser-headless-fixes 2026-08-04 02:39:39 -07:00
austinpower1258 2ca500aa2f cmuxTests: retain markdown test windows across close 2026-08-04 02:39:25 -07:00
austinpower1258 370a3ff944 cmuxTests: match mock socket thread QoS to waiters 2026-08-04 02:39:03 -07:00
austinpower1258 cb2506ab61 tests: fix omnibar overlay accumulator shadowing 2026-08-04 02:38:36 -07:00
austinpower1258 b554509c73 Add failing coverage for hibernated port retirement 2026-08-04 02:38:01 -07:00
lawrencecchen c92ac4282d test: activate status copy menu 2026-08-04 02:37:00 -07:00
27da2328b3 Default test-process windows to releasedWhenClosed = false (rebase of #7768) (#8832)
* Default test-process windows to releasedWhenClosed = false

AppKit defaults a code-created NSWindow to releasedWhenClosed == YES, so under ARC every close()
in test teardown sends an extra release. The window deallocates while still in the test's
autorelease pool, and the post-test pool drain then over-releases it: EXC_BAD_ACCESS in
objc_release, which kills the shared app host. xcodebuild relaunches the host and its summary
covers only the last launch, so verdicts pending in the dead host go missing rather than red.

A constructor in the test bundle swizzles NSWindow's two designated initializers so every window
created in the test process defaults to releasedWhenClosed == NO. A subclass's super.init reaches
the swizzled implementation, so NSPanel and every test-local subclass are covered without being
touched. Nothing in cmux sets releasedWhenClosed = YES deliberately, and production already sets
NO at its own call sites. The tradeoff is that AppKit-internal self-releasing windows leak in the
test process, which is harmless there.

Rebased onto current main from #7768; only the two source files are carried over, and the project
file entries are re-added against main's copy.

Co-authored-by: ejc3 <[email protected]>

* cmuxTests: also disable the window appearance animation in the guard

Greptile's review asked for this and trackTestWindow already does both: a window's appearance
animation is its own object and can outlive the window, committing CoreAnimation transactions off
the main thread for the rest of the run. Setting animationBehavior alongside releasedWhenClosed
means AppKit never creates the animation for a test-process window.

Nothing in the test targets asserts on animationBehavior, and the three production sites that
choose one deliberately assign after init returns, so an init-time default cannot override them.
Measured before pushing: the guard tests plus BrowserDeveloperToolsVisibilityPersistenceTests
produce the same verdicts with and without this change — same 11 pre-existing failures, nothing
added or removed, 0 restarts both ways.

* cmuxTests: cover guarded window animation defaults

* cmuxTests: type Swift Testing failure comment

---------

Co-authored-by: lawrencecchen <[email protected]>
Co-authored-by: ejc3 <[email protected]>
Co-authored-by: Austin Wang <[email protected]>
2026-08-04 02:35:48 -07:00
a77df5a35e cmuxTests: unbreak the build and re-sync the remote-tmux reorder/targeting suites (#8427)
* tests: drain all paneRects in programmaticMirrorReorder… (broken by #7315)

#7315 (exact feed-forward sizing / verified pane geometry) changed a mirror
window to publish only when its own paneRects reply lands, and those fetches are
enqueued incrementally — window @2's fetch appears after @1 resolves. The test
replied to a single snapshot of pending paneRects, so @2 never published, the
mirror built one tab instead of two, and the reorder + windowOrder assertions
failed (panelIds.count == 1, not 2).

The product is correct — the sibling mirror suites and the multiplex fuzzer build
multi-window mirrors green. This is a stale test setup: drain every paneRects
fetch (bounded loop) so both windows publish, then the two-tab reorder holds.

Red/green: on clean main the test fails with panelIds.count → 1 == 2; with the
drain it passes (1 test). Test-only change; no product code touched.

* tests: drain post-#7315 follow-up commands in RemoteTmuxWindowReorderTests

#7315 (verified pane geometry) and the pane-border-status work changed the
control-command stream the reorder/close state machine emits: a window-list
publish now also enqueues a per-window paneRects refetch, and closing a window
issues a border-status unsubscribe (a plain send(), kind .other). The suite
drives the connection with positional commandNumber:0 replies, so an undrained
follow-up sits at the FIFO head and swallows the reply meant for the reorder/
close list-windows recovery — the batch never recovers, the connection never
reconnects, and retained panes never release. All 33 assertions across 9 tests
failed on clean main for this one reason.

Fix is test-only: publish helpers drain every follow-up (paneRects + .other), a
drainLeadingOther helper clears them ahead of each correlated reply, and the
exact-pending assertions compare with those incidental follow-ups filtered out.
The product is correct — the multiplex fuzzer and the sibling mirror suites build
multi-window mirrors and reorder/close them green.

Red/green: clean main fails the suite with 33 issues; with this it passes 14/14.
No product code changed. Broke in #7315.

* tests: address review findings on the mirror/reorder test fixups

From the CodeRabbit/Greptile pass:

- drainLeadingOther replied to every paneRects with a hardcoded `%0`; a
  re-published @2/@3 needs its own pane id (the `windowId * 10` convention
  publishWindows stages), or its pending layout can't publish.
- reorderPending filtered incidentals globally, so a paneRects landing BETWEEN
  two list-windows (an ordering anomaly) would be elided and the equality
  assertion would still pass. Trim only TRAILING incidental follow-ups; an
  interleaved one now survives and fails the assertion.
- The mirror-targeting rects drain iterated a stale snapshot while each reply
  consumes the FIFO head, so an incidental preceding a fetch could mis-correlate
  pane data. Drain strictly from the head and stop at the first correlated command.

* tests: stop the reorder drains from swallowing correlated commands

Both drain helpers replied to whatever sat at the FIFO head, so a `listWindows` or
`windowReorder` arriving early was consumed with an empty reply and its later
positional result mis-correlated — the failure the drains exist to prevent. Each now
answers only the incidental follow-ups (`paneRects`, `.other`) and stops at the first
correlated command. `drainLeadingOther` also gains the bounded guard the other drains
already had.

---------

Co-authored-by: ejc3 <[email protected]>
Co-authored-by: Austin Wang <[email protected]>
2026-08-04 02:33:48 -07:00
EJandejc3 7081a23261 tests: update the remote-tmux resolver assertion to the shared builder's argv (#8550)
* tests: update the remote-tmux resolver assertion to the shared builder's argv

RemoteTmuxAuthTests/controlModeArgumentsUseRemoteTmuxResolverAfterDestinationGuard
fails on main. It asserts the remote command ends with

  'cmux-remote-tmux' '-CC' 'attach-session' '-t' 'work session'

but #8442 generalized the tmux-specific resolver into RemoteExecutableCommandBuilder,
which passes the executable name and not-found sentinel as arguments:

  'cmux-remote-executable' 'tmux' 'cmux-remote-tmux: tmux not found' '-CC' ...

The test's intent still holds — a destination that looks like an SSH flag is still
passed after `--` and the remote command still routes through the resolver — so only
the asserted literal was stale. Pin both halves instead: the command goes through the
resolver, and what it forwards is the tmux attach for this session.

* tests: pin the resolver's not-found sentinel in the control-mode argv

The resolver argv is 'cmux-remote-executable' <name> <sentinel> followed by the
forwarded arguments, so asserting the executable name and the tmux attach suffix
left the sentinel between them unpinned. Derive it from
RemoteTmuxHost.tmuxNotFoundSentinel so the assertion cannot drift from the
constant the resolver actually emits.

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 02:31:54 -07:00
austinpower1258 4636dd9353 Canonicalize TTY identities for port attribution 2026-08-04 02:30:16 -07:00
EJandejc3 07dd5a1cd9 A fake WKNavigation was killing the test host, hiding a whole suite (#8633)
* cmuxTests: stop a fake WKNavigation from killing the test host

BrowserDiscardRestorePolicyCancelTests logs that it started and then produces no verdict at
all, which is what a dead host looks like rather than a failing assertion. That is why the
suite reads as consistent with the product when you go through it test by test: it does not
fail, it dies, and it takes every suite sharing the host down with it.

The cause is constructing WKNavigation directly. WebKit builds the embedded C++
API::Navigation itself, so a bare WKNavigation() carries unconstructed storage. Allocating one
is harmless; releasing it is not. Reproduced outside the test bundle, deterministically:
EXC_BREAKPOINT inside CFRetain from -[WKNavigation dealloc] with WebKit initialised, and
SIGSEGV through WebCoreObjCScheduleDeallocateOnMainRunLoop from the same dealloc without it.
The first death needs no window: the fake is stored as the pending restore navigation, the
next call clears that reference, and the release traps mid-assertion before anything prints.

That also explains why no output survives. The probe reproduced the missing-log signature
too: with stdout on a pipe, the crash discards the buffer, so even the line printed just
before it never reaches the log.

The bookkeeping under test only ever compares these by identity, so the fakes are minted
through a helper that keeps them retained for the run. No assertion changes. WKNavigation()
appears nowhere else in the repo.

Whether the eight tests then pass is a separate question this crash has been hiding.

* cmuxTests: the same suite closes a test-owned window AppKit also releases

Retaining the fake navigations got this suite far enough to run and pass several tests where
it previously produced nothing, which confirmed the first cause and exposed a second one in
the same file. One test builds an NSWindow, holds it through ARC, and closes it in a defer
without disabling AppKit's close-time release, so the last retain goes away underneath the
live references and the host aborts instead of a test failing. That is the same defect already
proven in the shortcut routing suite, and about forty other closing sites in cmuxTests
already guard against it.

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 02:29:17 -07:00
EJandejc3 a76deb63e0 Close only the workspaces a tab manager actually owns (#8753)
closeWorkspace checks only that more than one tab is open, then runs its whole
teardown. It frees every panel's Ghostty surface, which SIGHUPs the child
processes, empties the workspace's panels and titles, clears owningTabManager,
and publishes a workspace-closed event. Membership in `tabs` is only enforced at
the very end, when the array element is removed; the recordHistory block does
look the index up earlier, but only to decide where in the history to record.

So handing a manager a workspace from another window kills that workspace's
terminals, strips its panels, and announces a close for a workspace that is still
open on screen.

#889 added this teardown and, directly above it, a `tabs.firstIndex(where:)`
guard, along with the test that covers this. A later "Reapply" merge kept the
teardown and dropped the guard, so the destructive half outlived its
precondition.

Two call sites already make this check themselves rather than relying on
closeWorkspace: AppDelegate re-checks `sourceManager.tabs.contains` before
closing a source workspace, and TerminalController records `existedBefore` and
skips candidates that fail it. Both predate #889, so they are not compensating
for the lost guard — they are evidence that callers have always needed this
precondition and have been paying for it individually.

One path does change. Workspace.swift resolves a manager as
`owningTabManager ?? tabManagerFor(tabId:) ?? AppDelegate.shared?.tabManager`,
and that last fallback is reached precisely when no manager owns the workspace.
Previously such a call tore the workspace down through an unrelated manager;
now it returns early, which is the intent of the guard.

testCloseWorkspaceIgnoresWorkspaceNotOwnedByManager covers this and has been
failing: it hands the manager a foreign workspace and checks that the workspace
keeps its panel, which is the terminal that would otherwise be killed.

Co-authored-by: ejc3 <[email protected]>
2026-08-04 02:26:45 -07:00
lawrencecchen db56ace820 Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-04 02:12:47 -07:00
austinpower1258 729b26d568 Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 02:10:30 -07:00
lawrencecchen 75fde6abbd Fix startup config drain and selectable status errors 2026-08-04 02:08:54 -07:00
lawrencecchen 637e151e2a fix(tui): update resource operation count 2026-08-04 02:06:36 -07:00
austinpower1258 5d25a5dd35 tests: harden browser headless regressions 2026-08-04 02:01:38 -07:00
cmux reload-cloud 8ef38547a1 Merge branch 'main' of https://github.com/manaflow-ai/cmux into HEAD 2026-08-04 02:00:53 -07:00
cmux reload-cloud c7374d1715 fix: use current Safari identity for Sheets 2026-08-04 02:00:26 -07:00
austinpower1258 2da63bafc8 Merge remote-tracking branch 'origin/main' into fix/atd-sidebar-link-click-8596 2026-08-04 01:58:08 -07:00
lawrencecchen d354cba9e6 fix(tui): register frontend journal command 2026-08-04 01:57:34 -07:00
cmux reload-cloud fe19c02230 test: require current Safari identity for Sheets 2026-08-04 01:57:22 -07:00
austinpower1258 54854e33a4 Add failing coverage for full-path TTY attribution 2026-08-04 01:57:19 -07:00
Lawrence Chen 3a8705467a Add per-version changelog pages (#9543)
* Add per-version changelog pages

* Localize changelog version pages

* Inject changelog storage

* Fix nested docs pager matching

* Fix Italian changelog labels

* Prerender changelog pages

* Handle unmatched docs pager paths
2026-08-04 01:56:59 -07:00
EJandejc3 d3eeacf0b3 CMUXProjectModel: find the worktree root instead of counting directories (#8641)
* CMUXProjectModel: find the worktree root instead of counting directories

Every project-loading test in XcodeProjectAdapterTests fails under `swift test`:

    Caught error: unreadable(file:///.../Packages/cmux.xcodeproj)

The suite locates cmux.xcodeproj by stepping up five parent directories from
its own #filePath. That was right when packages sat directly in Packages/, but
they now live under a group folder (Packages/macOS/CMUXProjectModel), so five
steps land on Packages/ and the adapter is handed a path that does not exist.
Seven of the fifteen tests in the package fail as a result.

Search upward for the directory that actually contains cmux.xcodeproj. Packages
are expected to move between the Shared, iOS and macOS group folders, so a fixed
depth breaks again on the next move while a search does not.

The search stops after eight directories rather than running to the filesystem
root. An unbounded walk out of a checkout that is nested inside another checkout
would find the outer checkout's cmux.xcodeproj and quietly test that project
instead. It also resolves symlinks on #filePath first, so a symlinked package
path still lands on a directory the walk can compare against.

When nothing is found the initializer throws and names what happened, instead of
handing the adapter a path it invented. Copying the package somewhere with no
cmux.xcodeproj above it now reports

    cmux.xcodeproj is not in /.../Tests/CMUXProjectModelTests or in any of the 7
    directories above it, so these tests have no project to load. Run them from a
    cmux checkout, or point CMUX_PROJECT_FIXTURE at a directory that contains
    cmux.xcodeproj.

and all nine tests in the suite fail. Two of them used to pass against the
missing path without loading anything: canLoad only inspects the path extension,
and the workspace test returns early when the file is absent.

* CMUXProjectModelTests: derive fixture siblings from the containing directory

CMUX_PROJECT_FIXTURE pointing at cmux.xcworkspace produced a projectURL nested inside
the workspace bundle, and a .xcodeproj override had the symmetric bug. When the override
names either bundle, its siblings now come from the directory that holds it.

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:56:14 -07:00
austinpower1258 d806cf59d4 Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 01:54:17 -07:00
austinpower1258 b53005c56f cmuxTests: avoid XCTest expectation host crashes 2026-08-04 01:47:34 -07:00
EJandejc3 8c9ee247a0 file-explorer: drop cancelled loads before they list a stale path (#8595)
loadChildren only checked cancellation after provider.listDirectory, so a
root reload triggered during an SSH provider swap left the cancelled local
load free to still call listDirectory, now through the freshly swapped SSH
transport, listing the old local path. Bail at the top of loadChildren when
the task is already cancelled, before any listing.

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:46:33 -07:00
austinpower1258 9d4bf9a990 Merge remote-tracking branch 'origin/main' into cli-headless-fixes 2026-08-04 01:40:38 -07:00
EJandejc3 0a68bbee35 remote-tmux: convert CLI tab-reorder final position to a bonsplit insertion gap (#8600)
controlSurfaceReorder passed the CLI's requested final tab position straight
to reorderSurface, but that API takes a bonsplit insertion gap. Moving a tab
to a higher slot needs index + 1 so the gap lands after the tab currently in
that slot; otherwise a move to sourceIndex + 1 is a silent no-op that still
reports success. Mirror the final-to-insertion conversion the other reorder
call sites already do.

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:40:04 -07:00
EJandejc3 91df18db5c tests: stop three fixtures from killing the test host on window close (#8701)
Three suites created an NSWindow and later closed it without opting out of
AppKit's close-time release, so each close over-released a window ARC still
held and took the whole test host down with it.

A dead host is worse than a failing test: the suite reports no verdict, and
every suite sharing that host loses its verdict too.

  TerminalNotificationSocketActionTests  2 restarts, 0 of 7 tests ran   -> 0 restarts, 7 pass
  FilePreviewPanelTextSavingTests        2 restarts, 2 of 27 tests ran  -> 0 restarts, 27 run
  FilePreviewReviewFeedbackTests         1 restart, 11 of 17 tests ran  -> 0 restarts, 17 run

FilePreviewPanelTextSavingTests closes a window in fourteen tests, all through
one private windowHosting helper, so the guard goes there rather than at each
call site. The other twenty-two suites in that file build windows and only ever
orderOut them, which does not release, so they need nothing.

Two of these suites still have assertion failures behind the crash that nobody
could see while the host was dying: three in FilePreviewPanelTextSavingTests
(24 of 27 pass) and one in FilePreviewReviewFeedbackTests (16 of 17 pass).
Those are separate bugs and get their own change.

The product already sets isReleasedWhenClosed = false everywhere it owns a
window; only these fixtures were missing it.

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:38:15 -07:00
austinpower1258 cd643fbcfb Merge remote-tracking branch 'origin/main' into browser-headless-fixes 2026-08-04 01:36:39 -07:00
austinpower1258 c7dcd52bcf Order test window for sidebar click dispatch 2026-08-04 01:36:22 -07:00
lawrencecchen 999ee84511 test(tui): reproduce burst input loss 2026-08-04 01:35:51 -07:00
austinpower1258 f6b382aaee Guarantee enough scans after late port kicks 2026-08-04 01:35:43 -07:00
EJandejc3 007fe3527a Pin four unread session-restore tests to the model the product implements (#8798)
* tests: pin the restore model these four unread tests were written against

Four session-restore tests in WorkspaceManualUnreadTests still describe the
pre-#2797-era restore model, where an unread notification present at snapshot time
was dropped and came back as a purely visual "restored unread indicator" with a
count of zero.

e4856922b0 changed that on purpose. Snapshots now carry the notifications
themselves, restore re-inserts them still unread, and the restored-unread
indicator is set only when a snapshot claims unread with no unread notification to
back it. Both gates are live: the workspace level checks
`snapshot.notifications?.contains { !$0.isRead }` before setting the indicator, and
the panel level does the same. Setting both would count one notification twice.

So these tests asserted an indicator that the product deliberately no longer sets,
and a count of zero for a notification the product deliberately keeps unread. They
now assert the restored notification directly and leave the indicator false, which
is the behavior the product implements. The independence the last two tests are
named for still holds: manual unread and a restored notification each contribute,
so the count is two until the manual half is cleared.

The assertions after markPanelRead and markRead are untouched, because marking read
clears the notification and the old expectations there were already correct. The
test names are unchanged; the CI shard timings key on them.

* cmuxTests: assert the combined unread count, not only its two flags

The independence test checked the manual indicator and the notification separately but
never the number they add up to, so a regression in either contribution could not move
a count this suite looks at. unreadCount(forTabId:) is the notification total plus one
for any workspace-level indicator, so this setup must read 2 before the panel is marked
read and 0 after.

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:35:21 -07:00
austinpower1258 fe88fd0ae1 Merge remote-tracking branch 'origin/main' into cli-headless-fixes 2026-08-04 01:34:19 -07:00
EJandejc3 71fb52ce63 remote-tmux: parse a session list that arrives with CRLF line endings (#8704)
A remote that runs tmux under a pty sends CRLF, because ONLCR rewrites every
newline on the way out. The session-list parser split on "\n" and then tried
to strip a trailing "\r", and neither step works: Swift treats CRLF as a
single Character, so the split finds no separator and `line.last == "\r"`
never matches, since the last Character of `...crlf\r\n` is `"\r\n"`.

The whole listing therefore parsed as one session whose name swallowed the
rest of the output, so such a host showed a single bogus workspace instead of
its sessions.

Split on any newline instead, which is what the sibling parser for the same
transport's stdout already does in RemoteTmuxVersion.swift. The strip and the
empty-line guard both go away, because split(whereSeparator:) omits empty
subsequences.

Before: RemoteTmuxSessionListParserTests, 8 tests, ** TEST FAILED **
After:  RemoteTmuxSessionListParserTests, 8 tests, ** TEST SUCCEEDED **

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:33:27 -07:00
austinpower1258 c4c171bec9 cmuxTests: adapt current SSH host test to shared loop 2026-08-04 01:31:46 -07:00
Austin Wang 7c715a4123 Exercise stale inherited surface after cached font lineage (#9571) 2026-08-04 01:31:28 -07:00
EJandejc3 2ed7358e92 cmuxTests: stop a blocking wait helper from shadowing the pumping ones (#8725)
Two test files define a file-private `waitForCondition` that polls by hopping
through DispatchQueue.main under XCTWaiter, so the main queue keeps draining while
a test waits. The test target also has a module-scope one that polls with
Thread.sleep and runs no run loop at all.

Swift prefers the overload that fills in fewer defaulted parameters, so
`waitForCondition(timeout: X) { ... }` binds to the module-scope blocking helper --
it only defaults pollInterval, where the file-private one would also default file
and line. A call with no timeout: argument cannot bind to it and gets the pumping
helper. So adding a timeout silently changed which helper ran, and blocked the main
thread for the whole budget.

That is fatal for anything waiting on main-actor work. Three call sites pass an
explicit timeout and all three are red on main:

- testRemoteSplitSkipsInitialGitMetadataProbe and
  testUnrelatedDefaultsChangeDoesNotRestartGitMetadataRefreshes wait for the
  initial sidebar git probe to drain. That probe is registered synchronously at
  schedule time and only clears once the ladder task, the snapshot, and its
  MainActor.run apply get main-actor turns, so a blocking wait denies the very work
  it waits for. Both fail on their first assertion, before reaching what they mean
  to check, with the run wedged long enough that the crash reporter logged an ANR.
  The product is correct in both cases.
- testFocusedPanelTitleRefreshesAutoWorkspaceTitleInSplitWorkspace waits for a
  panel title to propagate. The .ghosttyDidSetTitle observer is registered with
  queue: .main, so it runs as a queued main-queue operation rather than inline with
  the post, and the apply is deferred again by the panel title coalescer's default
  1/30s delay. A second of Thread.sleep starves both hops. Unlike the other two it
  fails at the behavior the test is named after, with earlier assertions passing.

Renaming the blocking helper to waitForConditionBlocking makes all three resolve to
their file-private helpers again, with the same budgets and poll intervals. Its
three existing callers wait on a socket accumulator filled off the main thread, so
they keep the blocking form and are unaffected. A future waitForCondition(timeout:)
in a file without a pumping helper now fails to compile instead of quietly
blocking.

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:31:02 -07:00
EJandejc3 af84599869 cmuxTests: expect composited terminal colours, and the menu flag the grouping test needs (#8509)
* tests: expect composited terminal background colors

GhosttyBackgroundThemeTests and PanelAppearanceBackgroundTests still expect
GhosttyBackgroundTheme.color to hand back the configured color with the
opacity in its alpha channel. That was true until #3166, which routed the
helper through WindowAppearanceSnapshot.compositedTerminalColor: the color is
now blended over the window background, so the opacity lands in the RGB
channels and the result is always opaque. The tests were never updated, so all
three background-theme tests and one panel test have been failing since.

Compositing is the behavior we want -- chromeColorScheme derives a luminance
from this color, which only means something once it is opaque -- so the
expectations move to the composited values.

The expected blend is computed in the test rather than by calling the app's
resolver. Asserting that the resolver equals itself would agree by
construction and could never catch a compositing regression; recomputing the
blend keeps the assertions honest. Restoring the pre-#3166 withAlphaComponent
behavior locally turns all nine tests red, which is what we want from them.

* tests: enable the agent-chat flag for the menu grouping test

renderedContextMenuGroupsCreateLayoutsAndManagementTail requires a New Agent
Chat item in the new-workspace menu, but #7705 put that item behind the
agent-chat-ui-enabled-release flag with a default of off, so the item is absent
and the test fails looking for it. The two changes landed within hours of each
other -- the menu reorganization (#7709) went in first and the flag PR did not
pick up its new test.

Default-off is the intent of #7705, and this test is about where the create
entries sit relative to the Layouts section, so turn the flag on for the body
using the helper the suite already has for exactly this.

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:27:42 -07:00
EJandejc3 445db7fe9d Skip an inherited terminal surface the registry no longer owns (#8656)
* Skip an inherited terminal surface the registry no longer owns

WorkspaceSplitWorkingDirectoryTests has two tests named for what they are meant to
prove — testNewTerminalSurfaceSkipsFreedInheritedSurfacePointer and its split-path
twin — and both currently prove the opposite by killing the test host:

    _os_unfair_lock_corruption_abort
    _os_unfair_lock_lock_slow
    ghostty_surface_inherited_config
    Workspace.inheritedTerminalConfig(preferredPanelId:inPane:)
    Workspace.newTerminalSurfaceLocal(...)  /  Workspace.newTerminalSplitLocal(...)

inheritedTerminalConfig guarded on `surface.surface != nil`, but a non-nil wrapper
pointer is not proof the native surface is alive. Teardown unregisters the runtime
surface and only then frees it, so a wrapper still holding the pointer after an
out-of-band free hands its caller freed memory. libghostty then locks an
os_unfair_lock inside that freed allocation, the kernel detects lock corruption, and
the process is SIGKILLed — so an unrelated suite sharing the test host dies too, and
its verdict is lost with it.

The registry owns exactly the pointers that have not been freed, which makes it the
liveness oracle a nil-check cannot be, and TerminalSurfaceRegistering already exposes
runtimeSurfaceOwnerId. TerminalSurface gains liveRuntimeSurface, which returns the
pointer only while the registry still owns it and otherwise clears the wrapper, so
every caller that reads through it sees nil instead of freed memory. Clearing goes
through the existing setter, so it advances runtimeSurfaceGeneration exactly as a
normal teardown does and pointer-backed caches invalidate for the same reason.

inheritedTerminalConfig now reads liveRuntimeSurface, so a stale candidate is skipped
like a torn-down one. That is also the behaviour the tests' second assertion asks for:
XCTAssertNil(sourcePanel.surface.surface, "Expected stale surface pointer to be
quarantined").

* Use liveSurfaceForGhosttyAccess for the inherited-config liveness check

Review pointed out that the new liveRuntimeSurface accessor checked only that
the registry owner was non-nil, so a recycled pointer owned by a different
surface would pass. liveSurfaceForGhosttyAccess already does the full check —
owner id equality plus an allocation-liveness probe — and quarantines a stale
wrapper the same way. Delete the weaker duplicate and call the existing
accessor from inheritedTerminalConfig; a stale candidate is still skipped in
favor of the font-lineage fallback.

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:27:39 -07:00
EJandejc3 197ede721e tests: build the file-preview text view the way the product does (#8719)
Seven test call sites constructed a bare SavingTextView(). The product never
does: makeFilePreviewTextView() builds an explicit TextKit 1 stack, because a
default NSTextView is TextKit 2 and its selection path pegged the main thread on
large documents (#4576, #5255). A bare init therefore has no configured text
container, and it also skips applyFilePreviewTextEditorInsets(), which the
factory applies.

So these tests exercised a view the app refuses to ship, and failed on it: the
save-shortcut tests read back an empty string instead of the saved text, and the
inset test read nil where it expected a value.

The files already disagreed with themselves. FilePreviewReviewFeedbackTests used
the bare init at line 44 and the factory at line 408, CanvasShortcutContextTests
uses the factory at three sites, and FilePreviewTextEditorTextKitTests exists to
assert the factory yields a pure TextKit 1 view.

  FilePreviewPanelTextSavingTests   27 tests, 3 failures -> 0 failures
  FilePreviewReviewFeedbackTests    17 tests, 1 failure  -> 0 failures

Both suites need the window-release guard in #8701 to reach these assertions at
all; without it they crash the test host first. Both arms above were measured
with that guard applied.

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:26:39 -07:00
EJandejc3 202908a278 Reject non-canonical (leading-zero) IPv4 spellings when classifying Tailscale peers (#8572)
* tailscale: reject non-canonical (leading-zero) IPv4 when classifying peers

parseIPv4 accepted "0100.64.1.2"-style leading-zero octets as decimal on
Darwin versions with a lenient inet_pton, so a host the dialer's inet_aton
reads as octal was classified as a Tailscale peer. Require the parse to
round-trip through inet_ntop to canonical dotted-decimal and refuse any
spelling that does not. IPv6 is unchanged.

* tailscale: reject non-canonical (leading-zero) IPv4 when classifying peers

parseIPv4 accepted "0100.64.1.2"-style leading-zero octets as decimal on
Darwin versions with a lenient inet_pton, so a host the dialer's inet_aton
reads as octal was classified as a Tailscale peer. Require the parse to
round-trip through inet_ntop to canonical dotted-decimal and refuse any
spelling that does not. IPv6 is unchanged.

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:25:12 -07:00
EJandejc3 523783ef6a command palette: index a branch short name as one searchable token (#8578)
branchTokensForSearch split a branch ref on the metadata delimiters (which
include "-"), so "feature/cmd-palette-indexing" tokenized to feature/cmd/palette/
indexing and the hyphenated short name never appeared in the search index; typing
the branch short name found nothing. Emit the part after the last "/" as a whole
token too, the way a directory basename already is.

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:22:37 -07:00
austinpower1258 9fcfb5f498 Add failing coverage for late-burst port retirement
A single kick near the end of an existing scan burst can receive fewer than the three authoritative misses required to retire a stale port. Pin that idle-workspace timing path before changing the scheduler.
2026-08-04 01:20:51 -07:00
austinpower1258 a3e99e705c Fix merged sidebar action test fixture 2026-08-04 01:18:44 -07:00
austinpower1258 2f48ef1063 Merge remote-tracking branch 'origin/main' into cli-headless-fixes 2026-08-04 01:16:36 -07:00
Ruixin Huang b46dcb71c1 fix: recover ssh-tmux sizing after peer detach (#9530)
* test: cover ssh-tmux peer detach sizing recovery

* fix: replay ssh-tmux sizes after peer detach

* test: cover malformed detach diagnostics

* fix: sanitize ssh-tmux detach handling
2026-08-04 01:16:07 -07:00
austinpower1258 c315b8cf3c Exercise sidebar link clicks through the window 2026-08-04 01:11:55 -07:00
austinpower1258 8eff531da8 cmuxTests: adapt current CLI socket tests to shared loop 2026-08-04 01:10:37 -07:00
austinpower1258 625712e757 Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 01:10:02 -07:00
austinpower1258 c2dbb5aba3 Reconcile sidebar link review feedback 2026-08-04 01:09:32 -07:00
EJandejc3 ee23ff9abf Fix four unsatisfiable tests in the sidebar git suites (#8723)
* sidebar-git: fix four unsatisfiable tests in the sidebar git suites

Two of them describe a git index that git cannot produce. The index trailer is the
SHA-1 of the index content, so rewriting only the trailing checksum while leaving
the entry table byte-identical is not a state a real repository reaches. The
product reads that shape deliberately: the index content signature covers the
entry count, path, mode and object id but not the trailer, so an index whose
content signature is unchanged is rebaselined as clean, which is exactly what
testCleanIndexSignatureRebaselinesWhenIndexRewriteKeepsTrackedContentClean pins.
The v4 and empty-index tests asserted the opposite for the same input, so one of
the two had to fail. They now stage a real change -- a new object id, and an added
entry -- whose stat still matches the worktree, so the dirty verdict comes from the
content signature the way it does for a real staged change.

The predicates are unchanged; the empty-index test's scenario and message move
from a staged delete to a staged add, because staging the first entry out of an
empty index is what actually moves the content signature.

writeGitIndexVersion4 gains the objectIDBytes parameter that
writeGitIndexVersion2EntryFromStat already had. It defaults to the zero id, so the
test call sites that do not stage a change need no edit; the convenience overload
threads it through. writeGitIndexVersion3SkipWorktreeEntry still
hard-codes a zero object id; it has no need to express a staged change.

testDisablingGitWatchClearsCachedPullRequestBadgesWhenPullRequestsAreShownByDefault
sent a scoped report_git_branch to a TabManager that only TerminalController knew
about. That path resolves its workspace through AppDelegate's main-window
contexts, so the report was dropped and the seeded badge survived. The test now
registers a windowless context like the other socket-routing tests, and asserts
the workspace is resolvable before sending, so a future wiring break reports there
instead of at the far assertion.

testSameDirectoryInitialGitMetadataProbesShareOneSnapshotRead waited with a helper
that blocks the main thread while pumping the main queue. That works in a
synchronous test, but this body is async: it runs as a main-actor job, inside a
main-queue drain that libdispatch will not re-enter, so the nested run loop ran
neither the helper's own poll hops nor the snapshot's MainActor.run apply. The
wait could only expire. It now awaits a suspending sibling helper with the same
timeout and interval.

The suspending helper propagates cancellation rather than swallowing it, so a
cancelled test unwinds instead of spinning the condition until its deadline.

* test: use a monotonic clock in the suspending wait helper

---------

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:07:53 -07:00
EJandejc3 70481686e9 CmuxNotifications: unbreak the test target so its 66 tests run (#8642)
`swift test` in Packages/macOS/CmuxNotifications does not build:

    NotificationDismissalModelTests.swift:47:5: error: missing return in
    instance method expected to return 'UUID?'

A test double's panelId(forSurfaceOrPanelId:in:) bumps a counter and then
leaves its lookup as a bare expression. Two statements means no implicit
return, so the whole target fails to compile and all 66 tests in its 5 suites
are silently skipped.

Return the lookup. Nothing else in the target changes.

Co-authored-by: ejc3 <[email protected]>
2026-08-04 01:05:09 -07:00
austinpower1258 64dc135ec3 Merge remote-tracking branch 'origin/main' into cli-headless-fixes 2026-08-04 01:04:44 -07:00
austinpower1258 4ff40fe83e Harden Hermes hook routing and process matching 2026-08-04 01:04:32 -07:00
austinpower1258 a70bd20b78 Merge remote-tracking branch 'origin/main' into fix/thirteen-test-suite-repairs 2026-08-04 00:56:25 -07:00
austinpower1258 9f88b6d71a Keep TCP port evidence complete across lsof warnings
Pass -w to the PID-scoped TCP listener query so unrelated filesystem mount warnings cannot poison every scan. Actual command failures, timeouts, malformed output, and PID-scoped uncertainty remain incomplete.

This is the green half for the persistent-warning regression in 6b54e60f76.
2026-08-04 00:55:13 -07:00
cmux reload-cloud 1b2763d4ce fix: separate Sheets transport and WebKit identities 2026-08-04 00:54:59 -07:00
austinpower1258 6b54e60f76 Add failing coverage for persistent lsof warnings
Issue #9152 reports a Time Machine filesystem warning on every lsof invocation. Model that warning in the full port lifecycle harness and prove PID-scoped TCP evidence must remain authoritative, while tightening the existing process-identity and retry test seams.

This is the tests-only red half of the regression pair.
2026-08-04 00:53:32 -07:00
lawrencecchen 6610c7a415 fix(tui): keep smoke terminals process-owned 2026-08-04 00:53:30 -07:00
Lawrence Chen 1cee402d7d Stabilize hosted tenant capability payload (#9556) 2026-08-04 00:51:00 -07:00
lawrencecchen 7cbc6301bd Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-04 00:50:23 -07:00
lawrencecchen ca5980883c fix(tui): establish the startup input route 2026-08-04 00:50:04 -07:00
cmux reload-cloud a224b9a5d5 test: require supported Sheets transport identity 2026-08-04 00:46:31 -07:00
cmux reload-cloud 6d8ba314a9 Merge branch 'main' of https://github.com/manaflow-ai/cmux into HEAD 2026-08-04 00:45:19 -07:00
austinpower1258 ec67c509f4 Merge remote-tracking branch 'origin/main' into fix/atd-sidebar-link-click-8596
# Conflicts:
#	Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowCellView.swift
#	cmuxTests/SidebarAppKitRowCellTests.swift
2026-08-04 00:44:43 -07:00
lawrencecchen 64d8bd9049 test(tui): reproduce empty startup input route 2026-08-04 00:40:20 -07:00
Abdulaziz AlbaharandClaude Fable 5 bcfc0b5028 Make the mobile browser stream self-healing (#9498)
* test: full unacked browser-stream window must recover, not deadlock

A full window whose acknowledgements never arrive (subscriber not yet
wired at start, connection route swap) currently parks the stream in
.flowControlled forever: the phone shows 'Waiting for Browser' with no
recovery path. Red on purpose; the fix lands in the next commit.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix: make the mobile browser stream self-healing

Three liveness fixes for the iOS browser mirror, all Mac-side:

Ack-stall recovery: a full unacked window now converts to a bounded
wait and, after 3s with no ack progress, is abandoned in favor of a
fresh capture. Frames are self-contained images, so recapture is a safe
retransmission. Previously lost in-flight frames (subscriber not wired
yet, connection route swap) deadlocked the stream and the phone sat on
'Waiting for Browser' forever.

Synchronized first capture: the first snapshot for a (re)hosted web
view now waits for WebKit's next committed render (bounded by a 1s
timeout with a 3-attempt fallback) instead of capturing the blank white
uncommitted buffer that used to be the phone's first frame. All other
captures get a 2s timeout so an occluded render host cannot wedge the
drive loop on a synchronized settle snapshot.

Idle reconciliation: an idle stream emits one lossless settle frame
after 10s of quiet. Page-driven dirty signals are lost whenever WebKit
suspends requestAnimationFrame for the occluded offscreen host, which
killed the injected beacon and froze the mirror on stale or blank
content; this bounds any such staleness to 10s.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix: scope capture commitment to the captured web view

A capture that raced a web view replacement must not mark the
replacement's synchronized first capture as done (its first frame would
be the blank bitmap again), nor consume its bounded retry budget.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-04 02:38:55 -05:00
austinpower1258 55b4dc3a37 test: harden Hermes integration fixtures 2026-08-04 00:38:28 -07:00
lawrencecchen d482b42907 test: cover startup pipe stall and status selection 2026-08-04 00:37:11 -07:00
lawrencecchen 8d7205bb3d test(tui): cover installed Claude hook alias 2026-08-04 00:35:44 -07:00
austinpower1258 efd73a3eac test: fix Hermes fixture type inference 2026-08-04 00:34:13 -07:00
austinpower1258 b32dfb7180 feat: make Hermes a durable first-class agent 2026-08-04 00:29:52 -07:00
Lawrence Chen 62f52ccff8 Fix Pi fork probes without slowing Pi hooks (#9549)
* test: cover Pi fork probe launch PATH

* fix: preserve Pi launch PATH for fork probes

* test: cover Pi extension hot-path work

* perf: keep Pi hook callbacks lightweight
2026-08-04 00:24:17 -07:00
austinpower1258 0792812120 test: cover Hermes hook approval migration 2026-08-04 00:23:43 -07:00
lawrencecchen 8b95103041 fix(tui): recognize Claude hook adapter alias 2026-08-04 00:21:54 -07:00
lawrencecchen 8faac39503 perf(tui): bound hook dispatcher writer holds 2026-08-04 00:10:18 -07:00
austinpower1258 5fca512ada test: cover Hermes durable restore and pinned hooks 2026-08-04 00:04:27 -07:00
lawrencecchen 57690dafed fix(tui): unify agent root aliases 2026-08-03 23:53:20 -07:00
cmux reload-cloud db30cc93eb Merge remote-tracking branch 'origin/main' into HEAD 2026-08-03 23:47:11 -07:00
Lawrence Chen 7dcb2bfb78 Restart the last terminal when quit is cancelled (#9492)
* Add regression test for cancelled last-terminal close

* Restart last terminal when close is cancelled

* Allow quit-cancel flow in debug UI tests

* Keep terminal recovery in the close transaction

* Always confirm quit after the last terminal exits

* Preserve terminal recovery with window docks

* Keep quit recovery inside the close request

* Document last-terminal quit confirmation invariant

* Clear close state only after cancellation

* Join terminal recovery to active quit decisions

* Test the last-terminal quit decision end to end

* Preserve notifications until quit commits
2026-08-03 23:45:54 -07:00
cmux reload-cloud cd90d0b9f6 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	cmuxTests/BrowserUserAgentPolicyWebKitTests.swift
2026-08-03 23:42:56 -07:00
lawrencecchen 2089a1e823 fix(tui): make hook helper help succeed 2026-08-03 23:42:40 -07:00
cmux reload-cloud c3cc0d1522 revert: remove #9483 browser policy changes
Restore the original navigation-action API and remove the regression test that encoded the same-URL stale-identity fallback. Keep PR #9482 nil/empty normalization as the sole convergence rule.
2026-08-03 23:40:34 -07:00
Lawrence Chen 622d2f7cb3 Move hosted Subrouter onboarding to Stack Auth (#9261)
* test: cover hosted Subrouter web flows

* feat: use Stack Auth for hosted Subrouter

* test: cover hosted auth fail-closed behavior

* test: preserve hosted account health

* test: keep hosted auth mock type-safe

* fix: harden hosted auth configuration

* test: keep CLI auth errors provider-neutral

* fix: use provider-neutral CLI auth errors

* test: publish canonical Subrouter hostname

* fix: publish sr.cmux.com to CLI clients

* test: keep CLI auth on issuing origin

* fix: complete CLI auth on issuing origin

* test: redact hosted account health details

* fix: redact hosted account health details

* test: isolate CLI config environment

* test: keep tenant credentials out of URLs

* fix: authorize hosted tenant requests by header

* test: require hosted tenant retirement on account deletion

* fix: retire hosted tenants before account deletion

* test: require trusted tenant retirement credential

* fix: authenticate hosted tenant retirement service

* test: configure hosted deletion credential

* test: preserve hosted rollout compatibility

* fix: preserve hosted rollout compatibility

* test: preserve shipped subrouter clients

* fix: preserve shipped subrouter clients

* test: protect subrouter credential responses

* fix: harden subrouter compatibility responses

* test: preserve hosted account metadata

* fix: preserve hosted account metadata

* test: keep hosted deletion retryable

* fix: keep hosted tenant cleanup retryable

* test: preserve hosted protocol failures

* fix: preserve hosted protocol semantics

* test: require legacy tenant cutover safety

* fix: migrate and retire legacy tenants safely

* test: bind hosted credentials to deployment config

* fix: bind hosted credentials to team config

* test: gate hosted tenant cutover errors

* fix: gate hosted tenant cutover safely

* fix: persist hosted cutover readiness

* test: close hosted cutover gaps

* fix: close hosted cutover gaps

* test: require resumable tenant finalization

* fix: make tenant finalization resumable

* test: cover large web test discovery

* fix: avoid web test discovery deadlock

* ci: pin current GhosttyKit artifact

* test: broker native hosted tenant exchange

* fix: broker scoped hosted tenant credentials

* style: remove trailing blank lines

* test: require secure exact hosted exchange

* fix: validate hosted exchange boundaries

* test: preserve dashboard recovery states

* fix: bound dashboard auth recovery

* test: preserve unconfigured service status

* fix: preserve unconfigured service response

* test: fail closed on hosted cleanup outages

* fix: fail closed on hosted cleanup uncertainty

* test: checkpoint hosted tenant deletion

* fix: checkpoint hosted tenant deletion

* test: bound account deletion token refresh

* fix: bound account deletion auth refresh

* test: keep hosted deletion retries visible

* fix: serialize visible deletion retries

* test: pin legacy migration source to target

* fix: bind legacy migration source to target

* test: cover account deletion without hosted Subrouter

* fix: gate hosted cleanup by deployment state

* test: keep hosted deletion checkpoint owned in flight

* fix: serialize hosted deletion checkpoint ownership

* test: checkpoint bounded legacy tenant retirement

* fix: bound legacy tenant retirement during deletion
2026-08-03 23:39:21 -07:00
Abdulaziz Albahar 72fce2fae8 Make Iroh transport diagnostics human-readable (#9485)
* test: require readable Iroh transport diagnostics

* fix: make Iroh transport diagnostics readable

* fix: support pinned Sentry breadcrumb API

* fix: satisfy diagnostic formatter ownership rules

* test: cover localized diagnostic reports

* fix: close diagnostic reporting merge blockers

* fix: honor explicit diagnostic locales

* test: gate compiled catalog locale checks

* fix: finalize diagnostic localization coverage

* test: require valid UTF-8 diagnostic reports
2026-08-04 01:38:50 -05:00
lawrencecchen fe86abc81a feat(tui): index nested agent topology 2026-08-03 23:38:10 -07:00
lawrencecchen a1157423ec style(tui): format degenerate split layout 2026-08-03 23:34:58 -07:00
lawrencecchen 11ab0fbc06 Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-03 23:31:43 -07:00
lawrencecchen 147b10c3ad fix(tui): make degenerate split layout total 2026-08-03 23:26:28 -07:00
lawrencecchen 749aacb8d3 test(tui): reproduce zero-size split crash 2026-08-03 23:15:45 -07:00
lawrencecchen 45800ecdf7 Add native menu scrolling and global sidebar access 2026-08-03 23:13:30 -07:00
lawrencecchen d132217415 fix(tui): preserve queued creation through terminal exit 2026-08-03 23:07:44 -07:00
Austin Wangandcmux reload-cloud a9c351be97 Fix Google Sheets browser identity replay loop (#9482)
* test: reproduce Google Sheets identity replay

* fix: make browser identity replay idempotent

* test: reject stale Sheets identity fallback

* revert: remove stale browser identity fallback

---------

Co-authored-by: cmux reload-cloud <[email protected]>
2026-08-03 22:28:58 -07:00
Abdulaziz Albahar d953ceda39 Match workspace group pin tint (#9519)
* Add regression test for group pin tint

* Match workspace group pin tint
2026-08-04 00:12:45 -05:00
cmux reload-cloud 6770f596be revert: remove stale browser identity fallback 2026-08-03 22:03:28 -07:00
cmux reload-cloud 1c906c1a2d test: reject stale Sheets identity fallback 2026-08-03 21:53:07 -07:00
cmux reload-cloud c5060a8f83 Merge branch 'main' of https://github.com/manaflow-ai/cmux into HEAD
# Conflicts:
#	Sources/Panels/WKWebView+BrowserUserAgentPolicy.swift
2026-08-03 21:42:23 -07:00
lawrencecchen 96c8c04f8a fix(tui): omit absent journal ingress fields 2026-08-03 21:38:34 -07:00
lawrencecchen 9644a6cf89 test(tui): reject null agent journal optionals 2026-08-03 21:29:00 -07:00
lawrencecchen 46d09c700f Test native menu scrolling and global sidebar access 2026-08-03 21:25:32 -07:00
lawrencecchen 2e3bbb12b9 feat(tui): normalize nested agent hook envelopes 2026-08-03 21:17:21 -07:00
Abdulaziz Albahar 6f99395b78 Focus Mac pairing QR flow on Tailscale (#9493)
* test: require Tailscale-only Mac pairing QR

* fix: focus Mac pairing on Tailscale QR

* test: require Tailscale pairing action names

* fix: name QR entrypoints for Tailscale

* test: require Tailscale setup guidance in scanner

* fix: explain Tailscale pairing prerequisites
2026-08-03 23:08:42 -05:00
Abdulaziz AlbaharandClaude Fable 5 87edd70966 iOS: remove redundant Switch Computer settings screen (#9490)
The workspace list's computer picker already switches Macs, pairing
lives in the Connection Method section and onboarding, and hiding a
computer lives in the Hidden Computers list. Settings > Switch Computer
duplicated all three, so drop MobileHostPickerView, its Settings entry,
and the 15 mobile.hostPicker.*/switchMac localization keys (en+ja) it
alone used. The Connection section now renders only when it has a live
connection row, so its header never sits empty.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-03 22:52:28 -05:00
Abdulaziz AlbaharandClaude Fable 5 ed44c7daf4 Add model selection lab to the New Task composer (#8800)
* Add model selection lab to the New Task composer

Adds a curated per-provider model catalog (Claude, Codex, OpenCode) with
opt-in model-flag injection into template commands, and five UX variants
for picking the model in the New Task sheet (combined agent menu, model
row, trailing chip, pill strip, context row), switchable at runtime from
the DEBUG-only CMUX Labs 'New Task Model Lab'. No model selected keeps
template commands byte-for-byte verbatim; release builds stay off.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Add Codex-style composer layout to the New Task sheet

New minimal layout (lab-switchable, DEBUG default): full-bleed prompt
canvas titled by the working directory, back chevron, and a bottom
control bar with a + options sheet (name, Mac, directory), agent pill,
model pill, and a circular submit button. Classic card layout stays
available via CMUX Labs and renders unchanged; release builds keep
classic. Adds GPT-5.6 Luna to the Codex model catalog.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address review findings on the model picker

- Dissolve the MobileTaskAgentModelCatalog static namespace into
  MobileTaskAgentProvider (detection init, models, model(id:),
  command(applying:to:)) per the no-static-namespace policy.
- Replace an existing --model/-m/--model= value in place instead of
  injecting a duplicate flag that the template's own value would
  override; stop scanning at the -- end-of-options token.
- Pin the task-composer accessibility preview to the classic layout and
  Off variant on fresh installs so the XCUITest suite keeps a stable
  element tree; CMUX_UITEST_TASK_COMPOSER_LAYOUT/_MODEL_VARIANT opt in.
- Give each lab variant exactly one placement in the composer layout:
  combined stays in the agent submenu, contextRow stays in Task Options,
  the rest collapse to the standalone bottom-bar pill.
- Gate the composer submit button on blocking completed-operation
  recovery, matching the classic layout.
- Make combined menu taps a single atomic template+model mutation.
- Share the model display-name fallback and accessibility triple; share
  the directory search/list fallback closures.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address round-2 review findings

- Quote-aware token scanning in command(applying:to:): flag text inside
  single/double-quoted arguments is one opaque token, so quoted mentions
  of --model are never rewritten; every real model flag before -- is
  replaced (not just the first); a flag directly before -- gets its
  value supplied in place.
- Gate selectedModel on the rendered picker variant so a draft-restored
  model cannot ride into snapshots or submissions while the picker is
  Off; the stored selection survives for when a variant is re-enabled.
- Show the selected model in the composer layout's combined variant
  (agent pill title gains ' · <model>') and add visible checkmarks to
  the combined submenu rows.
- Extend compact composer controls (+, submit, pills, chip, row, pill
  strip) to 44pt activation targets without changing their visuals.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address round-3 review findings

- Stop the model-flag scan at the first simple command's end: a newline
  separator or a token carrying an unquoted ;, |, or &. A compound
  template like 'claude "$CMUX_TASK_PROMPT"; formatter --model compact'
  now inserts Claude's flag after the first token and leaves the later
  command untouched.
- Route every submission through effectiveSubmissionSnapshot: while the
  picker variant is Off, a hidden model captured by the cached restored
  request (or an adopted recovery request) is stripped and the command
  recomposed with the same operation identifier, closing the untouched-
  draft bypass of the selectedModel gate.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address round-4 review findings

- Build the composer options sheet lazily: MinimalLayout now takes a
  deferred builder, so directory-candidate construction (a workspace
  walk) runs only when Task Options is presented, not on every prompt
  keystroke's body rebuild.
- Reconcile a hidden model at the submission boundary by marking the
  request dirty instead of post-hoc snapshot surgery: resolution runs
  through makeSubmissionSnapshot (whose selectedModel gate strips the
  model) and MobileTaskSubmissionIdentity mints a fresh operation ID for
  the changed bytes, keeping retries idempotent.
- Process a model flag attached to a command separator: --model=old;,
  --model old;, and --model; are rewritten before scanning stops, so a
  stale value can no longer override the selection.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address round-5 review findings

- Reconcile a hidden model at BOTH request-resolution boundaries via one
  shared resolver: when the Off picker hides a model a clean cached
  request still carries, resolution is forced through the selectedModel
  gate and the identity mints a fresh operation ID, so a persisted draft
  can never pair model-less bytes with an ID previously bound to
  model-bearing bytes. Replaces the submit-only proxy check.
- Treat redirection operators as part of the simple command: & adjacent
  to > (2>&1, >&2, &>file) and | preceded by > (>|file) no longer end
  the flag scan, so a stale --model after a redirection is still
  replaced. Control operators (;, |, &, &&, ||) still end it.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Address final review round: comments and delisted-model drafts

- Stop the model-flag scan at an unquoted word-initial #: a commented
  flag is never rewritten, and the selection is inserted after the first
  token instead of being silently swallowed by a comment edit.
- Do not reuse a draft's operation ID (or restore its completed-
  operation recovery) when the draft's model no longer survives
  curated-list validation; the resulting default-model command gets a
  fresh idempotency key.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Use the adjustments glyph for the composer options button

Dogfood feedback: + implied adding something; the button configures the
task (name, Mac, directory), so it now shows slider.horizontal.3.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Stop pill labels clipping when the selection gets longer

Dogfood feedback: switching the agent or model to a longer title left
the pill label clipped for the length of the resize animation. The pill
content now uses fixedSize so the capsule adopts the new intrinsic
width immediately, and the label subtree is identity-keyed on the title
so it swaps instead of animating through stale-width frames.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Add mobile task attachments

* Always show the model pill in the composer layout; fix iOS 26 pill clipping

Dogfood feedback: the combined lab variant hid the standalone model
pill (models lived only in the agent submenu), which read as the model
picker disappearing. The composer layout now has one canonical model
treatment: a dedicated pill beside the agent pill for every non-Off
variant; the agent menu is forced plain and the options sheet never
repeats the contextRow, so the pill stays the single entry point.

The label clipping on longer titles survived the fixedSize fix because
the identity key sat on the label content while the UIKit menu button
still animated its frame. The .id now keys the whole Menu, so a title
change swaps the button instead of animating through stale bounds.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Fade the composer pill scroller into the bar at both edges

Dogfood request: pills should dissolve toward the neighboring options
and submit buttons instead of clipping at the scroller bounds. iOS 26
uses the native soft scroll edge effect (progressive blur + fade);
earlier systems approximate it with a 14pt alpha-mask fade per edge.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Discover task models from connected Macs

* Adopt MacPairingKey lookups in task capability checks

Main's typed MacPairingKey re-key changed the secondary-subscription
registry key from a device-id string to the full pairing key. The
attachment and model-discovery capability checks now resolve through a
shared controlSubscriptionMatching helper that keeps the old semantics:
exact pairing when a tag is given, any same-device pairing otherwise.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Scroll the composer pills under the bar buttons with a real edge effect

Dogfood feedback: the scroll edge effect never rendered because the
buttons sat NEXT TO the scroller, so no content ever passed beneath an
edge. The pill scroller now spans the bar with the attachment/options
buttons and the submit button living in its leading/trailing safe-area
insets: pills genuinely scroll under them, which is what activates the
native iOS 26 soft scroll edge effect (progressive blur + fade). Pre-26
keeps an opaque button background as the fallback occlusion.

Also stop the prompt editor from yanking long text back down while
scrolling up: interactive keyboard dismissal resized the editor every
drag frame and UITextView re-scrolled to the caret each time; dismissal
is now immediate.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Render the pill scroller edge effect through UIKit's container interaction

SwiftUI's scrollEdgeEffectStyle only styles effects the system already
owns (bars/glass), so pills merely underlapped the buttons. The bar row
is now a thin UIKit host: a horizontal UIScrollView spans the bar, the
button clusters float above it, and on iOS 26 each cluster carries a
UIScrollEdgeElementContainerInteraction bound to the scroll view's
edge, which renders the real progressive blur+fade beneath the buttons
as pills pass under. Pre-26 clusters keep an opaque background.
Content insets track the cluster widths (the attachment button is
capability-gated), resting the pills between the clusters.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Attach the scroll edge effect via probes instead of rehosting the bar

The UIKit-hosted bar livelocked SwiftUI (hosting-controller sizing
feedback re-rendered every frame) and blanked the composer, which also
made the pills unscrollable. The pills return to the proven SwiftUI
ScrollView under safe-area-inset button clusters; a zero-size probe in
the scroll content walks to the backing UIScrollView and a coordinator
binds UIScrollEdgeElementContainerInteraction to transparent container
views behind each cluster. Fail-soft: if the probe finds no scroll view
or the OS predates iOS 26, nothing attaches and the bar just underlaps.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Reproduce the scroll edge effect deterministically in SwiftUI

Two native attempts failed structurally: SwiftUI's scrollEdgeEffectStyle
never renders for floating siblings, and hosting the bar (or just the
clusters) in UIKit for UIScrollEdgeElementContainerInteraction either
livelocked the view graph or dropped cluster content, because the
effect's shape must come from the container's descendants. The bar now
stays pure SwiftUI: clusters carry an ultraThinMaterial background
(full blur under the buttons) and a 24pt gradient-masked material band
beside each cluster fades passing pills into the bar background --
the same progressive blur+fade the system effect draws, with no
UIKit bridging left to break scrolling or layout.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Use native scroll edge effects in task composer

* Fix task composer scroll edge blur

* Use native shaped scroll edge effects

* Test composer pill scroller hard edges

* Fix composer hard-edge UI test lookup

* Restore hard edges to composer pill scroller

* Exercise overflowing composer pills in hard-edge test

* Test composer prompt scroll gesture ownership

* Prioritize prompt scrolling over sheet drag

* Test composer prompt scroll position stability

* Keep composer prompt at manual scroll position

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-03 22:25:17 -05:00
lawrencecchen 674a653392 Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-03 20:22:01 -07:00
Tanghui Lin f4cd2774de Fix infinite UA-policy restart loop on Google Sheets destinations (#9483)
Fixes #9462
2026-08-03 20:05:09 -07:00
Abdulaziz Albahar 840f8c074f Fetch complete Iroh discovery before Mac host activation (#9478)
* test(iroh): cover incomplete host registration discovery

* fix(iroh): fetch complete discovery for host activation
2026-08-03 21:52:13 -05:00
Abdulaziz Albahar 145b60e893 Fix iOS keyboard focus ownership after photo picker (#9371)
* test: cover mobile input session ownership

* fix: centralize mobile terminal input ownership

* chore: add mobile dock verification geometry

* test: cover keyboard ownership review edges

* fix: close mobile input ownership review gaps

* Test foreground recovery teardown handoff

* Keep disconnected recovery foreground-only

* Respect the active foreground recovery owner

* Test clientless foreground aggregation

* Require a live client for aggregation

* Use UIKit keyboard guide for terminal dock
2026-08-03 19:51:55 -07:00
Austin Wang 37a4d212ab Translate workspace group anchor guidance across locales (#9480)
* Test workspace group docs locale overrides

* Translate workspace group anchor guidance

* Test localized workspace group action labels

* Translate workspace group action labels

* Test workspace group docs match native labels

* Match Khmer docs to workspace group menu

* Use static imports in localization test
2026-08-03 19:49:44 -07:00
Austin Wang 7693b19065 Move mobile telemetry consent into CMUXMobileCore (#9505)
* Test consent provider in CMUXMobileCore

* Move mobile telemetry consent into core
2026-08-03 19:46:44 -07:00
lawrencecchen dc9fc8a9c0 feat(tui): ingest native agent hooks losslessly 2026-08-03 19:41:12 -07:00
lawrencecchen e98fc16b82 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-03 19:18:34 -07:00
lawrencecchen a1874d37a9 fix(tui): make terminal projections client-local and receipt ordered 2026-08-03 19:17:22 -07:00
Austin Wang c37ea7a31f Scope shortcut settings notifications to their config file (#9502)
* Test host shortcut notification source identity

* Scope host shortcut notifications to config file
2026-08-03 18:59:30 -07:00
Austin Wang 972084ddb3 Serve cached social preview images without redirects (#9503)
* Add social image delivery regression test

* Serve cached social image URLs directly
2026-08-03 18:58:02 -07:00
Austin Wang 9decec5213 Stop blank Ghostty opener stderr log bursts (#9486)
* test: reproduce Ghostty opener stderr log burst

* Fix empty Ghostty opener stderr log bursts
2026-08-03 18:31:32 -07:00
Austin WangandClaude Opus 5 20390187fd Skip directories when resolving provider executables on PATH (#9476)
* Add regression tests for PATH directory shadowing of provider binaries

FileManager.isExecutableFile(atPath:) returns true for directories on macOS,
so a directory named like a provider binary earlier on PATH is selected by the
CLI and app PATH walks. These tests fail until the resolvers reject directories.

Refs #8743

Co-Authored-By: Claude Opus 5 <[email protected]>

* Skip directories when resolving provider executables on PATH

FileManager.isExecutableFile(atPath:) returns true for directories on macOS, so
a directory named like a provider binary (~/bin/omx/, ~/bin/claude/) earlier on
PATH was selected as the executable and the launch failed at execv with a
confusing "Permission denied". Reject directories with
fileExists(atPath:isDirectory:) before the executable check in all three PATH
walks, mirroring the guard resolveClaudeExecutable already applied to configured
candidates.

Fixes #8743

Co-Authored-By: Claude Opus 5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-03 18:28:39 -07:00
lawrencecchen 4e891b94b3 perf(tui): make hook completion wakeups lossless 2026-08-03 18:27:38 -07:00
Austin Wang 9d7cc488b8 Reject unknown flags for surface resume set (#9477)
* Add regression test for resume set flag validation

* Reject unknown surface resume set flags

* Fix surface resume flag regression test
2026-08-03 18:26:56 -07:00
lawrencecchen c15888ffa8 test(tui): require durable hook worker wakeups 2026-08-03 18:25:40 -07:00
lawrencecchen b1dc7b81a5 perf(tui): bound and batch journal hook delivery 2026-08-03 17:41:30 -07:00
Lawrence Chen 97f4a5d6a3 Add Pi landing page and agent SEO (#9455)
* Add Pi landing page and agent SEO

* Keep homepage copy localized

* Limit Pi discovery to English

* Expand coding agent SEO coverage

* Localize coding agent landing pages

* Localize Pi guide card

* Remove stale English-only Pi link copy

* Keep agent metadata locale-native
2026-08-03 17:41:15 -07:00
Abdulaziz Albahar 490b45503b Require connected iOS dogfood launches (#9252)
* test: cover disconnected iOS dogfood launch

* fix: require connected iOS dogfood launches

* fix: make mobile readiness event driven

* fix: harden mobile readiness lifecycle

* perf: buffer deadline event reads

* test: remove source-shape admission assertion

* test: cover cached host binding publication

* fix: publish cached mobile host binding

* test: cover usable mobile session readiness

* fix: require usable mobile connection readiness

* test: cover injected attach admission ownership

* fix: start injected attach at connection owner

* test: require active Iroh route publication

* fix: publish Iroh route only after activation

* fix: compile weak dictation request capture

* test: align route readiness fixtures with connectivity v2

* chore: expose safe Iroh activation failure type

* test: reject mobile session closed during revalidation

* fix: require stable mobile admission before handoff

* test: reject foreground and control dial overlap

* fix: reserve foreground mobile connection routes

* test: reproduce registry churn disconnect

* test: preserve policy during registry churn

* fix: preserve mobile connectivity during registry churn

* test: preserve active iroh session during candidate admission

* fix: promote mobile sessions only after readiness

* test: reproduce saturated mobile reconnect

* fix: reserve reconnect admission until session readiness

* fix: preserve strict single-session capacity

* test: reproduce relay refresh disconnect

* fix: preserve authorized sessions across route refresh

* test: reproduce paginated host registration wedge

* fix: recover host registration across discovery pages

* test: reproduce orphaned iPhone build process

* fix: terminate iOS app before bundle replacement

* fix: preserve usable-session promotion after main merge

* fix: preserve secondary Mac route owner after merge

* fix(ios): let Ghostty render the cursor

* Pin current GhosttyKit checksum
2026-08-03 19:32:33 -05:00
Lawrence Chen cec7ac3fa7 Suppress Pi notifications after interrupted turns (#9451)
* test: cover silent Pi turn interruption

* fix: suppress Pi notifications after interruption

* perf: keep Pi completion inspection single-pass
2026-08-03 17:22:53 -07:00
lawrencecchen 18dbd62c00 test(tui): reproduce semantic destination races 2026-08-03 17:14:09 -07:00
lawrencecchen 4c2deae7f1 test(tui): require bounded journal hook workers 2026-08-03 17:04:23 -07:00
lawrencecchen 16ee5ed7ee Align recoverable workspace test with explicit focus 2026-08-03 16:54:31 -07:00
lawrencecchen 83a893a6bb Fix sidebar regression test imports 2026-08-03 16:52:04 -07:00
lawrencecchen 35916bf668 Keep modified navigation directional in file view 2026-08-03 16:44:09 -07:00
austinpower1258 f170ffef77 fix: make browser identity replay idempotent 2026-08-03 16:42:31 -07:00
austinpower1258 c56ee7d57f test: reproduce Google Sheets identity replay 2026-08-03 16:42:31 -07:00
Austin Wang d35187ec47 Pin GhosttyKit checksum for iOS startup fix (#9487) 2026-08-03 16:39:35 -07:00
lawrencecchen 2190da2f3b Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-03 16:38:29 -07:00
lawrencecchen c8b1485a5a Keep sidebar resource clicks terminal-focused 2026-08-03 16:38:22 -07:00
Abdulaziz Albahar 2141de5722 Fix middle tab drag reordering
Fix same-pane tab reordering to middle indices by taking the Bonsplit SwiftUI delegate path as the sole reorder owner. Includes hosted E2E coverage for later-to-middle and earlier-to-middle tab drags.
2026-08-03 18:35:45 -05:00
lawrencecchen 2f1cbb5e5f Test modifier navigation through sidebar trees 2026-08-03 16:34:16 -07:00
lawrencecchen afcb8821f5 Test explicit sidebar focus entrypoints 2026-08-03 16:32:33 -07:00
lawrencecchen a122f1f2d0 Compose native actions into sidebar views 2026-08-03 16:27:42 -07:00
Abdulaziz AlbaharandClaude Fable 5 4adc8e519a iOS: diff viewer scroll momentum survives finger lift (#9257)
* ios: keep diff scroll momentum by persisting the row only at scroll idle

FileDiffPageView propagated every scrollPosition row change up into the
pager's @State while the finger was still down or the view was
decelerating. Each write re-rendered the pager mid-fling and the bound
scrollPosition(id.top) re-anchored the tracked row on the next
layout pass, cancelling the remaining momentum: lifting the finger
stopped the diff dead.

Route persistence through SettledScrollRowReporter, which reports the
tracked row only when the scroll phase returns to idle (plus once on
page unmount), so nothing re-renders during a fling. Restore-on-remount
behavior is unchanged.

Co-Authored-By: Claude Fable 5 <[email protected]>

* ios: add a many-screen diff to the changes preview fixture

Every hand-written fixture diff fits on one screen, so scroll flings
and deceleration could not be exercised deterministically. Add a
400-line generated diff (Sources/RenderPipeline.swift) to the DEBUG
changes preview fixture.

Co-Authored-By: Claude Fable 5 <[email protected]>

* ios: drop the anchor from the diff page's live scrollPosition binding

On-sim verification showed flings still died with only the idle-phase
reporter fix: scrollPosition(id.top) itself re-aligns the
tracked row flush to the viewport top on every internal position
update during deceleration, so the fling stops at the first row
crossing (the settled frame shows the row pixel-flush at the top).
Removing the anchor removes the alignment contract; tracking and
restore-on-remount keep working via the id binding.

Co-Authored-By: Claude Fable 5 <[email protected]>

* ios: give the diff scroll offset a single owner

Real-path dogfood showed flings, rubber-banding, and pull-to-refresh
displacement all being cut short on real diffs even with the anchor
removed: any live scrollPosition(id:) binding makes SwiftUI a second
continuous owner of the scroll offset, and on heterogeneous multi-
thousand-row diffs every lazy row materialization re-resolves the bound
position against the moving offset (the uniform 400-row fixture never
re-resolved, which is why the earlier sim verification passed).

Drop the binding entirely. The top row is tracked in a plain reference
box via onScrollTargetVisibilityChange (no view state, no body
dependency, no layout participation), persisted at scroll-idle and on
unmount as before, and restore-on-remount becomes a one-shot
ScrollViewReader.scrollTo at appear. After that single command the
offset is owned exclusively by the scroll view's physics.

Co-Authored-By: Claude Fable 5 <[email protected]>

* ios: resolve the settled diff row by document order, not callback order

onScrollTargetVisibilityChange documents no ordering for the ids it
reports, so taking visibleIDs.first as the top row was an assumption.
FileDiffPresentation now carries a rowOrderIndex built once alongside
the rows (off-main on the async paths), and TopVisibleRowPolicy picks
the id earliest in document order. Also balances the braces in the
generated fixture diff and documents why the pre-iOS-18 fallback is
acceptable (app floor is iOS 18.4; macOS builds this package for
tests only).

Co-Authored-By: Claude Fable 5 <[email protected]>

* ios: preserve diff restore across refresh

* ios: avoid fixture lint false positive

* ios: run diff preparation off the main actor

* ios: align sentry-cocoa pins with main (9.24.0)

The fleet builder's shared warm DerivedData precompiles Sentry modules
against main's pin; this branch's older 9.21/9.23 pins invalidated those
.pcm files ("header has been modified since the module file was built")
and failed every cloud iOS build. Pins-only change, byte-identical to
main's lockfiles.

Co-Authored-By: Claude Fable 5 <[email protected]>

* ios: restore the diff scroll row from an explicit target, not the tracker

On-sim the remount restore landed at the file top: the visibility
callback fires for the unrestored top of the list before onAppear runs,
overwriting rowTracker.topRowID, so restoring from the tracker anchored
to row 1. The restore target is now captured explicitly — from the pager
at mount, from the live tracker only when a refresh re-arms the restore —
and scrollTo is re-applied once after the first layout pass because
LazyVStack only estimates offsets for unrealized rows.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-03 18:25:50 -05:00
lawrencecchen 7f9a8c62dc Test workspace tree creation action 2026-08-03 16:16:24 -07:00
Abdulaziz Albahar a2d28ba765 Keep Mobile Connect available in the command palette (#9467)
* test: cover mobile connect palette availability

* fix: keep mobile connect in command palette
2026-08-03 18:15:57 -05:00
Austin WangandClaude Opus 5 e733aa4954 Pass a valid empty MCP configuration to the auto-naming summarizer (#9473)
* Add failing test for auto-naming --mcp-config argument

Extracts the claude summarizer argv into
AutoNamingEnvironmentPolicy.claudeSummarizerArguments and asserts the
--mcp-config value is a valid MCP configuration object. It currently
emits a bare {}, which Claude Code rejects.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Pass a valid empty MCP configuration to the auto-naming summarizer

Claude Code 2.1.220 validates --mcp-config against a schema requiring an
mcpServers record, so the bare {} cmux passed made the summarizer exit
on argument validation and every workspace auto-naming attempt recorded
category: failed. Emit {"mcpServers":{}} instead.

Fixes #9457

Co-Authored-By: Claude Opus 5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-03 15:42:32 -07:00
Austin WangandClaude Opus 5 16dbad16e4 Fix Package.resolved policy false positive on leaf local-path packages (#9470)
* Add failing test for leaf local-path Package.resolved false positive

Adding a dependency-free local-path package to a manifest that already has
remote pins makes check-package-resolved-policy.py demand three Package.resolved
diffs that swift package resolve cannot produce.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Key Package.resolved policy off reachable remote dependency calls

check-package-resolved-policy.py demanded a Package.resolved diff whenever a
manifest's dependency calls changed and that manifest's graph had any remote
dependency anywhere. Adding a dependency-free local-path package to such a
manifest therefore reported violations for lockfiles that `swift package
resolve` leaves byte-identical, so the demanded diff could not exist.

The graph now records the normalized text of every `.package(url:)` call per
manifest, and a manifest edit requires a lockfile diff only when the set of
url calls reachable through its local-path closure differs between merge-base
and HEAD. That set is exactly what SwiftPM pins, so version-requirement bumps
on an unchanged URL and newly reachable remote-bearing local packages still
require the diff.

Fixes #8871

Co-Authored-By: Claude Opus 5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-03 15:38:39 -07:00
Austin WangandClaude Opus 5 ea8c7a6fb8 Retract recovered daemon transport errors from the workspace sidebar (#9472)
* Add failing test for recovered daemon transport bounce leaving sidebar error

Co-Authored-By: Claude Opus 5 <[email protected]>

* Retract recovered daemon transport errors from the workspace sidebar

Fixes #8917

* Move the daemon recovery regression test to Swift Testing

* Drop stray blank line in WorkspaceRemoteConnectionTests

* Import CmuxSidebar in the daemon recovery test

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-03 15:32:41 -07:00
Abdulaziz Albahar be6516f704 Preserve iOS workspace groups during reconnect
Track whether mobile workspace-list responses actually include groups, preserve the last authoritative group snapshot through reconnect and empty transient states, and allow healthy connected empty snapshots to clear stale group headers.
2026-08-03 17:21:05 -05:00
Austin Wang 8cc5dc4a6e Treat "no update available" as a success in Attempt Update (#9435)
* Add failing test: Attempt Update with no update available must not report install failure

* Add failing UI test: Attempt Update with no update available must not show an error pill

* Treat 'no update available' as a success in Attempt Update

Fixes the red "Update Didn't Start / check your internet connection" pill
shown when Attempt Update runs while already on the latest version.
2026-08-03 14:11:11 -07:00
Austin Wang 004d414746 Rename the focused workspace group with Cmd+Shift+R (#9428)
* Add failing regression test for Cmd+Shift+R on a focused workspace group

Covers https://github.com/manaflow-ai/cmux/issues/9199: renaming from the
shortcut while a group's anchor row is focused leaves the group header
name untouched.

* Rename the focused workspace group with Cmd+Shift+R

A workspace group's header row is backed by an anchor workspace whose own
title is hidden: the row renders the group name. The anchor's title is
seeded from the group name at creation and never resynced, so renaming
the focused anchor workspace prefilled a stale name ("Group 3") and
changed nothing visible.

Resolve the palette rename target through a shared resolver: when the
focused workspace is a group anchor, target the group, matching that
row's "Rename Group..." context menu item. Every other workspace still
renames itself.

Fixes #9199

* Move rename-target resolution onto CommandPaletteRenameTarget

Review feedback: the static-only resolver enum was a namespace type. The
focused-workspace resolution is now an initializer on the value it builds,
and the group anchor descriptor lives in its own file.

* Make the group rename UI test tolerate headless CI activation

* Assert the group rename regression through the control socket

The accessibility-label assertion passed on the unfixed build, so it was
not catching the bug. Setup and verification now go through the control
socket: create a group, rename only the group so the anchor workspace
title goes stale, focus the anchor, press Cmd+Shift+R, and assert the
group's name in the model changed.

* Drop the non-discriminating group rename UI test

The test passed on a build without the fix (run 30786168603), so it did
not capture the regression. Coverage stays with the resolver unit tests
until a UI-level check that actually fails on the bug is written.

* Restore the group rename UI test with a launch path that cannot pass vacuously

The previous version wrapped app.launch() in a non-strict XCTExpectFailure.
On a headless runner that absorbs the launch failure and, with
continueAfterFailure = false, abandons the rest of the test body without
recording anything, so the test reported success without running a single
assertion (proved by a variant carrying an unconditional XCTFail that also
passed: run 30787440944).

* Drop the group rename UI test: it cannot run on the e2e runner

With the vacuous-pass workaround removed, the test fails on both a fixed
and an unfixed build at the same line: app.activate() raises "Failed to
activate application (current state: Running Background)". The hosted
runner has no foreground GUI session, and XCUITest keystrokes only reach
a frontmost app, so a keystroke-driven test cannot work there.

Runs: 30788920066 (no fix) and 30788928069 (fix) — identical failure.
2026-08-03 14:10:06 -07:00
Abdulaziz Albahar f4787432f2 Fix INTERNAL iOS Ghostty startup crash
Pins the cmux Ghostty submodule to the locale-before-crash-reporting startup fix. ASC evidence for local 2026-08-02 showed all three cmux INTERNAL reports shared EXC_BAD_ACCESS in ghostty_init + 1388 with the main thread in setlocale/loadlocale via GhosttyRuntime.swift:110.
2026-08-03 15:46:37 -05:00
Abdulaziz AlbaharandClaude Fable 5 eee859d354 Harden route-content equivalence against reorders, missing baselines, and install races (#9402)
* Add failing tests for route-content equivalence hardening

Pins six behaviors from the cubic review of #9342: reorder-only
capability, relay fleet, and grant verification key revisions keep
live sessions; a snapshot installed for a revision recorded without
content fails closed; an older route revision install cannot roll
back a newer one; a redundant-dial close raced by invalidation
redials instead of returning the closed winner.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Harden route-content equivalence against reorders and races

Canonicalizes route content so order carries no meaning where the
admission policy reads sets: binding capabilities, the relay fleet,
and grant verification keys (by kid) are sorted when the content is
built, so reorder-only revision bumps keep live sessions.

didInstallRouteRevision now drops installs older than the recorded
revision, so an older completion of an overlapping reconciliation
cannot roll back a newer installed revision. The same-revision branch
compares the stored baseline and fails closed through the standard
superseded-peer invalidation when the baseline is missing or differs,
instead of silently adopting the content.

The peer session no longer returns a stale winner capture after the
redundant-dial close: settleRedundantDial re-reads the active slot
and its liveness after the close suspension and redials when the
winner was invalidated, replaced, or remotely closed.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-03 14:59:42 -05:00
lawrencecchen 4d62f5437b Optimize journal ingress merge representation 2026-08-03 10:14:47 -07:00
lawrencecchen a6e27363ef Sync SDK descriptors with journal catalog 2026-08-03 09:58:14 -07:00
lawrencecchen 0a92bc94c3 Make graphics invalidation race test deterministic 2026-08-03 09:38:44 -07:00
lawrencecchen 951150abd4 Reject terminal attaches after host exit 2026-08-03 09:29:46 -07:00
lawrencecchen 34dac05868 Test every journal CLI operation path 2026-08-03 09:29:46 -07:00
lawrencecchen 534282b87d Bound terminal journal chunk allocations 2026-08-03 09:06:17 -07:00
lawrencecchen ee03102caf Test bounded terminal journal chunk allocations 2026-08-03 09:04:37 -07:00
lawrencecchen c63683f6d6 Preserve terminal journal batches across writer faults 2026-08-03 09:02:58 -07:00
lawrencecchen 1ed726f3f3 Test terminal journal recovery without data loss 2026-08-03 09:00:55 -07:00
lawrencecchen 48f779bda7 Keep journal idempotency stable across reconnects 2026-08-03 08:54:13 -07:00
lawrencecchen 2cf5e616ac Test journal retries across CLI reconnects 2026-08-03 08:51:39 -07:00
lawrencecchen 4951cb8e3b Omit absent journal hook filters from results 2026-08-03 08:44:36 -07:00
lawrencecchen 80776b6d1f Test journal hook list filter contract 2026-08-03 08:43:05 -07:00
lawrencecchen 996476db96 Fix journal restore preview result schema 2026-08-03 08:37:12 -07:00
lawrencecchen f0d97ed45c Test journal restore preview result contract 2026-08-03 08:35:21 -07:00
lawrencecchen 4c6abe344d Implement fast session journal hooks and replay 2026-08-03 08:32:55 -07:00
lawrencecchen e4cc51324a Format agent state validation 2026-08-03 07:38:04 -07:00
lawrencecchen c8bf9ee9de Use canonical agent states in CLI 2026-08-03 07:28:15 -07:00
lawrencecchen 353894b5f8 Test canonical agent CLI states 2026-08-03 07:24:17 -07:00
lawrencecchen 765375e38c Refresh sidebars when agent state changes 2026-08-03 07:17:29 -07:00
lawrencecchen fd015152d9 Test live agent sidebar updates 2026-08-03 07:13:51 -07:00
lawrencecchen 31aabfacbd Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-03 06:57:42 -07:00
lawrencecchen 707ec830d0 Fix resize feedback and exact tab rename 2026-08-03 06:57:32 -07:00
lawrencecchen 704d411dee Test resize acknowledgements and exact tab rename 2026-08-03 06:57:11 -07:00
lawrencecchen 633549cf3e Add configurable native sidebar projections 2026-08-03 06:56:48 -07:00
Lawrence Chen 249d0ff799 Include Pi session titles in notifications (#9452)
Include the target Pi surface title in notification titles while preserving the Pi fallback and leaving other agents unchanged.
2026-08-03 05:37:22 -07:00
lawrencecchen ab63d93d3c Test journal receipt isolation by origin 2026-08-03 05:27:36 -07:00
lawrencecchen ddb3aaac9e Test journal convergence with terminal multiview 2026-08-03 05:25:33 -07:00
lawrencecchen 826f11302b Merge branch 'feat-terminal-multiview' into feat-cmux-tui-session-journal
# Conflicts:
#	cmux-tui/bindings/cpp/.cmux-resource-api.json
#	cmux-tui/bindings/go/.cmux-resource-api.json
#	cmux-tui/bindings/java/.cmux-resource-api.json
#	cmux-tui/bindings/python/.cmux-resource-api.json
#	cmux-tui/bindings/rust/.cmux-resource-api.json
#	cmux-tui/bindings/rust/src/resource/mod.rs
#	cmux-tui/bindings/rust/tests/catalog_manifest.rs
#	cmux-tui/bindings/typescript/.cmux-resource-api.json
#	cmux-tui/bindings/zig/.cmux-resource-api.json
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry.rs
#	cmux-tui/crates/cmux-tui-core/src/workspace_registry/terminal_exit_store.rs
#	cmux-tui/scripts/test_check_resource_api_boundary.py
#	cmux-tui/spec/commands.md
#	cmux-tui/spec/resource-api-v1.md
#	cmux-tui/spec/resource-operations-v1.md
2026-08-03 04:57:06 -07:00
lawrencecchen 6e797d0767 test(tui): reproduce input and resize lifecycle races 2026-08-03 04:51:57 -07:00
lawrencecchen 4db16e080b Add native SSH and rename menus to TUI columns 2026-08-03 04:30:55 -07:00
lawrencecchen 7cc56951fd Resolve checkpoint terminals by public identity 2026-08-03 04:13:28 -07:00
lawrencecchen fde0042687 Test checkpoint capture with public terminal IDs 2026-08-03 04:11:12 -07:00
lawrencecchen a2e698e366 Satisfy journal CLI warning gate 2026-08-03 04:06:23 -07:00
lawrencecchen bc9b31b49c fix(tui): atomically detach exited terminal views 2026-08-03 04:04:29 -07:00
lawrencecchen 97ee0fed25 Harden journal integrity and hook isolation 2026-08-03 04:04:08 -07:00
lawrencecchen 884e8a79fc Test journal integrity and hook isolation 2026-08-03 04:00:06 -07:00
lawrencecchen 1bfa8ab55f Add extensible journal hooks and restoration primitives 2026-08-03 03:57:27 -07:00
lawrencecchen e9e476bb16 test(tui): reproduce Ctrl-D exit topology leak 2026-08-03 03:36:05 -07:00
lawrencecchen 2a4d6b4923 Merge remote-tracking branch 'origin/main' into feat-tui-resource-columns 2026-08-03 03:35:45 -07:00
Lawrence Chen 6edf2570b8 Publish the Rust SDK as cmux-sdk (#9445)
* Rename Rust SDK package to cmux-sdk

* Preserve Python release artifact digests

* Make crate bootstrap recovery independent

* Pin crate bootstrap tests to build job

* Stop crate bootstrap publication on cancellation
2026-08-03 03:31:33 -07:00
lawrencecchen 7ad218bfde Register machine source menu contract 2026-08-03 03:19:20 -07:00
lawrencecchen 749c5bf576 Prototype configurable TUI resource columns 2026-08-03 03:15:47 -07:00
lawrencecchen 6a0417d8fa Share decoded journal fanout across subscribers 2026-08-03 02:14:05 -07:00
lawrencecchen c567d31b52 Add compiled journal regex filters 2026-08-03 02:03:10 -07:00
lawrencecchen 96937e1934 Test compiled journal regex filters 2026-08-03 01:53:14 -07:00
lawrencecchen d6dd4aa32e Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-03 01:47:40 -07:00
lawrencecchen 785498a4cf Negotiate journal support with resident sessions 2026-08-03 01:46:22 -07:00
lawrencecchen d54ed30b9d Test stale journal subscription negotiation 2026-08-03 01:43:26 -07:00
lawrencecchen 1e96971e62 fix(tui): keep modifiers outside semantic input 2026-08-03 01:42:42 -07:00
lawrencecchen cd91e0a660 test(tui): reproduce modifier prefix cancellation 2026-08-03 01:41:13 -07:00
1927f130f6 Make iOS workspace groups and reconnect dogfood-ready (#9326)
* test(ios): cover workspace group row actions

* fix(ios): restore workspace group row actions

* test(ios): cover group destructive confirmations

* test(ios): cover group read-state action refresh

* fix(ios): refresh group native action state

* test(ios): cover group native action inputs

* fix(ios): refresh group native action inputs

* test(ios): cover group swipe completion and rename alert

* fix(ios): restore workspace preview compilation

* test(ios): target visible group rename fixture

* fix(ios): preserve group swipe completion and compact rename

* test(ios): exercise group action presentation lifecycles

* test(ios): preserve workspace actions on group menus

* fix(ios): preserve workspace actions on group menus

* test(ios): exercise full group read swipe

* test(ios): isolate native group menu assertions

* test(ios): cover preserved group actions

* test(ios): keep preview fixture state owned

* test(ios): cover preserved group create actions

* fix(ios): preserve group creation entrypoints

* fix(ios): make destructive group requests atomic

* test(ios): cover configured group icons

* fix(ios): sync effective group icons

* test(ios): target live group row swipe

* test(ios): disambiguate group workspace rename

* fix(ios): disambiguate group workspace rename

* test: cover disconnected iOS dogfood launch

* fix: require connected iOS dogfood launches

* fix: make mobile readiness event driven

* Add failing iroh wake reconnect regressions

* Guarantee bounded foreground reconnect

* fix: harden mobile readiness lifecycle

* perf: buffer deadline event reads

* Add failing test: session snapshot mid-revalidation must classify transient

Every launch/foreground kicks a /users/me revalidation and
sessionTokenTransitionIsActive is true for its whole round trip.
authenticatedSessionSnapshot() throws .unauthorized for that window, which
the iroh broker token source treats as signed out, so endpoint activation
fails closed (endpointFailed authorizationFailed) on every app launch until
the revalidation completes. The same state is already classified
.networkError by accessToken(); the snapshot must match.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Classify transient token misses as connectivity, not authorization failure

Three-layer fix for the launch-time wedge where every iroh endpoint
activation failed closed (endpointFailed authorizationFailed) while a
foreground session revalidation owned the token store:

1. AuthCoordinator.authenticatedSessionSnapshot() now throws .networkError
   while sessionTokenTransitionIsActive, matching accessToken()'s
   classification. Every launch/foreground kicks a network /users/me
   revalidation, and that window previously read as "signed out".

2. CmxIrohBrokerTokenSource.credentialPair is now throwing. A throw means
   "cannot read a coherent pair right now" and the broker classifies it
   .connectivity, so retry policies, verified-policy preservation, and the
   cached offline-policy bootstrap all apply. nil still means definitively
   signed out and fails closed with .missingAuthentication.

3. The iOS activation token source maps AuthError.unauthorized to nil
   (fail closed) and rethrows every transient failure instead of collapsing
   both into nil with try?.

Diagnosed from cmuxdiag exports on build 1.0.4 (20260731034828): three
consecutive relayPolicyRefreshFailed/endpointFailed(authorizationFailed)
within 10ms each (no network round trip) at launch, recovering only ~15s
later when the revalidation settled and the backoff retried.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Apply the same transient-token classification to the Mac host runtime

The Mac host's activation token source had the identical try? collapse:
a session revalidation window read as signed-out and tore the host
runtime down as unauthorized. Same mapping as iOS: unauthorized fails
closed with nil, transient failures rethrow and classify connectivity.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Add failing wake-auth transport regressions

A broker 401 at app wake (token pair rotated by another lane between
capture and server validation) must not tear down the verified iroh
runtime, and the Mac being redialed must not be dialed a second time as
a background-control aggregation candidate.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Survive wake-time broker auth rejections without endpoint teardown

At app wake the relay-policy refresh races the RPC lane's force token
refresh: the pair captured coherently a moment earlier reaches the
broker after rotation and gets a 401. That single 401 used to fail the
endpoint, clear routes and the offline cache (or tear down the whole
runtime on warm wakes), and nap 30-36s of flat backoff, turning a
seconds-long token race into the 30s-2.5min reconnect outages visible
in every wake ring.

Four changes:
- CmxIrohTrustBrokerClient recovers exactly once from a 401: the token
  source re-captures (force-minting only when the rejected access token
  is unchanged) and the request retries with the recovered pair. Frozen
  pinned sources (sign-out revocation) opt out by default.
- 401/403 now preserve verified policy during refresh, and 401 retries
  initial activation; resolvePolicy falls back to the verified offline
  bootstrap on auth rejections like it already did for connectivity, so
  LAN and cached-relay dials keep working while auth settles.
- The relay-policy refresh loop retries authorization failures on a
  2s..120s ladder instead of the flat 30s+jitter schedule.
- The Mac being redialed is excluded from secondary aggregation while a
  stored-Mac reconnect is in flight, removing the duplicate
  background-control dial (and its drain wait) from every recovery.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Handle route-gated diagnostics in iOS settings

* test: remove source-shape admission assertion

* test(iroh): cover cached registration recovery

* fix(iroh): recover cached host registration

* test connection readiness failures

* test: cover cached host binding publication

* fix: publish cached mobile host binding

* iOS: replace disconnect chrome with Mail-style status line under the computers picker

While a reconnect attempt has not been rejected, the last visible workspace
list and terminals stay accessible. The workspace list shows a caption status
line (spinner + Reconnecting… / Not Connected) under the computers picker,
like Mail's Checking for Mail…; the terminal keeps only the compact status
pill. The full-screen TerminalDisconnectedOverlay, the list's
Disconnected/Reconnecting status row for non-startup states, and the
connection status toasts are removed. The reauth banner (rejected
connection, Sign Out is the only fix) and the initial-restore status row
(Retry / Add Computer, possibly no cached content) remain. Input gating and
the pill's recovery folding, previously behind the Toasts beta flag, are now
unconditional; a Reconnect item appears in the picker menu while Not
Connected.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Harden mobile connection readiness

* Keep subscription readiness separate from recovery

* Model delayed subscription acknowledgements

* test: cover usable mobile session readiness

* fix: require usable mobile connection readiness

* test: fail closed across broker auth cancellation

* test: cover complete iOS dogfood readiness

* test: fail closed when Mac pairing setup is unavailable

* fix: make iOS dev reload dogfood ready

* test: require ensure-mac to self-heal exact tag

* fix: let ensure-mac relaunch its exact tag

* fix(ios): keep list probe state coordinator-owned

* test(ios): pass active listener to recovery validation

* test: cover unsigned simulator identity evidence

* fix: trust seeded identity in unsigned simulator

* test: disambiguate group rename alert save

* test: expose expired-ticket group rename failure

* Authorize mac-scoped workspace mutations by Stack account, not ticket lifetime

The mobile data plane's design authority is the signed-in Stack account;
attach tickets are route discovery plus scope narrowing. Four verbs
(workspace.move, workspace.group.action, workspace.group.create, and
workspace.create with group_id) still hard-required a current attach
ticket, and minted tickets default to a 600s TTL, so iOS drag-and-drop
and the + button's New Workspace Group item silently disappeared ten
minutes after pairing (and never appeared for tokenless zero-touch
pairings).

Host: ticketAuthorizationResultIfNeeded no longer fails these verbs when
the attach token is missing, unknown, or expired; a token that maps to a
current stored ticket still narrows scope, so workspace-pinned tickets
remain rejected for Mac-wide mutations. Advertised as
workspace.mutations.account_auth.v1.

iOS: MobileShellWorkspaceMutationTicketPolicy mirrors the host: against
hosts advertising the capability, mutations stay allowed unless a
current workspace-scoped ticket narrows the connection; legacy hosts
keep the fail-closed behavior. Applied to the foreground gate, the
per-target mutation gate, and secondary-Mac handle capabilities.

Co-Authored-By: Claude Fable 5 <[email protected]>

* test(ios): cover recovery transport drain

* fix(ios): drain stale route before recovery

* test: expose process-local readiness clock

* fix: use system monotonic readiness clock

* test(ios): expose scoped-ticket group rename gap

* fix(ios): preserve account-authorized group actions

* test(ios): keep group menus group scoped

* fix(ios): keep workspace group menus group scoped

* fix(ios): pass readiness clock after main merge

* test(ios): close group action review gaps

* Fix missing return in restoreCLIArgument (main compile break)

5bf9595804 (#9265) left the final expression of a multi-statement String?
method without an explicit return; every target compiling this file fails,
which currently blocks all merge-gate runs.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): redact workspace mutation failure diagnostics

An rpcError message is an arbitrary host string; exported diagnostics now
carry only the bounded DiagnosticFailureKind plus the short RPC code, and
the os.log line marks the raw error private.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): stop presenting gated connect attempts as timeouts

connectAttemptGated means another attempt owns the route, not that the
Mac failed to respond. New pairing category with wait-for-active-attempt
copy and guidance (en+ja) instead of 'No response from …' timeout text.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): add missing statusLine keys to MobileShellUI catalog

mobile.workspaces.statusLine.reconnecting/notConnected were referenced by
WorkspaceConnectionStatusLineView but absent from the package catalog, so
Japanese fell back to English defaults.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(cli): monotonic events timeout budget, deterministic reconnect wait

The --timeout budget now runs on ContinuousClock so wall-clock changes
cannot expire or extend it; each socket call derives a fresh short-lived
Date from the monotonic remainder and authentication re-checks the budget
first. The reconnect pause replaces the Timer+RunLoop pump (which can spin
or park on the CLI's unpumped command thread) with a bounded thread sleep
clamped to the remaining budget.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(ios): drop stale swiped-row identity on structural refresh

A structural update invalidates the row identity captured at swipe start;
keeping editedItemID could defer a reload against a row that no longer
exists.

Co-Authored-By: Claude Fable 5 <[email protected]>

* test(ios): align drop fixture with connection chrome

* fix: return validated restore argument

* test(ios): port drop tests to the status-line WorkspaceListTable API

Main's drop tests (from #8602) still passed connectionRecoveryFailed,
isRecoveringConnection, and retryConnectionRecovery, which this branch's
status-line rework removed from WorkspaceListTable; the package no longer
compiled on the merged tree.

Co-Authored-By: Claude Fable 5 <[email protected]>

* test(ios): drop superseded relayPolicyRetrySchedule test

The cause-aware relayPolicyRetrySchedule(for:) API this test pinned was
replaced by the shared foreground reconnect-backoff ladder during the
connection-supervisor cross-merge (see the scheduleRelayPolicyRefresh
comment); the symbol exists nowhere, so cmuxFeatureTests did not compile.
The fast-auth-retry concern lives in the ladder's own coverage.

Co-Authored-By: Claude Fable 5 <[email protected]>

* test(ios): drop superseded relay schedule assertion

* test(ios): identify inherited group menu actions

* test(ios): lock group menu action order

* test(iroh): expose truncated registration discovery

* fix(iroh): distrust truncated registration discovery

* test(connectivity): expose truncated sync snapshots

* fix(connectivity): prove complete sync snapshots

* test(connectivity): expose discovery revision races

* fix(connectivity): snapshot routes atomically

* test(ios): expose discovery blocking saved reconnect

* fix(ios): prioritize saved routes during recovery

* test(connectivity): expose endpoint recovery race

* fix(connectivity): await endpoint recovery before dialing

* fix(connectivity): fail closed on offline auth fallback

* Harden mobile group actions and reconnect readiness

* Include mobile debug registry source

* Fix group rename alert target lifetime

* Address workspace merge policy findings

* Scope reconnect policy to owning view

* Fix SSH retry test diagnostic compilation

* Align host refresh tests with auth recovery

---------

Co-authored-by: Claude Fable 5 <[email protected]>
Co-authored-by: cmux reload-cloud <[email protected]>
2026-08-03 03:15:09 -05:00
Lawrence Chen 053ba0291c Publish four SDKs without taking over cmux CLI packages (#9376)
* Test isolated four-language SDK publishing

* Isolate and coordinate four SDK publishers

* Harden SDK release orchestration

* Make SDK publishing explicitly dispatched

* Enforce SDK release provenance

* Serialize coordinated SDK releases

* Test resumable SDK publishing

* Make SDK releases safely resumable

* Test ambiguous registry publish recovery

* Reconcile ambiguous registry publishes

* Test fully reproducible SDK preflights

* Complete reproducible SDK preflights

* Test SDK publisher security boundaries

* Secure reproducible SDK publishing

* Test usable registry release state

* Require usable registry release state

* Test pre-tag registry and Go gates

* Gate SDK tags on consumable releases

* Test final SDK release race guards

* Close final SDK release race windows

* Test SDK bootstrap and propagation recovery

* Make SDK bootstrap and propagation resilient

* Test release bootstrap and public Go verification

* Fail closed before coordinated SDK releases

* Run SDK surface gate after main validation

* Test Go probe polling without pipe reuse

* Poll Go verification without pipe reuse

* Test attested PyPI project bootstrap

* Reserve PyPI SDK name before release tags

* Test non-UTF-8 Go probe output

* Decode Go probe output defensively

* Test SDK registry ownership gates

* Require SDK registry ownership before tags

* Test registry ownership and monotonic recovery

* Reconcile registry ownership and release history

* Test publisher identity and reproducible recovery

* Test reproducible Python source archives

* Bind publisher identity and reproduce SDK artifacts

* Test registry error privacy and recovery placement

* Sanitize registry transport failures

* Test monotonic and attested release recovery

* Enforce monotonic attested release recovery

* Test current provenance and post-publish reconciliation

* Verify registry state after every publish

* Test prerelease recovery and registry index skew

* Recover prerelease and index propagation safely

* Test external SDK release authority

* Gate SDK release authority outside branch workflows

* Test repository-dispatched npm provenance

* Verify repository-dispatched npm attestations

* Test approval-fresh commit-bound release checks

* Revalidate release authority at tag creation

* Test least-exposure release credentials

* Limit SDK tag credentials to the atomic push

* Test credential-locked SDK bootstraps

* Harden SDK registry bootstraps

* Test isolated release authority and convergence

* Isolate SDK release credentials

* Test fresh recoverable SDK tag retries

* Make SDK tag retries fresh and recoverable

* Test isolated registry bootstrap credentials

* Isolate registry bootstrap credentials

* Test registry recovery identity binding

* Bind registry recovery to publisher identity

* Test publishing tool cancellation and Python pinning

* Harden publishing tool runtime behavior

* Test bounded registry publisher execution

* Bound registry publisher subprocesses

* Test multi-entry npm integrity metadata

* Verify multi-entry npm integrity metadata

* Test tag recovery after main advances

* Recover tag push after main advances

* Test rerun snapshot tag normalization

* Normalize rerun release tag snapshots

* Test crates.io access policy compliance

* Honor crates.io data access policy

* Test cross-process crates.io pacing

* Pace crates checks between processes

* Test PyPI bootstrap source revalidation

* Revalidate PyPI bootstrap source

* Test published SDK source identity

* Bind published SDKs to typed source

* Test multi-entry npm provenance SRI

* Accept multi-entry npm integrity metadata

* Test publisher artifact identity binding

* Bind publishers to validated artifacts

* Scope release artifacts to workflow attempts

* Test release artifact rerun identity

* Bind reruns to attempt artifacts

* Test publisher authority revalidation

* Revalidate publisher registry authority

* Test publisher verifier isolation

* Isolate PyPI publisher authority checks

* Route SDK jobs through runner controls

* Test npm provenance runner isolation

* Keep npm provenance on GitHub runner
2026-08-03 00:18:11 -07:00
Austin Wang ddd4a01bc5 Bump version to 0.64.22 (#9442) 2026-08-03 00:14:42 -07:00
lawrencecchen b96c2b6156 Add durable journal subscriptions 2026-08-02 23:39:44 -07:00
lawrencecchen 610a7c9692 test(tui): move backpressure fixtures into workers 2026-08-02 23:28:46 -07:00
lawrencecchen 35a397355c Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-02 23:22:05 -07:00
lawrencecchen df719a1857 fix(tui): keep slow client projections current 2026-08-02 23:21:59 -07:00
Austin Wang 67d4fc12e1 Merge pull request #9436 from manaflow-ai/issue-9431-intel-sentry-init-crash
Disable Ghostty native Sentry in embedded builds
2026-08-02 23:06:45 -07:00
lawrencecchen 3019323812 test(tui): reproduce slow-client output failures 2026-08-02 23:02:12 -07:00
austinpower1258 b0b96e7b34 Fix Swift Testing diagnostic type 2026-08-02 22:53:38 -07:00
Austin Wang 42d4f04126 Merge pull request #9422 from manaflow-ai/fix-close-surface-nonexistent-ref-fallthrough
Fail closed for stale destructive surface targets
2026-08-02 22:42:52 -07:00
austinpower1258 63c8c28288 fix: complete explicit surface review coverage 2026-08-02 22:21:24 -07:00
austinpower1258 1d48844494 Disable Ghostty native Sentry in cmux builds 2026-08-02 22:01:52 -07:00
Abdulaziz Albahar 06bc29603c Fail iOS workflow when selected filter runs zero tests (#9404)
* Test selected iOS execution guard

* Fail selected iOS runs that execute zero tests

* Test selected-test diagnostic safety

* Sanitize selected-test diagnostics

* Tighten selected-test log classification
2026-08-02 23:52:21 -05:00
austinpower1258 2d709e87b7 Test embedded GhosttyKit excludes native Sentry 2026-08-02 21:46:21 -07:00
lawrencecchen 4f738f2e80 Make journal migration repeat-safe 2026-08-02 21:32:28 -07:00
lawrencecchen a6ea6627a8 Add append-only cmux-tui session journal 2026-08-02 21:20:10 -07:00
austinpower1258 5e35ff0c4a fix: harden explicit destructive surface targeting 2026-08-02 21:16:31 -07:00
lawrencecchen e88b100593 fix(tui): seed legacy compatibility workspace 2026-08-02 21:13:41 -07:00
Austin Wang 84f5755b56 Fix cmux ssh startup script syntax error in no-progress retry loop (#9425)
* Add failing test that cmux ssh startup scripts parse under /bin/sh

Regression coverage for #9423.

* Fix cmux ssh startup script syntax error in no-progress retry loop

The reusable foreground-auth + SSH PTY attach path passed a compound 'if'
as the no-progress retry loop's attach command. That loop prefixes the
command with environment assignments, which POSIX only allows before a
simple command, so /bin/sh rejected the generated cmux-ssh-startup script
with 'syntax error near unexpected token then' and cmux ssh failed
immediately.

Wrap the attempt registration and the attach command in a shell function
and pass its name, matching SSHPTYAttachStartupCommandBuilder.

Fixes #9423

* Move #9423 regression coverage to Swift Testing

Cover the defect where it lives, in the shell generator, with a Swift
Testing case instead of an XCTest addition to the CLI integration suite.
Reverts the call-site-only workaround so the next commit fixes the
generator for every caller.

* Export the no-progress attach budget instead of prefixing the command

SSHPTYAttachExitCode.noProgressRetryLoopLines prefixed the caller's
command with environment assignments. POSIX only allows an assignment
prefix before a simple command, so the reusable foreground-auth attach
path, which passes a compound 'if ...; then ...; fi', generated a
cmux-ssh-startup script that /bin/sh rejected with 'syntax error near
unexpected token then'. cmux ssh failed immediately on 0.64.21.

Assign and export the budget on their own lines so any command shape is
legal and children still see the values.

Fixes #9423
2026-08-02 21:11:17 -07:00
lawrencecchen 9f433018c4 test(tui): cover legacy workspace selection recovery 2026-08-02 21:10:56 -07:00
lawrencecchen 9349df26ad Merge remote-tracking branch 'origin/main' into feat-terminal-multiview
# Conflicts:
#	cmux-tui/crates/cmux-tui/src/session/remote.rs
2026-08-02 20:46:53 -07:00
Austin Wang 7dff5ec471 Clear Dock notifications when focused (#9418)
* test: cover Dock notification dismissal on focus

* fix: dismiss Dock notifications on focus

* fix: preserve focus history host conformance
2026-08-02 19:53:16 -07:00
Austin WangandClaude Opus 5 4de871173e Preserve CLAUDE_SECURESTORAGE_CONFIG_DIR across agent restore (#9419)
* Add failing test for CLAUDE_SECURESTORAGE_CONFIG_DIR capture

Co-Authored-By: Claude Opus 5 <[email protected]>

* Allowlist CLAUDE_SECURESTORAGE_CONFIG_DIR in agent launch env capture

Co-Authored-By: Claude Opus 5 <[email protected]>

* Move Claude secure storage env tests to their own file

Co-Authored-By: Claude Opus 5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-02 19:17:28 -07:00
Austin WangandClaude Opus 5 b4c2163a37 Fix noclobber 'cannot overwrite existing file' error from bash shell integration (#9420)
* Add failing regression test for bash shim noclobber error

Repros https://github.com/manaflow-ai/cmux/issues/9356: with `set -o noclobber`,
the bash integration's second shim write prints "cannot overwrite existing file"
and leaves the shim stale.

Co-Authored-By: Claude Opus 5 <[email protected]>

* Force-clobber cmux-owned generated files in bash/zsh shell integration

Under `set -o noclobber` the bash integration's per-surface CLI shim write
(`} >"$shim_path"`) is refused by the shell, printing
"cannot overwrite existing file" on every prompt and leaving the shim stale.
`2>/dev/null` cannot suppress it: the shell reports the redirect failure before
the compound command's stderr redirection applies.

Switch that write, and the remaining plain-`>` writes to cmux-owned generated
files in the bash integration (bg pid file, gh stderr capture, history temp
file, history-last marker) plus the zsh gh stderr capture, to the explicit
clobber operator `>|`, matching what the rest of both integrations already use.

Fixes #9356

Co-Authored-By: Claude Opus 5 <[email protected]>

---------

Co-authored-by: Claude Opus 5 <[email protected]>
2026-08-02 19:16:26 -07:00
austinpower1258 bd89d1c16c fix: fail closed for stale destructive surface targets 2026-08-02 19:09:51 -07:00
austinpower1258 786a077bc3 test: cover stale destructive surface targets 2026-08-02 19:09:36 -07:00
Austin Wang 33ac210ab4 Bump version to 0.64.21 (#9414) 2026-08-02 17:24:10 -07:00
Myk MelezandClaude Fable 5 15ca94fd16 Pin mixed vanished/unreadable TTY diagnostics as incomplete
Review claimed a vanished TTY could launder another terminal's failed
diagnostic into a complete scan. It cannot: non-ENOENT diagnostics never
remove a terminal from the retry set, so completeness requires either a
clean re-query or explicit ENOENT for every terminal. Pin the exact
mixed case: ENOENT for one TTY plus Permission denied for another
re-queries only the unreadable terminal and stays incomplete when its
diagnostic persists.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PDYSKWqKSHrr6MAuMtou9C
2026-08-02 15:58:45 -07:00
Austin Wang ff3b4aa3cd Fix registry kind test fixture compilation (#9413)
* test: fix registry kind fixture compilation

* test: respect agent cwd policy in kind matrix
2026-08-02 15:06:45 -07:00
Austin Wang e7ca40e6e1 Preserve Pi resume identity across repeated restores (#9399)
* test: cover repeated Pi restore identity

* fix: preserve Pi identity across repeated restores
2026-08-02 14:52:07 -07:00
Austin Wang 9fc3212e72 Fix Kimi restore-of-restore binding kind decoding (#9397)
* Add restore binding kind regression tests

* Fix registry-owned restore binding kind decoding
2026-08-02 14:05:17 -07:00
Lawrence Chen afe629534f Keep sidebar icon until avatar URL exists (#9386)
* test: require avatar URL before photo mode

* fix: keep sidebar icon until avatar URL exists
2026-08-02 01:00:36 -07:00
Lawrence Chen e49777b6c1 Harden remote daemon transports and workspace RPC (#9390)
* Add remote daemon security regressions

* test(cmux-tui): reject multiline Cargo paths

* fix(cmux-tui): harden package build metadata

* fix(remote): harden workspace file mutations

* Harden remote network admission

* Harden remote runtime state handling

* fix(remote): serialize identity persistence safely

* fix(remote): persist one logical connection attempt

* Harden remote CLI secret handling

* Silence release-only remote CLI warning

* Fix remote CLI test lint

* docs(cmux-tui): remove unsafe credential examples

* test(cmux-tui): expose transient approval authorization

* fix(cmux-tui): close remote review blockers

* test(cmux-tui): expose unbounded admin request read

* fix(remote): bound diagnostics and lifecycle cleanup

* test(remote): expose admin frame boundary mismatch

* fix(remote): align relay MSRV and admin framing

* test(remote): expose replayed workspace cursors

* fix(remote): retain workspace query continuations

* test(remote): expose identity and socket durability gaps

* Harden client socket and trust persistence

* Add regressions for remote review findings

* Fix remote review findings

* test(remote): expose intermediate symlink traversal

* Harden remote directory creation against symlinks

* test(remote): expose authorization commit gaps

* Fix committed identity state and relay ticket expiry

* test(remote): expose blocking Iroh secret reads

* Harden persisted Iroh secret reads

* test(remote): expose mux reassembly budget release

* Retain ingress budgets through mux reassembly

* test(remote): specify owned client socket handoff

* Own client socket cleanup through bridge shutdown

* test(remote): expose final review races

* Fix final remote review races

* test(remote): expose cross-process ownership gaps

* Fix cross-process remote ownership races

* test(remote): expose final lifecycle leaks

* Bound remote startup and dropped request cleanup

* test(remote): expose shared auth state race

* Fix shared authorization state ownership

* test(remote): expose shutdown state lease gap

* Retain auth lease through blocking writes

* test(remote): expose daemon handoff contention

* Retry authorization state during daemon handoff

* test(remote): expose queued auth loss on exit

* fix(remote): drain auth persistence on shutdown

* test(remote): expose shutdown ownership races

* fix(remote): preserve daemon shutdown ownership

* test(remote): expose shutdown queue and hook races

* fix(remote): coalesce auth persistence snapshots

* test(remote): expose auth finalization gaps

* fix(remote): finalize auth before lifecycle cleanup

* test(remote): isolate concurrent cleanup pauses

* test(remote): expose stale metadata after auth failure

* fix(remote): clear lifecycle metadata after auth failure

* test(remote): expose legacy sidecar handoff race

* fix(remote): fence legacy sidecar process exit

* test(remote): satisfy cleanup clippy gate

* test(remote): expose failed finalization handoff

* test(remote): expose unavailable pidfd upgrade

* fix(remote): fall back when pidfd is unavailable

* fix(remote): authenticate shutdown finalization

* test(remote): expose unsafe shutdown recovery

* fix(remote): bind shutdown to daemon lifecycle

* test(remote): expose stale shutdown evidence

* fix(remote): close shutdown evidence gaps

* test(remote): expose unclean shutdown recovery

* fix(remote): recover unclean daemon shutdowns

* test(remote): expose unfenced legacy restart

* fix(remote): fence legacy automatic restarts

* test(remote): expose lifecycle fence dead ends

* fix(remote): make lifecycle fencing recoverable

* test(remote): expose rollback and malformed runtime gaps

* fix(remote): fence authorization state across rollbacks

* test(remote): expose unconfirmed auth rollback fence

* fix(remote): reconfirm auth fence durability

* test(remote): expose preflight and fence durability gaps

* fix(remote): preflight recovery before auth mutation

* test(remote): expose lifecycle startup retry gaps

* fix(remote): make fenced startup retries durable

* test(remote): expose active lifecycle durability gaps

* fix(remote): durably own active daemon lifecycle

* test(remote): expose unlocalized recovery guidance

* fix(remote): localize recovery guidance

* test(remote): expose final lifecycle review gaps

* fix(remote): fence authorization before lifecycle state

* test(remote): cover review regressions

* fix(remote): pin workspace operations to descriptors

* fix(tui): use shared pty child abstraction

* test(remote): expose delayed enrollment timeout

* fix(remote): preserve invitation approval window

* test(remote): cover replaced workspace roots

* fix(remote): preserve pinned workspace identity

* fix(tui): call renameat2 through the Linux syscall

* test(remote): cover pinned query and approval windows

* fix(remote): preserve pinned query and approval windows

* test(remote): cover resume expiry task lifecycle

* fix(remote): cancel obsolete resume expiry tasks

* test(remote): cover background task shutdown

* fix(remote): bound background task lifetimes

* fix(sdks): preserve Rust 1.88 support

* test(remote): cover transient Unix dial failures

* fix(remote): retry transient Unix dial failures

* test(remote): cover terminal reconnect failures

* fix(remote): retry only carrier failures

* test(remote): cover review regressions

* fix(remote): close reviewed lifecycle gaps

* Clarify relay routing key in TUI help

* Keep terminal provider failures out of reconnect

* test(remote): match control-character build diagnostic

* fix(relay): bound Durable Object outbound queues

* test(tui): expose acknowledged stream close race

* fix(tui): preserve completed Go stream opens

* test(tui): cover carrier and Git environment isolation

* test(tui): cap authenticated Iroh carrier fixture

* fix(tui): bound carriers and isolate Git RPCs

* test(tui): cover workspace HTTP raw admission

* fix(tui): admit workspace HTTP before parsing

* test(tui): cover Unix accept recovery and daemon locale

* fix(tui): recover Unix listeners and localize daemon output

* test(tui): cover autoreview regressions

* fix(tui): close autoreview regressions

* test(tui): cover final remote daemon review findings

* fix(tui): bound final remote daemon resources

* fix(tui): close final autoreview findings

* test(remote): preserve pagination cursor after deadline

* fix(remote): commit pagination cursors after delivery

* fix(remote): acknowledge delivered pagination pages

* test(remote): cover final transport review regressions

* fix(remote): close final transport review findings

* fix(tui): preserve Rust 1.91 SQLite compatibility

* test(remote): cover custom recovery socket selection

* fix(remote): close final latency and recovery findings

* test(remote): cover final socket hardening regressions

* fix(remote): preserve socket directory ownership boundaries

* test(remote): distinguish managed and caller-owned directories

* fix(remote): separate managed and caller-owned directories

* test(remote): cover final autoreview findings

* fix(remote): close final autoreview findings

* test(pty): bound hardened descriptor fallback

* fix(pty): fail fast on oversized fallback scans
2026-08-01 19:29:55 -07:00
lawrencecchen ea1e38db67 Implement client-local terminal projections 2026-08-01 18:27:41 -07:00
Abdulaziz AlbaharandClaude Fable 5 175127ea59 Preserve live peer sessions across equivalent route revision bumps (#9342)
* Add failing tests: live sessions must survive equivalent route revisions

Two regressions captured from foreground telemetry on build 20260801001626:

1. equivalentRouteRevisionBumpKeepsTheLivePeerSession: a broker
   connectivity sync that bumps the account route revision without
   changing the peer's material route content (only last_seen_at moved)
   tears down the live admitted session with runtimeReconfigured.

2. concurrentRedialCannotDisplaceAnInstalledLiveSession: two concurrent
   connectedSession callers can both pass the installed-slot check across
   the dead-on-arrival probe suspension, so the second install displaces
   the first admitted session without closing it and records a second
   established lifecycle event.

Both tests fail on current code; the fix lands in the next commit.

Co-Authored-By: Claude Fable 5 <[email protected]>

* Preserve live peer sessions across equivalent route revision bumps

The connectivity engine tore down every peer session whenever the
account route revision changed, even when the peer's route content was
identical. Broker registration heartbeats bump the revision while only
moving last_seen_at and path-hint freshness, so a foreground iOS client
lost its live control session every 10-90 seconds to a
runtimeReconfigured close followed by a full rediscover-dial-pair cycle.

The engine now derives CmxConnectivityRouteContent from each installed
snapshot: per-peer admission material (binding id, app instance, tag,
platform, identity generation, pairing flag, capabilities) plus
account-wide trust material (relay fleet, LAN rendezvous, grant
verification keys). On a revision change it invalidates only peers whose
material content differs. A changed endpoint identity keys the peer out
of the new content, a removed binding leaves it unrouted, and any
account-material change tears down all peers, so every security-relevant
change still invalidates. A missing baseline or a revision bump without
a replacement snapshot fails closed and keeps the old invalidate-all
behavior.

Also close the double-establish race in CmxConnectivityPeerSession: the
dead-on-arrival probe suspends the actor between clearing the pending
dial and installing it, so a concurrent caller could install its own
dial in that window and the late installer silently displaced the live
session while double-recording an established lifecycle. The installer
now rechecks the installed slot after the probe and adopts the winner,
closing its own redundant session.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-01 19:41:59 -05:00
lawrencecchen 962bdfd035 Test enhanced prefix split routing regression 2026-08-01 16:10:49 -07:00
Austin Wang 2f4059bd32 Keep explicit agent restore records across shell preexec (#9391)
* test: cover restore binding across shell preexec

* fix: retain manual agent restore bindings

* test: cover Grok restore generations and Dock replacement

* fix: preserve replacement Dock resume bindings

* fix: construct Vault presenter on main actor
2026-08-01 15:00:57 -07:00
Austin Wang 6f79836228 Fix Vault popover crashes in recycled rows (#9392)
* test: keep Vault presentation out of hosted row content

* fix: own Vault popovers outside recycled rows

* fix: harden Vault popover table lifecycle
2026-08-01 14:42:01 -07:00
Austin Wang 6d47bf38c3 Resolve restore targets from live process identity (#9384)
* test: cover stale surface restore routing

* Prefer live terminal routing for restore

* test: reject ambiguous restore TTY routing

* Reject ambiguous TTY restore routing

* test: cover live restore target resolution

* Resolve restore target from live process

* test: cover authoritative restore routing failure

* Fail closed on missing live restore target

* test: cover restored TTY registration race

* Wait for fresh restore TTY registration

* test: scope relay restore TTY routing

* test: constrain relay TTY resolution

* test: preserve relay restore workspace aliases

* Scope relay restore to authenticated terminal

* test: cover live and Dock TTY routing

* fix: complete live TTY restore routing

* test: cover ended and cached TTY routing

* fix: retire stale TTY lifecycle evidence

* test: cover fail-closed restore and reconnect routing

* fix: make restore routing readiness authoritative

* test: cover transferred and persistent TTY routing

* fix: preserve live TTY proof across bridge retries

* test: cover relay restore after new workspace move

* fix: preserve relay routing across workspace moves

* test: cover relay ownership after surface moves

* fix: keep relay provenance scoped through moves

* test: cover stale relay TTY lifecycle

* fix: retire relay TTY provenance on terminal end

* test: cover durable relay identity after moves

* fix: keep relay identity durable across moves

* test: cover authoritative TTY trust boundaries

* fix: bind TTY reports to terminal runtime

* test: cover Grok restore routing

* test: cover relay TTY readiness gaps

* fix: wait for relay TTY readiness
2026-08-01 13:42:40 -07:00
lawrencecchen 56fb9aca77 Align lifecycle docs with terminal projections 2026-08-01 08:58:15 -07:00
lawrencecchen 35974b46a9 Document terminal projection resource path 2026-08-01 08:49:35 -07:00
lawrencecchen 6f783b4ec9 Document terminal project in CLI help 2026-08-01 08:44:50 -07:00
lawrencecchen 71f2a5aaf6 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview
# Conflicts:
#	cmux-tui/bindings/ERGONOMICS.md
#	cmux-tui/crates/cmux-tui-core/src/mux.rs
#	cmux-tui/crates/cmux-tui-core/src/surface.rs
#	cmux-tui/crates/cmux-tui/src/session/remote.rs
#	cmux-tui/crates/cmux-tui/tests/terminal_host_recovery.rs
2026-08-01 08:38:43 -07:00
Lawrence Chenandaustinpower1258 80f40831da cmux-tui: render inline Kitty images through libghostty (#8811)
* test: cover kitty placement frame reuse

* fix: cache kitty placement frames

* test: cover pixel-accurate kitty clipping

* fix: clip kitty placements in pixel space

* test: cover pixel-accurate kitty replay clipping

* fix: clip kitty replay in pixel space

* test: cover number-only kitty image attach

* test: cover both numbered kitty image aliases

* fix: preserve kitty number aliases across attach

* test: cover inflight kitty replay across resize

* fix: preserve inflight kitty replay

* test: cover anonymous kitty replay collisions

* fix: preserve anonymous kitty placements in replay

* test: cover kitty object count limits

* fix: bound kitty graphics object counts

* test: cover Kitty graphics in web render mode

* test: cover host kitty scene invalidation

* fix: restore kitty graphics after host resize

* feat: render Kitty graphics in web terminal

* test: cover graphics writer shutdown quiescence

* fix: layer Kitty graphics above cell backgrounds

* fix: draw web graphics from callback ref

* fix: quiesce graphics before terminal restore

* test: cover kitty replay allocation order

* fix: preserve kitty replay allocation order

* test: cover incremental render graphics deltas

* test(tui): cover linear graphics state maintenance

* fix: send incremental render graphics deltas

* test: cover bounded kitty replay semantics

* test(tui): cover late Kitty image ordering

* test: cover render transport size boundaries

* fix(tui): maintain Kitty graphic IDs linearly

* test: cover atomic cell geometry updates

* test(tui): cover Kitty PNG compatibility

* test: cover full render metadata budget

* test: preserve measured cell pixels across resize

* test: bound kitty pixel cache lookups

* fix: make cell geometry updates atomic

* test: bound kitty placement grouping

* fix: align render transport size budgets

* test: bound render taps and resize replay

* fix: preserve kitty images in bounded vt replay

* fix: bound render taps and skip unused replay

* docs(tui): document inline Kitty image support

* style(tui): format merged changes

* test: cover UTF-8 before kitty replay

* test: cover large kitty resize replay

* fix: distinguish UTF-8 from C1 kitty APC

* fix: preserve kitty upload across resize

* test(tui-sdk): cover retained render metadata overflow

* fix(tui-sdk): bound retained render events

* test(tui-web): expose placement canvas memory blowup

* fix(tui-web): bound graphic canvas backing

* test: preserve hosted Kitty image aliases

* fix: preserve hosted Kitty image aliases

* test(tui): cover stale Kitty write after resize clear

* fix(tui): discard stale Kitty writes after resize clear

* test(browser): expose terminal host alias protocol gap

* fix(browser): support terminal host Kitty aliases

* test(cmux-tui): expect resize alias sidecars

* test(tui): preserve sparse viewport across replay

* fix(tui): preserve sparse rows in terminal replay

* fix(tui): align replayed scrollback rows

* test(tui): await terminal host process exit

* test(tui): cover Kitty alias history and sparse replay

* fix(tui): preserve Kitty alias and sparse row history

* fix(tui): address Kitty graphics review findings

* fix(tui): harden Kitty graphics integration

* fix(tui): resolve final Kitty autoreview findings

* fix(tui): close Kitty autoreview findings

* test(tui): cover final Kitty review regressions

* fix(tui): close final Kitty autoreview findings

* test(tui): reject overflowing PTY pixel geometry

* fix(tui): reject invalid PTY pixel geometry

* test(tui): cover remaining Kitty review regressions

* fix(tui): close remaining Kitty review findings

* fix(cmux-tui): close graphics review gaps

* fix(cmux-tui): preserve attach and startup progress

* fix(tui): reconcile image render geometry

* fix(tui): align attach wire progress

* fix(tui): bound inline image rendering resources

* fix(tui): close inline image review gaps

* fix(tui): bound inline graphics hot paths

* test(tui): cover graphics attachment memory regressions

* fix(tui): bound graphics attachment allocations

* test(tui): budget retained render capacity

* test(tui): cover graphics review regressions

* fix(tui): close graphics autoreview gaps

* test(tui): cover second graphics review regressions

* fix(tui): close remaining graphics review gaps

* test(tui): cover remaining graphics review regressions

* fix(tui): close graphics review findings

* test(tui): cover final graphics review regressions

* fix(tui): close final graphics review findings

* test(tui): cover graphics admission regressions

* fix(tui): make graphics admission lazy and refillable

* test(tui): cover final host lifecycle findings

* fix(tui): bound host lifecycle work

* test(tui): cover final protocol review findings

* fix(tui): close final protocol review gaps

* test(tui): cover bounded graphics writer failure

* fix(tui): bound graphics output failure lifecycle

* test(tui): cover final remote graphics review gaps

* fix(tui): validate and localize remote attach data

* test(tui): cover final graphics ownership findings

* fix(tui): scope graphics output ownership

* test(tui): cover graphics resource safety gaps

* fix(tui): bound graphics resource lifecycles

* test(tui): cover graphics budget scan fanout

* fix(tui): make graphics admission single-pass

* test(tui): cover review resource safety gaps

* fix(tui): bound graphics attachment resources

* test(tui): cover enhanced input adapter compatibility

* fix(tui): reconcile shortcut merge with graphics input

* test(tui): reconcile merged attach fixtures

* test(tui): bound inline surface state

* fix(tui): keep libghostty state out of line

* test(tui): cover cell pixel fanout retry gap

* fix(tui): reconcile skipped cell pixel fanout

* test(tui): cover aggregate graphics ownership gaps

* fix(tui): bound aggregate graphics ownership

* test(tui): cover graphics teardown ownership

* fix(tui): rebalance graphics ownership on teardown

* test(tui): cover aggregate graphics recovery

* fix(tui): recover aggregate graphics capacity

* test(tui): isolate graphics counters per thread

* test(tui): cover graphics resource ownership gaps

* fix(tui): close graphics resource ownership gaps

* test(tui): cover Kitty replay state divergence

* fix(tui): preserve Kitty replay state across mirrors

* test(tui): cover terminal resource lifecycle stalls

* fix(tui): decouple terminal resource lifecycle work

* test(tui): cover review lifecycle regressions

* fix(tui): close review lifecycle gaps

* test(tui): cover graphics review regressions

* fix(tui): reconcile graphics lifecycle under load

* test(tui): cover graphics baseline and quota exhaustion

* fix(tui): reconcile graphics baselines and quota overflow

* test(tui): make graphics backpressure deterministic

* test(tui): cover exited quota and reset replay ordering

* fix(tui): preserve graphics state across resets and exits

* test(tui): cover scrolled Kitty placement alignment

* fix(tui): align Kitty graphics with scrolled viewports

* test(tui): bound stalled renderer output

* fix(tui): preserve renderer output backpressure

* test(tui): cover relabel and retry bounds

* fix(tui): bound graphics recovery work

* fix(ci): isolate fork-agent singleton default

* test(tui): bound persistent graphics recovery

* fix(tui): bound persistent graphics recovery

* test(tui): drain stalled quota worker

* test(tui): cover panic and fanout lifecycles

* fix(tui): bound graphics worker lifecycles

* test(web): cover exhausted graphics decode queue

* fix(web): retire exhausted graphics decode jobs

* test(tui): cover final Kitty review findings

* fix(tui): close final Kitty replay gaps

* test(tui): cover encoded Kitty quota

* fix(tui): budget encoded Kitty uploads

* test(browser): sync Kitty replay ceilings

* fix(browser): match Kitty replay ceilings

* test(tui): cover unsupported Kitty grayscale

* fix(tui): bound Kitty snapshot formats

* test(tui): cover Kitty quota recovery

* fix(tui): reconcile Kitty quota recovery

* test(tui): cover reconnect completion retry

* fix(tui): retry failed host reconnect completion

* test(tui): cover attach priority and replay cursor state

* fix(tui): preserve attach priority and replay state

* test(tui): cover superseded attach resize failure

* fix(tui): settle the latest promoted resize

* chore(tui): satisfy strict attach lifecycle lint

* test(tui): cover numeric Kitty final chunks

* fix(tui): parse Kitty chunk flags numerically

* test(tui): cover Kitty images in web scrollback

* fix(tui): render Kitty images in web scrollback

* test(tui): admit terminals after graphics quota failure

* fix(tui): degrade graphics after quota failure

* test(tui): finish graphics probe at DA1 marker

* fix(tui): end graphics probe at DA1 marker

* test(tui): reject stale scrollback image epochs

* fix(tui): version scrollback image anchors

* test(tui): refresh active scrollback epochs

* fix(tui): refresh active scrollback epochs

* test(tui): ignore screen-only history epochs

* fix(tui): scope history epochs to retained rows

* test(tui): keep image frames out of history epochs

* fix(tui): keep image frames out of history epochs

* test(tui): assert stable screen-only history epochs

* test(tui): bound deferred work and reserve attaches

* fix(tui): bound deferred graphics coordination

* test(tui): exhaust saturated Kitty quota retries

* fix(tui): exhaust saturated Kitty quota retries

* test(tui): retain overlapping Kitty replay placements

* fix(tui): clip Kitty placements at replay boundaries

* fix(tui): preserve merged attach invariants

* test(ci): cover Ghostty path metadata

* fix(ci): inspect executable Ghostty consumers

* test(ios): replace wall-clock synchronization

* chore(xcode): normalize project ordering

* fix(ssh): simplify retry script assembly

* test(app): update detached transfer fixture

* test: update remote PTY lifecycle fake

* test: require explicit app-host test mode

* fix: declare app-host test launch mode

* fix: clear Xcode 26.3 warning gate

* test: detect embedded app-host test bundle

* fix: detect app-host tests from embedded bundle

* chore: drop unreliable app-host scheme marker

* test: avoid async ARC lifetime assertion

* test: require app-host build identity

* fix: stamp app-host test builds before launch

* test: require test-runner app-host marker

* fix: forward app-host test identity through xcodebuild

* fix: keep completed iroh dial single-flight through install

* test: re-report lifecycle after status clear

* Fix cmux-tui merge integration

* test(web): cover render attach WebSocket budget

* fix(web): admit full render attach frames

---------

Co-authored-by: austinpower1258 <[email protected]>
2026-08-01 07:54:34 -07:00
lawrencecchen 684ad0f7dc Fix projected terminal response and close 2026-08-01 07:52:54 -07:00
lawrencecchen c86144508d Complete terminal multiview lifecycle contracts 2026-08-01 07:28:42 -07:00
lawrencecchen d6cb088450 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview 2026-08-01 05:46:43 -07:00
lawrencecchen d7286996fa Harden terminal multiview lifecycle 2026-08-01 05:46:39 -07:00
Austin Wang b813ab9a25 Discover fresh Grok sessions from disk (#9382)
* test: cover Grok session discovery

* test: cover Grok timestamp fallbacks

* fix: discover fresh Grok sessions
2026-08-01 05:03:27 -07:00
lawrencecchen feccd2f698 Merge remote-tracking branch 'origin/main' into feat-terminal-multiview
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/surface.rs
2026-08-01 04:55:07 -07:00
lawrencecchen e66472146e Implement client-local terminal multiview architecture 2026-08-01 04:53:42 -07:00
Lawrence Chen f560731d26 Merge pull request #8667 from manaflow-ai/feat-cmux-tui-remote-daemon
Add authenticated remote daemon and clients to cmux-tui
2026-08-01 04:34:02 -07:00
Lawrence Chen 367f2ef891 Fix sidebar avatar loading fallback (#9375)
* fix: refine sidebar avatar loading fallback

* refactor: simplify sidebar avatar fallback

* test: cover sidebar avatar launch restoration

* fix: keep sidebar avatar icon during session restore
2026-08-01 04:26:09 -07:00
Lawrence Chen 9a7e4032d9 Disclose yearly billing in compact pricing labels (#9378)
* Simplify annual pricing labels

* Tighten pricing card layout

* Align pricing numerals and app grid

* Remove annual pricing totals

* Add annual billing cadence regression coverage

* Disclose yearly billing in compact labels
2026-08-01 04:04:34 -07:00
Lawrence Chen fdd8a1b7a2 Show signed-out state on app pricing page with in-app sign-in (#7821)
* Show signed-out state on app pricing page with in-app sign-in

When the embedded /app-pricing webview has no authenticated session, the
page used to claim "Current plan: Free" (misleading for signed-out Pro
users, invites duplicate purchase) and offered no way to sign in.

Now a banner at the top says the user is not signed in and links to
sign-in, the current-plan badge is suppressed while unauthenticated, and
the Free card CTA becomes Sign in. The sign-in link runs the existing
native-sign-in handler flow inside the webview, so Stack cookies land in
the webview session and /handler/after-sign-in hands tokens to the app
via its <scheme>://auth-callback URL. BrowserNavigationDelegate now opens
the app's own auth-callback scheme via NSWorkspace (user-activated
main-frame links only) since WKWebView cannot open native schemes.

* Scope auth-callback intercept to the app web origin and split it into its own file

Two review-driven fixes to the new native auth-callback intercept:

1. Security (Codex/Greptile P1): the intercept accepted a user-clicked
   <scheme>://auth-callback link from ANY page in the embedded browser.
   Because HostBrowserSignInFlow accepts stateless callbacks, a malicious
   page could hand attacker-chosen tokens to the app and swap the
   signed-in account on one click. The predicate now also requires the
   navigation's SOURCE frame origin to match AuthEnvironment.appWebOrigin
   (the origin serving /handler/after-sign-in), reusing the normalized
   BrowserWebAuthnSecurityOrigin comparison. Links from any other origin
   fall through to the regular external-navigation handling.

2. workflow-guard-tests: BrowserNavigationDelegate.swift grew +36 lines,
   past the 25-line incidental allowance over its 635-line budget. The
   predicate and router now live in a dedicated collaborator,
   BrowserAuthCallbackNavigationPolicy, matching the delegate's existing
   pattern of small policy objects; the delegate is back to +20.

* Pin auth-callback intercept to this build's own callback scheme

Structured-review P1: AuthCallbackRouter accepts the built-in cmux,
cmux-nightly, and cmux-dev schemes plus the extra one, and the trusted
/handler/after-sign-in page can legitimately emit any allowed scheme as
native_app_return_to. Stable cmux would therefore auto-open a
token-bearing cmux-nightly://auth-callback link, handing this session's
tokens to whatever app registered that scheme (attacker-registerable
when Nightly is absent). The predicate now requires the destination
scheme to equal AuthEnvironment.callbackScheme before NSWorkspace.open;
other schemes fall through to regular external-navigation handling.

* Fail-closed auth-callback dispositions and in-process delivery

Two structured-review P1s on the intercept:

1. Not fail-closed: a rejected cmux://auth-callback link fell through to
   the generic external-app prompt, so an untrusted page's attacker-token
   link could still reach the app after one confirming click, and a
   crafted cmux-nightly link from the trusted page could reach that
   scheme's handler. The policy now returns a disposition: user-activated
   main-frame auth-callback-shaped links that fail the scheme/origin
   checks are cancelled outright (.block). Non-link-activated navigations
   keep the browser's regular handling, same as every other custom scheme.

2. Token egress through LaunchServices: NSWorkspace.open routes the
   token-bearing URL to whatever app currently claims the scheme. Accepted
   callbacks are now delivered in-process through the app delegate's
   application(_:open:) entrypoint (the exact path LaunchServices would
   invoke), so the URL never leaves this process.

The disposition handling lives in a BrowserNavigationDelegate extension in
the policy file, keeping the delegate at +6 lines over its budget base.

* Fail closed on every auth-callback-shaped navigation; return webview to pricing after delivery

Extends 6c71e6b91d on review findings:

1. disposition() now blocks ALL auth-callback-shaped navigations that are
   not the exact trusted flow (user-activated main-frame link, own scheme,
   trusted source origin). JS redirects and subframe navigations previously
   passed through to the generic external-app prompt, where one confirming
   click could hand attacker-chosen tokens to the stateless callback path.

2. The popup/new-window path (BrowserPanel.createWebViewWith) applies the
   same rule via shouldBlockExternalNavigation: auth-callback-shaped URLs
   from window.open never reach the external-app prompt.

3. After a delivered callback, the embedded flow no longer strands the
   webview on the 'Signed in to cmux' page: /app-pricing passes
   web_return_to on the after-sign-in URL and the navigation delegate
   navigates the webview back to it (same-origin relative path only), so
   the pricing page reloads with the authenticated session and shows the
   restored plan. The switch-account flow preserves the param.

* Add signed-out pricing regression coverage

* Complete embedded pricing sign-in safely

* Fail closed on targetless auth callbacks

* Split auth callback disposition policy

* Add auth callback recovery regression tests

* Complete auth callbacks across browser surfaces
2026-08-01 03:48:27 -07:00
Austin Wang a65e552e38 Fix Grok session discovery for fresh launches (#9379)
* test: cover Grok session discovery

* test: cover Grok timestamp fallbacks
2026-08-01 03:27:14 -07:00
Lawrence Chen 43d1d3c8e9 Simplify annual pricing labels (#9373)
* Simplify annual pricing labels

* Tighten pricing card layout

* Align pricing numerals and app grid

* Remove annual pricing totals
2026-08-01 03:02:49 -07:00
Abdulaziz Albahar 03b33ab399 Merge pull request #9329 from manaflow-ai/feat-cmd-bracket-workspace-history-2
Cmd+[ / Cmd+] traverse global workspace focus history; pane cycling becomes rebindable
2026-08-01 04:50:28 -05:00
austinpower1258 6340fce385 feat: add live pane attention color editor 2026-07-31 23:46:43 -07:00
austinpower1258 cb37e0dfa3 test: bound stuck close regression failure 2026-07-31 23:44:03 -07:00
austinpower1258 e443d3aa7b test: assert terminal free gate state directly 2026-07-31 23:42:37 -07:00
cmux reload-cloud d7a2c873f1 Merge remote-tracking branch 'origin/main' into issue-9065-remove-cgwindowlistcreateimage 2026-07-31 23:40:36 -07:00
cmux reload-cloud 673bb9b65c fix: serialize screenshot capture safely 2026-07-31 23:38:34 -07:00
austinpower1258 6e66669792 test: isolate terminal free gate state 2026-07-31 23:28:55 -07:00
austinpower1258 d5c5c3e8ad fix: isolate stuck terminal close teardowns 2026-07-31 23:27:02 -07:00
austinpower1258 40578022e7 test: require surface-scoped native free gate 2026-07-31 23:23:24 -07:00
cmux reload-cloud a605f2eb49 fix: preserve screenshot fallback and overlay semantics 2026-07-31 23:17:02 -07:00
austinpower1258 7fcc434fe6 test: cover stuck close teardown isolation 2026-07-31 23:16:41 -07:00
cmux reload-cloud 5670958972 fix: bound permission-free screenshot compositing 2026-07-31 22:58:40 -07:00
austinpower1258 3758434f05 test: preserve notification parsing after invalid flash color 2026-07-31 22:50:55 -07:00
austinpower1258 287529a038 Merge remote-tracking branch 'origin/main' into feat/configurable-pane-flash-color 2026-07-31 22:45:11 -07:00
cmux reload-cloud cc9196c198 fix: composite own window snapshots without capture permission 2026-07-31 22:44:12 -07:00
austinpower1258 96c6c1f86a test: bound terminal teardown watchdog waits 2026-07-31 22:41:06 -07:00
austinpower1258 4ba55e78b7 Merge remote-tracking branch 'origin/main' into issue-9220-sidebar-click-hang 2026-07-31 22:30:21 -07:00
austinpower1258 77ab34b3a4 fix: move explicit terminal teardown off main actor 2026-07-31 22:25:38 -07:00
austinpower1258 0a42b05f6b test: reproduce main-actor terminal teardown hang 2026-07-31 21:56:12 -07:00
cmux reload-cloud 79f7124312 Merge origin/main into issue-9065-remove-cgwindowlistcreateimage 2026-07-31 21:47:02 -07:00
austinpower1258 31bc556044 test: cover live titles for Dock terminals 2026-07-31 20:59:46 -07:00
Abdulaziz AlbaharandClaude Fable 5 4c62139cc6 tests: fileprivate helpers using file-private StoredShortcut alias
First unit-target compile of this file (gate run) rejected internal methods
whose signatures use the private AppStoredShortcut typealias.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-31 19:44:19 -07:00
Abdulaziz AlbaharandClaude Fable 5 4093b1bf1b tests_v2: replace sleep-then-assert with condition polls in focus-history e2e
check-test-determinism.py --strict flagged the post-close sleep; poll for
selection and close instead.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-31 19:19:54 -07:00
Myk MelezandClaude Fable 5 92fa684218 Document why English ENOENT matching is locale-safe on Darwin
Review asked to pin LC_ALL on the ps invocation because vanishedTTYNames
matches the English strerror(ENOENT) text. Darwin libc ships no localized
message catalogs, so ps emits this exact string under any locale (verified
empirically with LC_ALL=ja_JP.UTF-8); record that constraint instead of
widening the CommandRunning API for an unreachable failure mode.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PDYSKWqKSHrr6MAuMtou9C
2026-07-31 17:21:29 -07:00
Myk MelezandClaude Fable 5 a5dc5afa06 Retire stale ports despite vanished TTYs, privileged owners, and zombies
One fix per mechanism the failing tests pin:

- `runPS` drops the terminals `ps` reports as ENOENT and retries with
  the rest, bounded so a pty churning mid-scan cannot spin. Terminals
  that are all gone report authoritative emptiness — a freed pty can
  hold no process — so their stale badges clear too, while any other
  diagnostic still yields incomplete, which retains ports rather than
  dropping them on weak evidence. Vanished terminals are matched by
  device name, so the two-device diagnostic form (`/dev/ttyX and
  /dev/X`) and TTYs registered by full device path are recognized, and
  "every terminal is gone" outranks the retry budget so authoritative
  emptiness does not depend on which attempt the final pty closed
  during.
- Birth identities are read through `sysctl(KERN_PROC_PID)` instead of
  `proc_pidinfo`. It reports the same birth timestamp for any live
  process regardless of owner, and still reports nothing for an exited
  PID, so recycling detection keeps working and now covers privileged
  PIDs it previously had to guess about. `SZOMB` is rejected
  explicitly: sysctl also describes an exited-but-unreaped process, and
  session restore treats a matching identity as proof the agent is
  alive.
- `PIDPresence` routes through the same process-table read that
  supplies birth identities, so liveness and identity cannot drift
  apart, and an unreaped process reads as absent to every caller
  weighing whether it might still own something.
  `Workspace.agentPIDProcessIdentity(pid:)` reads through that single
  reader as well instead of keeping its own `proc_pidinfo` copy.

The tests added here pin behavior this fix introduces rather than the
original bug: zombie identities reading as absent, diagnostic forms
that must not trigger the retry, and the retry budget staying
incomplete when a TTY never becomes scannable.

Fixes #9152.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016pmrs1n9Z2UKquWcbXhmBe
2026-07-31 16:53:39 -07:00
Myk MelezandClaude Fable 5 6ddcc4c0e2 Add failing tests for ports that can never retire
Sidebar port badges accumulate dead ports for the lifetime of the app
because three independent mechanisms each leave a panel's scan
permanently incomplete, and PortScanSnapshotReconciler treats incomplete
scans as non-evidence that only ever unions ports:

- BSD `ps` aborts an entire batched `-t` query when any listed terminal
  device is gone, so one closed pty makes every panel's port scan look
  incomplete.
- `proc_pidinfo` refuses any process whose effective UID differs from
  ours, so the root-owned `/usr/bin/login` heading every terminal reads
  as unidentifiable, files under `incompletePIDs`, and scores its panel
  incomplete on every scan.
- A zombie answers `kill(pid, 0)` like a running process while holding
  no readable identity — the same incompleteness, reachable through any
  unreaped child, though a zombie has exited and can hold no socket.

An end-to-end test also drives the real scanner — registerTTY, kick,
coalesce, burst, reconcile, publish — and asserts a port is retired once
its process stops listening, since these bugs left every stage passing
its own unit test while the feature was fully broken. Identity and
presence deliberately stay on the real providers: an earlier draft
injected them and passed against the broken tree.

Covers github.com/manaflow-ai/cmux/issues/9152.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016pmrs1n9Z2UKquWcbXhmBe
2026-07-31 16:53:39 -07:00
Abdulaziz AlbaharandClaude Fable 5 81fe1ddd71 Cmd+[ / Cmd+] traverse global workspace focus history; pane cycling becomes rebindable
The Ghostty goto_split:previous/next mirror in the shortcut dispatch now
yields to a bound Focus Back/Forward shortcut (matchConfiguredShortcut,
including shortcuts.when gating), so ⌘[ / ⌘] reach the focus-history branch
and drive the exact same TabManager.navigateBack()/navigateForward() path as
the titlebar arrow buttons: same history model, same closed-workspace
pruning, same enable conditions. Unbinding Focus Back/Forward hands the keys
back to the mirror, as the keyboard-shortcuts docs already promised.

Pane cycling stays available two ways: the Ghostty goto_split trigger on any
non-colliding key, and new cmux-owned rebindable actions focusPreviousPane /
focusNextPane (default unbound) that share the same cyclePaneFocus body, per
the shared-entrypoint policy. The window key-equivalent fallback route gets
the same yield so both dispatch layers agree.

The new actions follow the full shortcut policy: KeyboardShortcutSettings +
CmuxSettings ShortcutAction (defaults, display names, panes group), Settings
recorder rows, cmux.json shortcuts.bindings support, schema enum, web
keyboard-shortcuts page (en+ja), and the shortcut-actions reference. Labels
localized in Localizable.xcstrings for all catalog languages.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-31 10:46:58 -07:00
Abdulaziz AlbaharandClaude Fable 5 e81d4fcfbe Add failing coverage: Cmd+[ / Cmd+] must traverse global workspace focus history
Ghostty's macOS defaults bind goto_split:previous/next to cmd+[ / cmd+], the
same keys as Focus Back/Forward. The shortcut dispatch mirrors those triggers
to cycle pane focus and checks the mirror before the focus-history branch, so
the keys cycle panes inside the current workspace (or do nothing) while the
titlebar arrow buttons navigate across workspaces.

Coverage added ahead of the fix so CI shows red then green:
- cmuxTests/FocusHistoryBracketShortcutRoutingTests: dispatches real ⌘[ / ⌘]
  events through debugHandleCustomShortcut with the Ghostty mirror installed
  via a new DEBUG seam; expects workspace focus-history navigation.
- cmuxUITests/FocusHistoryShortcutUITests: end-to-end over the control socket
  (simulate_shortcut uses the same matcher as the app-level monitor); walks
  back/forward across three workspaces and checks closed-workspace skipping.
- tests_v2/test_focus_history_shortcut_cross_workspace.py: local socket
  verification against a tagged build.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-31 09:33:42 -07:00
ejc3 8358a7df40 cmuxTests: read the saved XDG_STATE_HOME through getenv, not ProcessInfo
ProcessInfo.processInfo.environment is captured once and does not reflect setenv calls
made after first access, so saving the prior value through it and restoring in a defer
writes back a stale snapshot whenever an earlier test changed the variable at runtime.
getenv reads the live value, which is also the level the setenv/unsetenv pair below
operates at.
2026-07-30 23:33:44 -07:00
ejc3 8c30fb259a cmuxTests: restore the browser-profile selection this suite pins
BrowserPanelRemoteStoreTests pins the built-in default profile in setUp because the
selection is persisted in UserDefaults and a leftover profile from another test breaks
its store-scoping checks. Pinning without restoring commits the same offense in the
other direction: every suite that runs after this one inherits the built-in default.
Save the selection in setUp and put it back in tearDown.
2026-07-30 23:33:44 -07:00
ejc3 9e416c0b19 cmuxTests: repair four more suites, including two shortcut tests pinning a retired model
KeyboardShortcutSettingsFileStoreTests asserted the older model where a shortcut
saved through Settings outranks one bound in cmux.json. The product deliberately
went the other way: a file-managed action is authoritative and read-only, with
file-first lookup, refused writes, and a Settings row that reports itself managed
instead of editing. The tests now pin that, which is what they were for.

RemoteTmuxMirrorFeedForwardTests, RenderableSystemSymbolTests and
TabManagerFocusedNotificationIndicatorTests were failing on their own fixtures.

Each run before and after on a macOS builder, on this branch with the sixteen
already here, so the whole set is verified together rather than in isolation.
2026-07-30 23:33:44 -07:00
ejc3 22b5e66ce6 cmuxTests: repair three more suites, one of which was killing the test host
MarkdownPanelTests fulfilled a one-shot XCTestExpectation twice: WebKit can
report a provisional failure and then a finish for a single load, and the second
fulfill() raises XCTest's API-violation NSException from inside a suspended
await fulfillment. That does not fail a test, it takes the shared app host down
and every suite batched with it, so this one was corrupting other suites'
results as well as its own. The load delegate now settles once.

CLIHookNoResponseTests and CmuxDurableDeepLinkRestoreTests were failing on their
own fixtures in the same way as the thirteen already in this branch.

Each verified on a macOS builder before and after, on the combined branch rather
than in isolation, so the three land on top of the existing thirteen with the
whole set re-run.
2026-07-30 23:33:43 -07:00
ejc3 a003053402 cmuxTests: repair thirteen suites that were failing on their own fixtures
Thirteen suites in the pre-existing red set fail for reasons inside the tests,
not the product. Each one here was run before and after on a macOS builder and
goes from a failing verdict to a passing one; no product behavior changes and no
assertion is weakened or deleted.

The recurring shapes:

- Oracles that could be satisfied before the thing under test had happened, so
  the wait returned early and the assertion read startup state. These now read
  live state that only exists after the operation commits.
- Fixtures that could not reach the state their test described, so an assertion
  waited on a condition that was unreachable rather than merely slow.
- Expectations pinned to a machine-dependent value (a resolver path, a config
  directory, a bundled binary layout) instead of deriving it the way the product
  does.
- Shared global state left behind for the next test in the same host.

Verified per suite rather than in bulk: applied to a clean tree off the CmuxGit
compile fix, then each suite run through the app host. All thirteen report a
passing suite verdict with a non-zero test count.
2026-07-30 23:33:37 -07:00
ejc3 f821f78784 browser: fix headless-broken Browser test suites and the bugs they caught
The Browser* suites in cmuxTests had 18 failures across 9 suites under a local
headless `xcodebuild test`. Three were real product bugs the tests had been
catching all along; the rest were tests asserting behavior the product had
deliberately moved away from, or waiting on the wrong signal.

Product fixes:

- A panel constructed with a URL but `renderInitialNavigation: false` kept the
  `.newTab` lifecycle state it was born with. The deferred path returns from
  `init` before any visibility or navigation transition runs, and nothing else
  seeds the state, so a restored deferred tab reported itself as a new tab.
  Seed it in `init` for both the request and URL paths.

- The legacy `browserForcedDarkModeEnabled` migration could never run. Fallback
  registration goes into the process-wide registration domain, so once any panel
  bootstrapped defaults, `browserThemeMode` always resolved to a value and
  `BrowserThemeSettings.mode(defaults:)` took its early return instead of
  migrating. Users upgrading with forced dark mode on silently lost the setting.
  The key does not need a registered fallback: the accessor already falls back to
  `defaultMode` and the SwiftUI binding carries its own default.

- A visible portal slot whose anchor was removed outright kept rendering against
  the dead anchor. The off-window-reparent branch already distinguished an anchor
  that is still parented (drag churn, keep it on screen) from one that is not,
  but the following line preserved the slot unconditionally, so the orphaned case
  never reached the hide-while-retrying path.

Test fixes:

- Under-page background and hidden-discard-delay expectations predated the
  behavior they assert: the terminal color is composited over the window
  background rather than alpha-blended, and an out-of-range stored delay is
  rejected in favor of the default rather than clamped to the maximum.

- The discard tests waited on `webView.isLoading` while the discard gate also
  reads the panel's own `isLoading`, which stays set for the minimum indicator
  duration after WebKit finishes. Wait for the condition the gate actually reads,
  and report the blockers when a discard is refused.

- `waitForBrowserPanel` accepted the omnibar URL, which the panel publishes as
  soon as a navigation is requested and before `isLoading` rises, so it could
  return before the page loaded at all. Wait for the web view's committed URL.

- The remote-store tests assumed the built-in default profile was ambient, but a
  panel without an explicit profile adopts the last-used one, and that selection
  is persisted. Pin the default profile, and delete temporary test profiles so
  they stop accumulating in the shared defaults.

- The portal reveal test still required a visibility change to cycle WebKit's
  `_exitInWindow`/`_enterInWindow` pair, which was removed on purpose because
  cycling it fires visibilitychange and broke the DevTools pane across workspace
  switches. It now asserts that invariant instead.

- The omnibar suggestions hit test built its point by flipping y by hand, but
  `hitTest` takes superview coordinates and the flipped hosting view disagrees
  with its unflipped slot about y. Convert through AppKit and host the slot in a
  window so the SwiftUI overlay answers hit tests.
- `testBackgroundPreloadIsConsumedByInitialNavigation` built an NSWindow with
  AppKit's default `isReleasedWhenClosed` and closed it, so the window was
  over-released and XCTest's memory checker walked the freed object at teardown
  and took the test host down with a SIGSEGV in `objc_release`. The host restart
  was also hiding tests: the suites now report 54 tests instead of 34.
2026-07-30 23:21:53 -07:00
austinpower1258 03f37f60c6 fix: skip invalid screenshot window numbers 2026-07-28 17:08:36 -07:00
austinpower1258 527d503432 test: cover invalid screenshot window numbers 2026-07-28 17:08:06 -07:00
austinpower1258 f1667eabb6 fix: latch compositor after capture timeout 2026-07-28 14:54:31 -07:00
austinpower1258 49bc794003 Merge remote-tracking branch 'origin/main' into issue-9065-remove-cgwindowlistcreateimage 2026-07-28 14:46:20 -07:00
austinpower1258 8ed80bf636 fix: bound composited screenshot failures 2026-07-28 14:41:25 -07:00
austinpower1258 947f4d8584 Merge remote-tracking branch 'origin/main' into issue-9065-remove-cgwindowlistcreateimage 2026-07-28 14:04:26 -07:00
austinpower1258 2b12a95cf5 fix: bound composited screenshot routing 2026-07-28 14:03:30 -07:00
austinpower1258 25e8907da6 fix: type AppKit screenshot fallback 2026-07-28 13:53:26 -07:00
austinpower1258 dbb34c1efc fix: preserve composited window screenshots 2026-07-28 13:51:28 -07:00
austinpower1258 f584f09353 Merge remote-tracking branch 'origin/main' into issue-9065-remove-cgwindowlistcreateimage 2026-07-28 13:37:52 -07:00
austinpower1258 2c955f6a86 test: wait for rendered screenshot content 2026-07-28 13:14:57 -07:00
austinpower1258 e8d36004e2 test: match XCTest activation assertion 2026-07-28 13:06:34 -07:00
austinpower1258 ba63392574 test: harden screenshot regression setup 2026-07-28 12:50:14 -07:00
austinpower1258 cdc7e50c0e fix: replace legacy window screenshot capture 2026-07-28 12:47:23 -07:00
austinpower1258 26f31b7306 test: tolerate background UI test launch 2026-07-28 12:39:58 -07:00
austinpower1258 b9b51b2b6d test: cover window screenshot socket output 2026-07-28 12:20:22 -07:00
lawrencecchen 6c7bdeab2e fix(tui-sdk): make clean CI consumers portable 2026-07-27 21:16:05 -07:00
lawrencecchen 2afaca5883 test(tui-sdk): cover clean SDK consumers 2026-07-27 20:31:41 -07:00
mcorcelleandClaude Opus 4.8 7aeb4e3a60 Address review: strict hex validation, matching preview, alpha test
- resolvedColor now checks the `#` prefix and 7-character length before
  NSColor(hex:), which tolerates a missing prefix. Keeps the runtime in
  step with the schema's `colorHexOrNull` (#RRGGBB, no alpha).
- colorRow takes a `fallback` preview color, defaulting to the existing
  cmuxAccentColor(). Pane Flash passes systemBlue so the settings swatch
  shows the color the ring actually renders when unset.
- Adds testFlashColorFallsBackWhenHexCarriesAlpha covering #RRGGBBAA.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-27 14:03:52 +02:00
lawrencecchen 745b82a353 fix(tui-sdk): preserve JSON field presence 2026-07-26 22:23:33 -07:00
lawrencecchen 3a3e7a936d feat(tui): generate and verify seven language SDKs 2026-07-26 21:36:54 -07:00
lawrencecchen 92264853ca Merge main into TUI programmability inventory 2026-07-26 20:02:47 -07:00
lawrencecchen cc4ead3acb fix(tui): harden inventory authority contracts 2026-07-25 20:13:23 -07:00
lawrencecchen ac8dcf9f1f test(tui): cover external review hardening gaps 2026-07-25 20:07:27 -07:00
lawrencecchen 237f2a85f7 fix(tui): make action and event contracts truthful 2026-07-25 19:46:25 -07:00
lawrencecchen dd2a0d0261 test(tui): cover review contract gaps 2026-07-25 19:45:09 -07:00
lawrencecchen e8bb839066 fix(tui): derive menu inventory from runtime metadata 2026-07-25 19:25:45 -07:00
lawrencecchen 4d4df2e9d9 test(tui): require runtime-owned menu metadata 2026-07-25 19:22:28 -07:00
lawrencecchen 0632b61a32 fix(tui): align runtime and protocol contracts 2026-07-25 19:06:57 -07:00
lawrencecchen eb69ebd76e test(tui): cover autoreview contract regressions 2026-07-25 19:05:18 -07:00
lawrencecchen b02a3c6f4a fix(tui): derive inventory from runtime metadata 2026-07-25 18:43:19 -07:00
lawrencecchen 373410b4ee test(tui): require runtime-owned inventory metadata 2026-07-25 18:32:51 -07:00
lawrencecchen 60ca64ba49 fix(tui): exclude comments from event discovery 2026-07-25 18:24:22 -07:00
lawrencecchen 10a03284b6 test(tui): ignore comment-only wire events 2026-07-25 18:22:32 -07:00
lawrencecchen bd4bbc2b79 docs(tui): enforce authority and stream routes 2026-07-25 18:18:08 -07:00
lawrencecchen 24c53b3c53 test(tui): cover inventory routing contracts 2026-07-25 18:15:48 -07:00
lawrencecchen ba9b19e564 docs(tui): correct short ID example 2026-07-25 18:10:21 -07:00
lawrencecchen 4075b1b90b docs(tui): make current protocol risks explicit 2026-07-25 18:03:27 -07:00
lawrencecchen 3c1340ad10 docs(tui): resolve programmability review gaps 2026-07-25 18:02:14 -07:00
lawrencecchen ab864f093a test(tui): cover trailing enum variants 2026-07-25 17:59:07 -07:00
lawrencecchen d898471b3f Merge remote-tracking branch 'origin/main' into task-cmux-tui-spec-completeness 2026-07-25 17:55:45 -07:00
ejc3 4a11af6f08 cmuxTests: give the CLI mock servers an owned lifecycle and one shared loop
Follow-up cleanup on the mock control-socket rework.

The accept loops had no way to stop. Closing the listener FD does not wake a
thread already parked in poll/accept on Darwin, so every server leaked its
thread for the life of the test process — worse than the old bounded loops,
which at least self-terminated once they had accepted their quota. The registry
now owns each loop: it pairs the listener with a private stop pipe, and
`stop(listenerFD:)`/`stopAll()` signal the loop and join it. Both suites reap
their loops in tearDown.

Registry hardening:
- Hold the lock across retire-old and register-new. Two concurrent starts on one
  FD could each observe the same predecessor, each wait for it, then each spawn a
  loop, putting two loops back on one listener — the stealing bug returning by
  another door. The loop threads never take the lock (they only signal
  completion), so holding it across the join can't deadlock.
- The registry, not the loop, owns the stop pipe for the loop's whole life, so a
  stop byte can never land in an unrelated descriptor that reused the number.
- Check pipe(); without a stop pipe a loop would be unstoppable, so fail loudly
  rather than start one. A loop that ignores its stop byte now fails the test
  instead of being left running.

Consolidation:
- One `cliMockServeLineFramedConnection` reader replaces four copies of the
  read/frame/respond loop, and one `cliMockWriteAll` replaces the duplicated
  partial-write handling. `CLIMockOnceFlag` replaces the two identical latches.
- Drop `connectionCount`/`connectionLimit` from the servers that no longer bound
  connections, along with the dead default, and rewrite the comments that still
  described a fixed pool. Same-named helpers owned by other suites keep their
  live parameters.
2026-07-24 21:57:33 -07:00
ejc3 71eea9f69f cmuxTests: make CLI mock control sockets headless-robust; refresh vm-new expectations
The CLI hook integration suites drive the bundled cmux helper as a subprocess
against a mock control socket. Headless (piped stdio, no controlling TTY) the
helper always falls back to a `system.top` agent-process lookup on a second,
dedicated control connection because caller-TTY resolution can't succeed. The
mocks accepted only one connection, so that extra connection was starved: hooks
stalled for the 2s socket timeout, resolution fell back to unresolved routing,
and assertions saw the wrong RPC sequence (or empty output after a 5s process
timeout).

Rework the mock accept path so every connection the helper opens is serviced:

- A single poll-based accept loop per listener FD dispatches each connection to
  its own handler, and a new server on the same FD supersedes (stops and joins)
  the previous one so a leftover loop can't steal the next hook's connection and
  fulfill the wrong expectation.
- The loops run on raw threads instead of GCD queues. A blocking accept() parked
  on a GCD worker ties it up for the whole test; a suite that opens a server per
  hook drained the shared GCD pool that runProcess needs for its stdout/stderr
  readers and exit waiter, which looked exactly like the helper hanging.

Also:
- Bind the tmux-compat-env test's control socket under a short /tmp path. The
  AF_UNIX sun_path limit is 104 bytes and this machine's temp dir alone overflows
  a socket nested under it.
- Update the default-freestyle vm-new tests to expect vm.attach_info: `vm new`
  uses forceSSH:false, which resolves through vm.attach_info (already covered by
  the SSH startup suites), not the older vm.ssh_info path.
2026-07-24 21:57:33 -07:00
lawrencecchen 76af0dc225 docs(tui): enforce programmability inventory 2026-07-24 03:00:19 -07:00
austinpower1258 80723789d0 Merge remote-tracking branch 'origin/main' into fix/atd-sidebar-link-click-8596
# Conflicts:
#	Packages/macOS/CmuxGit/Sources/CmuxGit/Probe/GitHubPullRequestRequestCoordinator.swift
2026-07-21 18:53:13 -07:00
austinpower1258 83b3f31a02 Restrict sidebar description link schemes 2026-07-21 18:44:20 -07:00
austinpower1258 53880f2426 Cover wrapped sidebar description links 2026-07-21 18:14:33 -07:00
austinpower1258 00ab332503 Address sidebar link review feedback 2026-07-21 17:56:39 -07:00
austinpower1258 7776008d6a Tighten sidebar description link hit bounds 2026-07-21 17:48:11 -07:00
austinpower1258 acdd03d1c1 Open AppKit sidebar description links 2026-07-21 17:30:06 -07:00
austinpower1258 87e8565e1a Add sidebar description link click regression 2026-07-21 17:30:02 -07:00
austinpower1258 cee2d91102 Fix CmuxGit coordinator initializer 2026-07-21 17:29:54 -07:00
mcorcelleandClaude Opus 4.8 6725ac6502 Make pane flash and attention ring color configurable
The pane attention flash and unread notification ring were hardcoded to
systemBlue via a single-case accent enum, so the one element that signals
"this pane needs input" could not be themed while everything around it
(workspace badge, selection highlight, terminal theme) could.

Add `notifications.paneFlashColor`, a nullable hex reusing the existing
`colorHexOrNull` schema def and `parseNullableHex` validation. Null keeps
the built-in systemBlue, so default appearance is unchanged.

Because every consumer resolves through `presentation.accent.strokeColor`,
resolving the accent from settings covers the notification ring, the flash,
the tmux pane overlay and the SwiftUI ring view in one place.

Surfaced in Settings under Workspace Colors, directly below Notification
Badge, using the existing colorRow helper.

Refs #8560

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-21 16:53:43 +02:00
Owen JohnsonandClaude Fable 5 7b159391e7 fix(control-socket): validate split orientation and de-flake tree layout test
Addresses bot-review findings on #7459.

- Type the split `orientation` as a two-case enum (horizontal|vertical)
  instead of an unvalidated String. An orientation outside the wire
  contract now fails closed at the conversion boundary (node -> nil ->
  layout: null), the same path as an unparseable pane UUID. Emitted wire
  JSON is unchanged for the two valid values (rawValue == the wire string).
- Fix the layout-conversion doc comment to describe what the code does: a
  single unparseable leaf nils the ENTIRE workspace layout (deliberate
  fail-closed; consumers fall back to the flat panes array), not just the
  affected subtree. No behavior change.
- Replace the fixed time.sleep(1.0) in test_cli_tree_layout with a bounded
  poll on `cmux --json tree` until the created workspaces materialize
  (racy under CI load); reuses the existing _workspace_from_tree predicate.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-08 13:15:15 -04:00
owenjohnsonandClaude Opus 4.8 ad34dc6991 feat(control-socket): emit split-layout geometry in system.tree
`system.tree`'s `panes` array is flat — it lists a workspace's panes but
discards how they're arranged: which splits are vertical vs horizontal,
their ratios, and their nesting. That geometry is live in the workspace's
bonsplit controller (`treeSnapshot()`) but never reached the wire, so a
consumer that recreates a saved layout (e.g. an external snapshot/restore
tool) could only guess — multi-pane restores came out flat/horizontal.

Add an additive `layout` field to each workspace in the `system.tree`
payload: the split tree in the shape `--layout` already accepts
({direction, split, children} for a split; a {pane: {id, ref}} leaf
otherwise), so the tree cmux emits and the layout it ingests are one
schema read two ways. Pane leaves carry the same id/ref as the flat
`panes` array and participate in --id-format (refs/uuids/both). The field
is `null` when unavailable, so existing consumers are unaffected.

- New ControlSystemTreeLayoutNode (keeps the socket package free of any
  Bonsplit dependency; the app maps ExternalTreeNode into it at capture)
- Capture treeSnapshot() at the existing serializer call site
- Serialize in systemTreeWorkspacePayload; the CLI relays it unchanged
  (treeApplyMarkers passes unknown keys through)
- tests/test_cli_tree_layout.py: single-leaf + nested H-over-V round-trip

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-06 08:27:38 -04:00
3261 changed files with 429032 additions and 44881 deletions
+1
View File
@@ -19,4 +19,5 @@ self-hosted-runner:
# Linux: Blacksmith primary (LINUX_RUNNER), WarpBuild overflow fallback.
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-8vcpu-ubuntu-2404
- blacksmith-32vcpu-ubuntu-2404
- warp-ubuntu-latest-x64-4x
@@ -0,0 +1,57 @@
name: Set up cmux-tui Rust
description: Install the repository-pinned cmux-tui Rust toolchain.
runs:
using: composite
steps:
- name: Install pinned Rust toolchain
shell: bash
env:
TOOLCHAIN_FILE: ${{ github.action_path }}/../../../cmux-tui/rust-toolchain.toml
run: |
if command -v python3 >/dev/null 2>&1; then
python_cmd=python3
elif command -v python >/dev/null 2>&1; then
python_cmd=python
else
echo "::error::Python is required to read the pinned Rust toolchain" >&2
exit 1
fi
"$python_cmd" <<'PY'
import os
import pathlib
import re
import subprocess
contents = pathlib.Path(os.environ["TOOLCHAIN_FILE"]).read_text(encoding="utf-8")
def value(name):
match = re.search(rf'^\s*{name}\s*=\s*"([^"]+)"', contents, re.MULTILINE)
if match is None:
raise SystemExit(f"missing {name} in cmux-tui/rust-toolchain.toml")
return match.group(1)
channel = value("channel")
profile = value("profile")
components_match = re.search(r"^\s*components\s*=\s*\[([^]]*)\]", contents, re.MULTILINE)
if components_match is None:
raise SystemExit("missing components in cmux-tui/rust-toolchain.toml")
components = re.findall(r'"([^"]+)"', components_match.group(1))
command = ["rustup", "toolchain", "install", channel, "--profile", profile]
for component in components:
command.extend(["--component", component])
subprocess.run(command, check=True)
subprocess.run(["rustup", "default", channel], check=True)
cargo = subprocess.run(
["rustup", "which", "cargo"],
check=True,
capture_output=True,
text=True,
).stdout.strip()
with open(os.environ["GITHUB_PATH"], "a", encoding="utf-8") as path_file:
print(pathlib.Path(cargo).parent, file=path_file)
PY
cargo --version
rustc --version
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Run each cmux-tui-core Rust test in a fresh process."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import subprocess
import sys
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("cargo_messages", type=Path)
parser.add_argument("core_root", type=Path)
return parser.parse_args()
def is_below(path: Path, root: Path) -> bool:
try:
path.relative_to(root)
except ValueError:
return False
return True
def test_binaries(cargo_messages: Path, core_root: Path) -> list[Path]:
binaries: dict[Path, set[str]] = {}
with cargo_messages.open(encoding="utf-8") as messages:
for line in messages:
try:
message = json.loads(line)
except json.JSONDecodeError:
continue
if message.get("reason") != "compiler-artifact":
continue
if not message.get("profile", {}).get("test"):
continue
target = message.get("target", {})
kinds = set(target.get("kind", []))
if not kinds.intersection({"lib", "test"}):
continue
source = target.get("src_path")
executable = message.get("executable")
if not source or not executable:
continue
if not is_below(Path(source).resolve(), core_root):
continue
binaries.setdefault(Path(executable).resolve(), set()).update(kinds)
library_binaries = [path for path, kinds in binaries.items() if "lib" in kinds]
if len(library_binaries) != 1:
raise SystemExit(
"expected one cmux-tui-core library test binary, "
f"found {len(library_binaries)}"
)
if not binaries:
raise SystemExit("cargo did not report any cmux-tui-core test binaries")
return sorted(binaries)
def tests_in(binary: Path) -> list[str]:
result = subprocess.run(
[str(binary), "--list"],
check=True,
stdout=subprocess.PIPE,
text=True,
)
tests = []
for line in result.stdout.splitlines():
name, separator, kind = line.rpartition(": ")
if separator and kind == "test":
tests.append(name)
if not tests:
raise SystemExit(f"{binary.name} did not list any tests")
if len(tests) != len(set(tests)):
raise SystemExit(f"{binary.name} listed duplicate test names")
return tests
def main() -> int:
args = parse_args()
core_root = args.core_root.resolve()
binaries = test_binaries(args.cargo_messages, core_root)
total = 0
for binary in binaries:
tests = tests_in(binary)
print(f"Running {len(tests)} tests from {binary.name} in fresh processes")
for index, test_name in enumerate(tests, start=1):
print(f"[{index}/{len(tests)}] {test_name}", flush=True)
subprocess.run(
[str(binary), test_name, "--exact", "--test-threads=1"],
check=True,
)
total += 1
print(f"Passed {total} isolated cmux-tui-core tests")
return 0
if __name__ == "__main__":
sys.exit(main())
+14 -7
View File
@@ -10,10 +10,10 @@ concurrency:
jobs:
build-ghosttykit:
runs-on: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
timeout-minutes: 20
timeout-minutes: 35
env:
GHOSTTYKIT_CRASH_REPORT_SUBDIR: cmux/crash
GHOSTTYKIT_BUILD_FLAVOR: crashsubdir-cmux-crash-v1
GHOSTTYKIT_BUILD_FLAVOR: crashsubdir-cmux-crash-sentry-off-v1
steps:
- name: Clear stale git locks (self-hosted reused workspace)
shell: bash
@@ -54,7 +54,6 @@ jobs:
fi
- name: Select Xcode
if: steps.check-release.outputs.exists == 'false'
run: |
set -euo pipefail
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
@@ -78,7 +77,6 @@ jobs:
xcodebuild -version
- name: Cache Zig packages
if: steps.check-release.outputs.exists == 'false'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/zig
@@ -86,16 +84,25 @@ jobs:
restore-keys: zig-packages-
- name: Install zig
if: steps.check-release.outputs.exists == 'false'
run: |
set -euo pipefail
./scripts/install-zig-ci.sh
- name: Test Ghostty OS opener stderr reader
run: |
set -euo pipefail
cd ghostty
zig build test \
-Dapp-runtime=none \
-Demit-macos-app=false \
-Dsentry=false \
-Dtest-filter="open stderr reader exits"
- name: Build GhosttyKit.xcframework
if: steps.check-release.outputs.exists == 'false'
run: |
set -euo pipefail
cd ghostty && zig build -Dcrash-report-subdir="$GHOSTTYKIT_CRASH_REPORT_SUBDIR" -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast
cd ghostty && zig build -Dcrash-report-subdir="$GHOSTTYKIT_CRASH_REPORT_SUBDIR" -Dsentry=false -Demit-xcframework=true -Demit-macos-app=false -Dxcframework-target=universal -Doptimize=ReleaseFast
- name: Package xcframework
if: steps.check-release.outputs.exists == 'false'
@@ -121,6 +128,6 @@ jobs:
--repo manaflow-ai/ghostty \
--target "${{ steps.ghostty-sha.outputs.sha }}" \
--title "GhosttyKit xcframework (${{ steps.ghostty-sha.outputs.sha }}, ${GHOSTTYKIT_BUILD_FLAVOR})" \
--notes "Pre-built GhosttyKit.xcframework for commit ${{ steps.ghostty-sha.outputs.sha }} with crash-report-subdir=${GHOSTTYKIT_CRASH_REPORT_SUBDIR}" \
--notes "Pre-built GhosttyKit.xcframework for commit ${{ steps.ghostty-sha.outputs.sha }} with crash-report-subdir=${GHOSTTYKIT_CRASH_REPORT_SUBDIR} and sentry=false" \
GhosttyKit.xcframework.tar.gz
echo "Published release $TAG"
+23 -11
View File
@@ -9,25 +9,37 @@ jobs:
fail-fast: false
matrix:
include:
- os: macos-14-large
- os: macos-14
timeout: 60
run_unit_tests: false
run_mobile_transport_tests: true
startup_smoke: true
virtual_display: true
skip_zig: false
expected_arch: arm64
expected_os_major: "14"
- os: macos-15-intel
timeout: 90
run_unit_tests: false
run_mobile_transport_tests: true
startup_smoke: true
virtual_display: true
skip_zig: false
expected_arch: x86_64
expected_os_major: "14"
expected_os_major: "15"
- os: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
timeout: 60
run_unit_tests: true
timeout: 120
run_unit_tests: false
run_mobile_transport_tests: true
startup_smoke: true
virtual_display: true
skip_zig: false
expected_arch: ""
expected_os_major: ""
- os: ${{ vars.MACOS_RUNNER_26 || 'blacksmith-6vcpu-macos-26' }}
timeout: 60
run_unit_tests: true
timeout: 120
run_unit_tests: false
run_mobile_transport_tests: true
startup_smoke: true
virtual_display: false
skip_zig: true # zig 0.15.2 MachO linker can't resolve libSystem on macOS 26
@@ -95,7 +107,7 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: GhosttyKit.xcframework
key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }}
key: ghosttykit-sentry-off-v1-${{ steps.ghostty-revision.outputs.sha }}
- name: Download pre-built GhosttyKit.xcframework
if: steps.cache-ghosttykit.outputs.cache-hit != 'true'
@@ -147,14 +159,14 @@ jobs:
sleep $((attempt * 5))
done
- name: Run mobile transport package tests on Intel Sonoma
if: matrix.expected_arch == 'x86_64'
- name: Run mobile transport package tests on compatibility hosts
if: matrix.run_mobile_transport_tests
run: |
./scripts/ci/run-swift-testing-suites.sh Packages/Shared/CMUXMobileCore
./scripts/ci/run-swift-testing-suites.sh Packages/Shared/CmuxIrohTransport
- name: Run mobile transport tests in x86_64 iOS Simulator on Intel Sonoma
if: matrix.expected_arch == 'x86_64'
- name: Run mobile transport tests in the compatibility iOS Simulator
if: matrix.run_mobile_transport_tests
env:
CMUX_EXPECTED_SIMULATOR_ARCH: ${{ matrix.expected_arch }}
run: ./scripts/ci/run-iroh-ios-simulator-package-tests.sh
+167 -8
View File
@@ -124,6 +124,9 @@ jobs:
- name: Install workflow guard Python dependencies
run: python3 -m pip install --disable-pip-version-check --no-input PyYAML==6.0.3 bashlex==0.18
- name: Validate Blacksmith Testbox broker trust boundary
run: python3 tests/test_ci_testbox_broker_guard.py
- name: Validate nightly prune Python compatibility
run: PYTHON_BIN=python3.9 bash ./tests/test_ci_nightly_prune_python_compat.sh
@@ -151,6 +154,18 @@ jobs:
- name: Validate app-host xcodebuild attempt budget
run: ./tests/test_ci_app_host_xcodebuild_attempts.sh
- name: Validate app-host user configuration isolation
run: python3 tests/test_ci_app_host_home_isolation.py
- name: Validate app-host identity and cleanup confirmation
run: bash tests/test_ci_app_host_identity.sh
- name: Validate app-host process receipts
run: bash tests/test_ci_app_host_processes.sh
- name: Validate isolated app-host home cleanup
run: bash tests/test_ci_app_host_home_cleanup.sh
- name: Validate cmux profiling support scripts
run: ./tests/test_start_cmux_profiling.sh
@@ -175,6 +190,9 @@ jobs:
- name: Validate cmux scheme test configuration
run: ./tests/test_ci_scheme_testaction_debug.sh
- name: Validate selected iOS test execution guard
run: python3 tests/test_ios_selected_test_execution.py
- name: Validate cmuxTests sharding
run: |
python3 scripts/ci/cmux_unit_test_shard.py --validate
@@ -243,6 +261,9 @@ jobs:
- name: Validate pbxproj test-wiring lint
run: ./tests/test_ci_pbxproj_test_wiring.sh
- name: Initialize Bonsplit for deferred-work ownership guard
run: git submodule update --init --depth 1 vendor/bonsplit
- name: Validate stored DispatchWorkItem ownership
run: |
python3 tests/test_lint_stored_dispatch_work_items.py
@@ -255,7 +276,10 @@ jobs:
run: python3 scripts/check-workspace-package-groups.py --check
- name: Validate SwiftPM lockfile policy
run: python3 scripts/check-package-resolved-policy.py
run: |
python3 tests/test_package_resolved_policy_remote_inputs.py
python3 tests/test_check_package_resolved_policy.py
python3 scripts/check-package-resolved-policy.py
- name: Validate bash shell integration job control
run: python3 tests/test_bash_integration_no_done_notifications.py
@@ -263,6 +287,9 @@ jobs:
- name: Validate sidebar lazy-layout guard
run: python3 tests/test_ci_sidebar_lazy_layout_guard.py
- name: Validate focused Dock shortcut routing guard
run: python3 tests/test_dock_shortcut_routing_guard.py
- name: Validate bash prompt bootstrap composes with user PROMPT_COMMAND (starship)
run: python3 tests/test_issue_5164_starship_prompt_composition.py
@@ -321,6 +348,12 @@ jobs:
- name: Web tests
run: bun run test
- name: Install browser for instant navigation tests
run: bunx playwright install --with-deps chromium
- name: Instant navigation tests
run: bun run test:instant
# Checks for in-app React webviews (currently the diff viewer; more cmux React
# surfaces will live alongside it).
react-apps-check:
@@ -466,6 +499,10 @@ jobs:
matrix:
shard: [1, 2, 3, 4]
env:
# This independent job-level marker makes every app-host wrapper fail
# closed if a setup step or environment handoff loses either redirect.
CMUX_CI_APP_HOST_ISOLATION_REQUIRED: "1"
CMUX_APP_HOST_SHARD: ${{ matrix.shard }}
CMUX_CI_XCODE_APP: ${{ vars.CMUX_CI_XCODE_APP_MACOS_15 }}
CMUX_CI_REQUIRED_MACOS_SDK_MAJOR: "26"
CMUX_SKIP_ZIG_BUILD: "1"
@@ -510,7 +547,7 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: GhosttyKit.xcframework
key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }}
key: ghosttykit-sentry-off-v1-${{ steps.ghostty-revision.outputs.sha }}
- name: Download pre-built GhosttyKit.xcframework
if: steps.cache-ghosttykit.outputs.cache-hit != 'true'
@@ -539,6 +576,9 @@ jobs:
mkdir -p "$DERIVED_DATA_PATH"
echo "CMUX_DERIVED_DATA_PATH=$DERIVED_DATA_PATH" >> "$GITHUB_ENV"
- name: Prepare isolated app-host home
run: scripts/ci/prepare-app-host-home.sh
- name: Resolve Swift packages
run: |
set -euo pipefail
@@ -597,6 +637,29 @@ jobs:
echo "::warning::Passwordless sudo unavailable; XCTest will use its default automation-mode setup"
fi
- name: Run bundled command PATH regression
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
# The tolerant full-suite step accepts ordinary Swift Testing failures.
# Keep the shell-resolution integration test non-tolerant so losing
# cmux's bundled commands from PATH cannot pass a shard.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
if ! command -v fish >/dev/null 2>&1; then
HOMEBREW_NO_AUTO_UPDATE=1 brew install fish
fi
command -v fish >/dev/null 2>&1
scripts/ci/run-in-console-session.sh \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
CMUX_SKIP_ZIG_BUILD=1 \
-only-testing:cmuxTests/CmuxBundledBinPathIntegrationTests \
test
- name: Run agent chat transcript lifecycle regressions
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
@@ -662,6 +725,27 @@ jobs:
-only-testing:cmuxTests/GhosttySurfaceOverlayTests/testFiveTabRendererFootprintReturnsToOneRendererTargetAcrossHideRevealCycles \
test
- name: Run PTY spawn starvation regressions
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
# The tolerant full-suite step can accept ordinary assertion
# failures. Keep the issue #9769 coverage — background-prime work
# must exclude never-startable surfaces, and `cmux send` must
# surface queued delivery — on a non-tolerant focused invocation.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
scripts/ci/run-in-console-session.sh \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
CMUX_SKIP_ZIG_BUILD=1 \
-only-testing:cmuxTests/BackgroundPrimeStartableSurfaceTests \
-only-testing:cmuxTests/CLISendQueuedOutputTests \
test
- name: Run notification routing regressions
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
@@ -894,6 +978,45 @@ jobs:
-only-testing:cmuxTests/NotificationScrollRestoreRecoveryTests \
test
- name: Run sidebar workspace-switch layout regression
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
# The tolerant full-suite step can accept Swift Testing assertion
# failures when XCTest reports "0 unexpected". Keep #9612's real
# window-switch workload non-tolerant so NSHostingView layout
# reentry cannot silently return.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
scripts/ci/run-in-console-session.sh \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
CMUX_SKIP_ZIG_BUILD=1 \
-only-testing:cmuxTests/SidebarWorkspaceSwitchLayoutFaultTests \
test
- name: Run terminal portal visibility regressions
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
# These tests mutate the process-wide terminal portal registry and
# include Swift Testing assertions that the tolerant broad shard can
# otherwise classify as expected. Give them a dedicated app host.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
scripts/ci/run-in-console-session.sh \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
CMUX_SKIP_ZIG_BUILD=1 \
-only-testing:cmuxTests/GhosttyTerminalViewVisibilityPolicyTests \
test
- name: Run unit tests
run: |
set -euo pipefail
@@ -1044,6 +1167,7 @@ jobs:
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_socket_autodiscovery.py
python3 tests/test_codex_wrapper_resume_hooks.py
python3 tests/test_claude_wrapper_hooks.py
python3 tests/test_hermes_wrapper_hooks.py
python3 tests/test_claude_wrapper_mutual_shim_loop.py
python3 tests/test_claude_wrapper_shim_root_survives_tmpdir_change.py
python3 tests/test_claude_wrapper_user_binary_resolution.py
@@ -1059,9 +1183,11 @@ jobs:
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_omo_fallback_path.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_omx_fallback_path.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_omc_fallback_path.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_issue_8743_path_directory_shadowing.py
python3 tests/test_issue_2448_shell_claude_wrapper_dispatch.py
python3 tests/test_issue_8093_ghostty_ssh_binary_path.py
python3 tests/test_issue_6714_zsh_shim_noclobber.py
python3 tests/test_issue_9356_bash_shim_noclobber.py
python3 tests/test_issue_8953_zsh_prompt_wrap_guard.py
python3 tests/test_shell_git_branch_stale_cwd.py
python3 tests/test_shell_git_config_remote_url_parsing.py
@@ -1079,9 +1205,15 @@ jobs:
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_pi_extension_install.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_pi_extension_dispatch.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_pi_compacted_feed.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_codex_permission_prompt_notification.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_omp_extension_install.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_omp_subagent_lifecycle.py
CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_campfire_extension_install.py
- name: Clean up isolated app-host home
if: ${{ always() }}
run: scripts/ci/run-in-console-session.sh scripts/ci/cleanup-app-host-home.sh
tests:
name: tests
# Aggregate gate for the test/build suites in this workflow. Required by
@@ -1221,6 +1353,7 @@ jobs:
name: cmux-ghostty-cli-helper
path: ghostty-cli-helper/ghostty
if-no-files-found: error
retention-days: 1
- name: Retry universal Ghostty CLI helper upload
if: steps.upload-ghostty-cli-helper.outcome == 'failure'
@@ -1229,6 +1362,7 @@ jobs:
name: cmux-ghostty-cli-helper
path: ghostty-cli-helper/ghostty
if-no-files-found: error
retention-days: 1
overwrite: true
- name: Select Xcode
@@ -1246,7 +1380,7 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: GhosttyKit.xcframework
key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }}
key: ghosttykit-sentry-off-v1-${{ steps.ghostty-revision.outputs.sha }}
- name: Validate cached GhosttyKit.xcframework
id: validate-ghosttykit-package-tests
@@ -1269,6 +1403,29 @@ jobs:
run: |
./scripts/install-rust-ci.sh
- name: Run Bonsplit package tests
run: |
set -euo pipefail
# Blacksmith macOS runners intermittently abort a package's test
# runner at startup. Retry exactly once only for the known signal
# 5/6 crash immediately after build and before test output, matching
# the package loop below.
test_status=0
output="$(swift test --package-path vendor/bonsplit 2>&1)" || test_status=$?
printf '%s\n' "$output"
if [ "$test_status" -ne 0 ] \
&& printf '%s\n' "$output" | grep -Fq 'Build complete!' \
&& printf '%s\n' "$output" | grep -Eq 'Exited with unexpected signal code [56]([^0-9]|$)' \
&& ! printf '%s\n' "$output" | grep -Eq '^(Test Suite|Test Case|◇ |↳ |✔ |✘ )'; then
echo "Test runner crashed at startup (runner flake); retrying Bonsplit once."
test_status=0
output="$(swift test --package-path vendor/bonsplit 2>&1)" || test_status=$?
printf '%s\n' "$output"
fi
if [ "$test_status" -ne 0 ]; then
exit "$test_status"
fi
- name: Run Swift package unit tests
run: |
set -euo pipefail
@@ -1305,6 +1462,7 @@ jobs:
CmuxControlSocket
CmuxFoundation
CmuxGit
CmuxNotifications
CmuxSettings
CmuxSettingsUI
CmuxTerminal
@@ -1354,13 +1512,14 @@ jobs:
# test runner at startup (signal 5/6 immediately after "Build
# complete!", zero test output). That is a runner flake, not a
# test failure: retry exactly once, and only when no test
# failures were reported.
# output was emitted.
test_status=0
output="$(swift test --package-path "$pkgdir" 2>&1)" || test_status=$?
printf '%s\n' "$output"
if [ "$test_status" -ne 0 ] \
&& printf '%s\n' "$output" | grep -q 'Exited with unexpected signal code' \
&& ! printf '%s\n' "$output" | grep -Eq 'with [1-9][0-9]* failures?'; then
&& printf '%s\n' "$output" | grep -Fq 'Build complete!' \
&& printf '%s\n' "$output" | grep -Eq 'Exited with unexpected signal code [56]([^0-9]|$)' \
&& ! printf '%s\n' "$output" | grep -Eq '^(Test Suite|Test Case|◇ |↳ |✔ |✘ )'; then
echo "Test runner crashed at startup (runner flake); retrying $pkg once."
test_status=0
output="$(swift test --package-path "$pkgdir" 2>&1)" || test_status=$?
@@ -1558,7 +1717,7 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: GhosttyKit.xcframework
key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }}
key: ghosttykit-sentry-off-v1-${{ steps.ghostty-revision.outputs.sha }}
- name: Download pre-built GhosttyKit.xcframework
if: steps.cache-ghosttykit-lag.outputs.cache-hit != 'true'
@@ -1879,7 +2038,7 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: GhosttyKit.xcframework
key: ghosttykit-${{ steps.ghostty-revision.outputs.sha }}
key: ghosttykit-sentry-off-v1-${{ steps.ghostty-revision.outputs.sha }}
- name: Download pre-built GhosttyKit.xcframework
if: steps.cache-ghosttykit-release.outputs.cache-hit != 'true'
+21 -1
View File
@@ -140,9 +140,29 @@ jobs:
legacy_assets="assets-legacy"
mkdir -p "$legacy_assets"
for file in assets/cmux-tui/cmux-tui-*; do
if [[ "$(basename "$file")" == cmux-tui-hook-* ]]; then
continue
fi
cp "$file" "$legacy_assets/$(basename "$file" | sed 's/^cmux-tui-/cmux-mux-/')"
done
cp assets/cmux-tui/manifest.json "$legacy_assets/manifest.json"
python3 - "$GITHUB_SHA" "$legacy_assets" <<'PY'
import hashlib
import json
import pathlib
import sys
from datetime import datetime, timezone
root = pathlib.Path(sys.argv[2])
files = sorted(path for path in root.glob("cmux-mux-*") if path.is_file())
manifest = {
"commit": sys.argv[1],
"builtAt": datetime.now(timezone.utc).isoformat(),
"binaries": {
path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in files
},
}
(root / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
PY
publish_legacy_prefix() {
local prefix="$1"
local cache="$2"
+235 -78
View File
@@ -27,6 +27,36 @@ on:
required: false
default: false
type: boolean
target_set:
description: "Binary targets to build: all or macos-arm64"
required: false
default: all
type: string
build_cloudflare_relay:
description: "Build and verify the Cloudflare relay"
required: false
default: true
type: boolean
verify_linux_arm64:
description: "Run package entrypoint verification on native Linux ARM64"
required: false
default: true
type: boolean
macos_runner:
description: "Optional macOS runner label override"
required: false
default: ""
type: string
linux_runner:
description: "Optional Linux x64 runner label override"
required: false
default: ""
type: string
windows_runner:
description: "Optional Windows runner label override"
required: false
default: ""
type: string
checkout_ref:
description: "Optional git ref to build instead of the caller ref"
required: false
@@ -35,44 +65,114 @@ on:
permissions: {}
env:
RUST_TOOLCHAIN: "1.95.0"
jobs:
plan-build:
name: select binary targets
runs-on: ${{ inputs.linux_runner != '' && inputs.linux_runner || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 5
outputs:
matrix: ${{ steps.targets.outputs.matrix }}
linux_package_matrix: ${{ steps.targets.outputs.linux_package_matrix }}
steps:
- name: Select binary targets
id: targets
env:
TARGET_SET: ${{ inputs.target_set }}
PACKAGE_NPM: ${{ inputs.package_npm }}
PACKAGE_PYPI: ${{ inputs.package_pypi }}
MACOS_RUNNER: ${{ inputs.macos_runner != '' && inputs.macos_runner || vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
LINUX_RUNNER: ${{ inputs.linux_runner != '' && inputs.linux_runner || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
LINUX_ARM64_RUNNER: ${{ vars.LINUX_ARM64_RUNNER || 'ubuntu-24.04-arm' }}
VERIFY_LINUX_ARM64: ${{ inputs.verify_linux_arm64 }}
shell: bash
run: |
python3 <<'PY' >> "$GITHUB_OUTPUT"
import json
import os
targets = [
{
"target": "aarch64-apple-darwin",
"build_target": "aarch64-apple-darwin",
"runner": os.environ["MACOS_RUNNER"],
"cross": False,
"ext": "",
"compatibility_target": "",
},
{
"target": "x86_64-apple-darwin",
"build_target": "x86_64-apple-darwin",
"runner": os.environ["MACOS_RUNNER"],
"cross": True,
"ext": "",
"compatibility_target": "",
},
{
"target": "x86_64-unknown-linux-musl",
"build_target": "x86_64-unknown-linux-musl",
"runner": os.environ["LINUX_RUNNER"],
"cross": True,
"ext": "",
"compatibility_target": "x86_64-unknown-linux-gnu",
},
{
"target": "aarch64-unknown-linux-musl",
"build_target": "aarch64-unknown-linux-musl",
"runner": os.environ["LINUX_RUNNER"],
"cross": True,
"ext": "",
"compatibility_target": "aarch64-unknown-linux-gnu",
},
]
target_set = os.environ["TARGET_SET"]
if target_set == "all":
selected = targets
elif target_set == "macos-arm64":
selected = [
target for target in targets
if target["target"] == "aarch64-apple-darwin"
]
else:
raise SystemExit(f"unsupported target_set: {target_set!r}")
packages_requested = (
os.environ["PACKAGE_NPM"] == "true"
or os.environ["PACKAGE_PYPI"] == "true"
)
if packages_requested and target_set != "all":
raise SystemExit("npm and PyPI packaging require target_set=all")
print("matrix=" + json.dumps({"include": selected}, separators=(",", ":")))
linux_package_targets = [
{
"architecture": "x64",
"runner": os.environ["LINUX_RUNNER"],
}
]
if os.environ["VERIFY_LINUX_ARM64"] == "true":
linux_package_targets.append(
{
"architecture": "arm64",
"runner": os.environ["LINUX_ARM64_RUNNER"],
}
)
print(
"linux_package_matrix="
+ json.dumps({"include": linux_package_targets}, separators=(",", ":"))
)
PY
build:
name: build ${{ matrix.target }}
needs: plan-build
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
build_target: aarch64-apple-darwin
runner: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
cross: false
ext: ""
compatibility_target: ""
- target: x86_64-apple-darwin
build_target: x86_64-apple-darwin
runner: ${{ vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15' }}
cross: true
ext: ""
compatibility_target: ""
- target: x86_64-unknown-linux-musl
build_target: x86_64-unknown-linux-musl
runner: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
cross: true
ext: ""
compatibility_target: x86_64-unknown-linux-gnu
- target: aarch64-unknown-linux-musl
build_target: aarch64-unknown-linux-musl
runner: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
cross: true
ext: ""
compatibility_target: aarch64-unknown-linux-gnu
matrix: ${{ fromJSON(needs.plan-build.outputs.matrix) }}
steps:
- name: Checkout caller ref
if: inputs.checkout_ref == ''
@@ -94,7 +194,7 @@ jobs:
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y binutils clang libclang-dev pkg-config
sudo apt-get install -y binutils clang libclang-dev musl-tools pkg-config
- name: Resolve Ghostty Zig version
id: ghostty-zig-version
@@ -108,76 +208,94 @@ jobs:
with:
version: ${{ steps.ghostty-zig-version.outputs.version }}
- name: Install Rust toolchain
shell: bash
run: |
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
rustup default "$RUST_TOOLCHAIN"
rustc --version
- name: Set up pinned Rust
uses: ./.github/actions/setup-cmux-tui-rust
- name: Install Rust target
shell: bash
run: rustup target add ${{ matrix.target }}
- name: Install cargo-zigbuild
if: matrix.cross == true
if: matrix.cross == true && runner.os == 'Linux'
shell: bash
run: cargo install --locked [email protected]
- name: Build cmux-tui (native)
if: matrix.cross == false
env:
CMUX_TUI_DISTRIBUTION_VERSION: ${{ inputs.version }}
PACKAGE_NPM: ${{ inputs.package_npm }}
working-directory: cmux-tui
shell: bash
env:
PACKAGE_NPM: ${{ inputs.package_npm }}
VERSION: ${{ inputs.version }}
run: |
# Package artifacts must build and stamp the checked-out submodule,
# independent of any self-hosted runner source override.
unset CMUX_GHOSTTY_SRC
CMUX_TUI_BUILD_COMMIT="$(git -C .. rev-parse HEAD)"
CMUX_TUI_GHOSTTY_COMMIT="$(git -C ../ghostty rev-parse HEAD)"
CMUX_TUI_DISTRIBUTION_VERSION="$VERSION"
export CMUX_TUI_BUILD_COMMIT CMUX_TUI_GHOSTTY_COMMIT CMUX_TUI_DISTRIBUTION_VERSION
if [[ "$PACKAGE_NPM" == "true" ]]; then
CMUX_TUI_NPM_BOOTSTRAP_VERSION="$VERSION"
CMUX_TUI_NPM_BOOTSTRAP_VERSION="$CMUX_TUI_DISTRIBUTION_VERSION"
export CMUX_TUI_NPM_BOOTSTRAP_VERSION
fi
cargo build -p cmux-tui --bin cmux-tui --release --locked --target ${{ matrix.build_target }}
cargo build -p cmux-tui --bin cmux-tui --bin cmux-tui-hook --release --locked --target ${{ matrix.build_target }}
cargo build -p cmux-relay --bin cmux-relay --release --locked --target ${{ matrix.build_target }}
- name: Build cmux-tui (cross)
if: matrix.cross == true
- name: Build cmux-tui (Linux cross)
if: matrix.cross == true && runner.os == 'Linux'
env:
CMUX_TUI_DISTRIBUTION_VERSION: ${{ inputs.version }}
PACKAGE_NPM: ${{ inputs.package_npm }}
working-directory: cmux-tui
shell: bash
env:
PACKAGE_NPM: ${{ inputs.package_npm }}
VERSION: ${{ inputs.version }}
run: |
# Package artifacts must build and stamp the checked-out submodule,
# independent of any self-hosted runner source override.
unset CMUX_GHOSTTY_SRC
CMUX_TUI_BUILD_COMMIT="$(git -C .. rev-parse HEAD)"
CMUX_TUI_GHOSTTY_COMMIT="$(git -C ../ghostty rev-parse HEAD)"
CMUX_TUI_DISTRIBUTION_VERSION="$VERSION"
export CMUX_TUI_BUILD_COMMIT CMUX_TUI_GHOSTTY_COMMIT CMUX_TUI_DISTRIBUTION_VERSION
if [[ "$PACKAGE_NPM" == "true" ]]; then
CMUX_TUI_NPM_BOOTSTRAP_VERSION="$VERSION"
CMUX_TUI_NPM_BOOTSTRAP_VERSION="$CMUX_TUI_DISTRIBUTION_VERSION"
export CMUX_TUI_NPM_BOOTSTRAP_VERSION
fi
cargo zigbuild -p cmux-tui --bin cmux-tui --release --locked --target ${{ matrix.build_target }}
cargo zigbuild -p cmux-tui --bin cmux-tui --bin cmux-tui-hook --release --locked --target ${{ matrix.build_target }}
cargo zigbuild -p cmux-relay --bin cmux-relay --release --locked --target ${{ matrix.build_target }}
- name: Build cmux-tui (macOS cross)
if: matrix.cross == true && runner.os == 'macOS'
env:
CMUX_TUI_DISTRIBUTION_VERSION: ${{ inputs.version }}
PACKAGE_NPM: ${{ inputs.package_npm }}
working-directory: cmux-tui
shell: bash
run: |
# Xcode's macOS SDK natively supports cross-architecture builds.
# cargo-zigbuild cannot resolve SDK frameworks when the host is arm64.
unset CMUX_GHOSTTY_SRC
CMUX_TUI_BUILD_COMMIT="$(git -C .. rev-parse HEAD)"
CMUX_TUI_GHOSTTY_COMMIT="$(git -C ../ghostty rev-parse HEAD)"
export CMUX_TUI_BUILD_COMMIT CMUX_TUI_GHOSTTY_COMMIT CMUX_TUI_DISTRIBUTION_VERSION
if [[ "$PACKAGE_NPM" == "true" ]]; then
CMUX_TUI_NPM_BOOTSTRAP_VERSION="$CMUX_TUI_DISTRIBUTION_VERSION"
export CMUX_TUI_NPM_BOOTSTRAP_VERSION
fi
cargo build -p cmux-tui --bin cmux-tui --bin cmux-tui-hook --release --locked --target ${{ matrix.build_target }}
cargo build -p cmux-relay --bin cmux-relay --release --locked --target ${{ matrix.build_target }}
- name: Stage binary
shell: bash
run: |
mkdir -p dist
binary="dist/cmux-tui-${{ matrix.target }}${{ matrix.ext }}"
hook_binary="dist/cmux-tui-hook-${{ matrix.target }}${{ matrix.ext }}"
relay_binary="dist/cmux-relay-${{ matrix.target }}${{ matrix.ext }}"
cp "cmux-tui/target/${{ matrix.target }}/release/cmux-tui${{ matrix.ext }}" "$binary"
cp "cmux-tui/target/${{ matrix.target }}/release/cmux-tui-hook${{ matrix.ext }}" "$hook_binary"
cp "cmux-tui/target/${{ matrix.target }}/release/cmux-relay${{ matrix.ext }}" "$relay_binary"
if [[ -n "${{ matrix.compatibility_target }}" ]]; then
cp "$binary" "dist/cmux-tui-${{ matrix.compatibility_target }}${{ matrix.ext }}"
cp "$hook_binary" "dist/cmux-tui-hook-${{ matrix.compatibility_target }}${{ matrix.ext }}"
cp "$relay_binary" "dist/cmux-relay-${{ matrix.compatibility_target }}${{ matrix.ext }}"
fi
ls -la dist
@@ -236,10 +354,11 @@ jobs:
cloudflare-relay:
name: Cloudflare Durable Object relay
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
if: inputs.build_cloudflare_relay
runs-on: ${{ inputs.linux_runner != '' && inputs.linux_runner || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 30
env:
RUSTUP_TOOLCHAIN: "1.88.0"
RUSTUP_TOOLCHAIN: "1.91.0"
permissions:
contents: read
defaults:
@@ -268,7 +387,7 @@ jobs:
- name: Install pinned Rust toolchain and Worker builder
run: |
rustup toolchain install 1.88.0 --profile minimal --component clippy --target wasm32-unknown-unknown
rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal --component clippy --target wasm32-unknown-unknown
cargo install --locked [email protected]
- name: Install pinned npm dependencies
@@ -292,8 +411,10 @@ jobs:
build-windows:
name: build x86_64-pc-windows-gnu
if: inputs.include_windows
runs-on: ${{ vars.WINDOWS_RUNNER || 'windows-latest' }}
runs-on: ${{ inputs.windows_runner != '' && inputs.windows_runner || vars.WINDOWS_RUNNER || 'windows-latest' }}
timeout-minutes: 60
env:
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS: -C link-arg=-fuse-ld=lld
permissions:
contents: read
steps:
@@ -325,12 +446,8 @@ jobs:
with:
version: ${{ steps.ghostty-zig-version.outputs.version }}
- name: Install Rust toolchain
shell: bash
run: |
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
rustup default "$RUST_TOOLCHAIN"
rustc --version
- name: Set up pinned Rust
uses: ./.github/actions/setup-cmux-tui-rust
- name: Install Rust target
shell: bash
@@ -338,27 +455,55 @@ jobs:
rustup target add x86_64-pc-windows-gnu
printf '%s\n' 'C:\msys64\mingw64\bin' >> "$GITHUB_PATH"
- name: Build libghostty-vt + cmux-tui (Windows GNU)
- name: Verify LLVM GNU linker
shell: bash
run: ld.lld --version
- name: Build libghostty-vt + cmux-tui (Windows GNU)
env:
CMUX_TUI_DISTRIBUTION_VERSION: ${{ inputs.version }}
PACKAGE_NPM: ${{ inputs.package_npm }}
VERSION: ${{ inputs.version }}
shell: bash
run: |
# Keep the manual Zig build and Cargo's build script on the same
# checked-out Ghostty source whose revision is stamped below.
unset CMUX_GHOSTTY_SRC
CMUX_TUI_BUILD_COMMIT="$(git rev-parse HEAD)"
CMUX_TUI_GHOSTTY_COMMIT="$(git -C ghostty rev-parse HEAD)"
CMUX_TUI_DISTRIBUTION_VERSION="$VERSION"
export CMUX_TUI_BUILD_COMMIT CMUX_TUI_GHOSTTY_COMMIT CMUX_TUI_DISTRIBUTION_VERSION
if [[ "$PACKAGE_NPM" == "true" ]]; then
CMUX_TUI_NPM_BOOTSTRAP_VERSION="$VERSION"
CMUX_TUI_NPM_BOOTSTRAP_VERSION="$CMUX_TUI_DISTRIBUTION_VERSION"
export CMUX_TUI_NPM_BOOTSTRAP_VERSION
fi
cd ghostty
zig build -Demit-lib-vt=true -Demit-xcframework=false -Doptimize=ReleaseFast -Dtarget=x86_64-windows-gnu --prefix "$RUNNER_TEMP/ghostty-vt-win-gnu"
cd ../cmux-tui
cargo build -p cmux-tui --bin cmux-tui --release --locked --target x86_64-pc-windows-gnu
cargo build -p cmux-tui --bin cmux-tui --bin cmux-tui-hook --release --locked --target x86_64-pc-windows-gnu
- name: Verify long Windows state path
shell: bash
run: |
python cmux-tui/scripts/smoke-windows-long-state-path.py \
--binary cmux-tui/target/x86_64-pc-windows-gnu/release/cmux-tui.exe
- name: Verify Windows terminal process exit
shell: bash
run: |
python cmux-tui/scripts/smoke-windows-process-wait.py \
--binary cmux-tui/target/x86_64-pc-windows-gnu/release/cmux-tui.exe
- name: Set up Python for ConPTY verification
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12.10"
- name: Verify Windows ConPTY resize
shell: bash
run: |
python -m pip install --disable-pip-version-check --require-hashes \
-r cmux-tui/scripts/requirements-windows-conpty.txt
python cmux-tui/scripts/smoke-windows-conpty-resize.py \
--binary cmux-tui/target/x86_64-pc-windows-gnu/release/cmux-tui.exe
- name: Verify remote commands fail clearly on Windows
shell: bash
@@ -376,20 +521,23 @@ jobs:
run: |
mkdir -p dist
cp "cmux-tui/target/x86_64-pc-windows-gnu/release/cmux-tui.exe" "dist/cmux-tui-x86_64-pc-windows-gnu.exe"
cp "cmux-tui/target/x86_64-pc-windows-gnu/release/cmux-tui-hook.exe" "dist/cmux-tui-hook-x86_64-pc-windows-gnu.exe"
ls -la dist
- name: Upload binary artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: cmux-tui-x86_64-pc-windows-gnu
path: dist/cmux-tui-x86_64-pc-windows-gnu.exe
path: |
dist/cmux-tui-x86_64-pc-windows-gnu.exe
dist/cmux-tui-hook-x86_64-pc-windows-gnu.exe
if-no-files-found: error
package:
name: package distributions
needs: build
if: inputs.package_npm || inputs.package_pypi
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.linux_runner != '' && inputs.linux_runner || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 30
permissions:
contents: read
@@ -483,6 +631,9 @@ jobs:
binary = root / name / "bin" / "cmux-tui"
if not binary.stat().st_mode & stat.S_IXUSR:
raise SystemExit(f"{binary} is not executable")
hook = root / name / "bin" / "cmux-tui-hook"
if not hook.stat().st_mode & stat.S_IXUSR:
raise SystemExit(f"{hook} is not executable")
PY
chmod +x dist/binaries/cmux-tui-x86_64-unknown-linux-musl
dist/binaries/cmux-tui-x86_64-unknown-linux-musl --version >/tmp/cmux-tui-version.txt 2>&1 || \
@@ -526,6 +677,18 @@ jobs:
for wheel in dist/pypi-wheels/*.whl; do
python3 -m zipfile -l "$wheel" >"/tmp/$(basename "$wheel").list"
done
python3 - <<'PY'
import glob
import stat
import zipfile
for path in glob.glob("dist/pypi-wheels/*.whl"):
with zipfile.ZipFile(path) as wheel:
info = wheel.getinfo("cmux_tui/bin/cmux-tui-hook")
mode = info.external_attr >> 16
if not mode & stat.S_IXUSR:
raise SystemExit(f"{path}: bundled cmux-tui-hook is not executable")
PY
chmod +x dist/binaries/cmux-tui-x86_64-unknown-linux-musl
dist/binaries/cmux-tui-x86_64-unknown-linux-musl --version >/tmp/cmux-tui-version.txt 2>&1 || \
dist/binaries/cmux-tui-x86_64-unknown-linux-musl --help >/tmp/cmux-tui-version.txt 2>&1
@@ -552,7 +715,9 @@ jobs:
verify-linux-packages:
name: verify Linux package entrypoints (${{ matrix.architecture }})
needs: package
needs:
- plan-build
- package
if: ${{ inputs.package_npm || inputs.package_pypi }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
@@ -560,15 +725,7 @@ jobs:
contents: read
strategy:
fail-fast: false
matrix:
include:
- architecture: x64
runner: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
# Run ARM64 containers on native hardware. QEMU registration is not
# persistent on every third-party x64 runner and can disappear
# between setup and the package smoke test.
- architecture: arm64
runner: ${{ vars.LINUX_ARM64_RUNNER || 'ubuntu-24.04-arm' }} # github-hosted-required: package smoke tests need native ARM64 execution
matrix: ${{ fromJSON(needs.plan-build.outputs.linux_package_matrix) }}
env:
PYPI_VERSION: ${{ inputs.pypi_version != '' && inputs.pypi_version || inputs.version }}
steps:
+54 -17
View File
@@ -6,13 +6,45 @@ on:
- main
paths:
- "cmux-tui/**"
- ".github/actions/setup-cmux-tui-rust/**"
- ".github/workflows/cmux-tui-nightly.yml"
- ".github/workflows/cmux-tui-release-cut.yml"
- ".github/workflows/cmux-tui-release.yml"
- ".github/workflows/cmux-tui-sdks.yml"
- ".github/workflows/cmux-tui-spec.yml"
- ".github/workflows/sdk-bootstrap-crates.yml"
- ".github/workflows/sdk-bootstrap-npm.yml"
- ".github/workflows/sdk-bootstrap-pypi.yml"
- ".github/workflows/sdk-publish-crates.yml"
- ".github/workflows/sdk-publish-go.yml"
- ".github/workflows/sdk-publish-java.yml"
- ".github/workflows/sdk-publish-npm.yml"
- ".github/workflows/sdk-publish-python.yml"
- ".github/workflows/sdk-release-cut.yml"
- ".github/workflows/tui-publish-npm.yml"
- ".github/workflows/tui-publish-pypi.yml"
- "tests/test_tui_publish_workflow_security.py"
pull_request:
paths:
- "cmux-tui/**"
- ".github/actions/setup-cmux-tui-rust/**"
- ".github/workflows/cmux-tui-nightly.yml"
- ".github/workflows/cmux-tui-release-cut.yml"
- ".github/workflows/cmux-tui-release.yml"
- ".github/workflows/cmux-tui-sdks.yml"
- ".github/workflows/cmux-tui-spec.yml"
- ".github/workflows/sdk-bootstrap-crates.yml"
- ".github/workflows/sdk-bootstrap-npm.yml"
- ".github/workflows/sdk-bootstrap-pypi.yml"
- ".github/workflows/sdk-publish-crates.yml"
- ".github/workflows/sdk-publish-go.yml"
- ".github/workflows/sdk-publish-java.yml"
- ".github/workflows/sdk-publish-npm.yml"
- ".github/workflows/sdk-publish-python.yml"
- ".github/workflows/sdk-release-cut.yml"
- ".github/workflows/tui-publish-npm.yml"
- ".github/workflows/tui-publish-pypi.yml"
- "tests/test_tui_publish_workflow_security.py"
workflow_dispatch:
concurrency:
@@ -37,6 +69,12 @@ jobs:
with:
python-version: "3.12.8"
- name: Install workflow guard dependencies
run: |
python3 -m pip install \
--disable-pip-version-check \
"PyYAML==6.0.3"
- name: Test protocol inventory
run: python3 cmux-tui/scripts/test_check_spec_inventory.py
@@ -70,8 +108,11 @@ jobs:
-p 'test_*.py' \
-v
- name: Test SDK publishing workflow guards
run: python3 tests/test_tui_publish_workflow_security.py -v
- name: Check package versions
run: python3 cmux-tui/bindings/check-versions.py
run: python3 cmux-tui/bindings/check-versions.py --published-only
- name: Test shared conformance runner
run: |
@@ -153,10 +194,10 @@ jobs:
env:
PYTHONPATH: cmux-tui/bindings/python
run: |
python3 -m unittest discover -s cmux-tui/bindings/python/tests -v
python3 -m pip install \
--disable-pip-version-check \
"setuptools==80.9.0"
python3 -m unittest discover -s cmux-tui/bindings/python/tests -v
python3 -m pip install \
--no-build-isolation \
--no-deps \
@@ -176,7 +217,7 @@ jobs:
distribution = next(
item
for item in importlib.metadata.distributions(path=[str(package)])
if item.metadata["Name"] == "cmux"
if item.metadata["Name"] == "cmux-sdk"
)
assert not distribution.requires
PY
@@ -192,32 +233,32 @@ jobs:
if: matrix.language == 'rust'
working-directory: cmux-tui
run: |
cargo +1.88.0 fmt -p cmux-client -p cmux-sidebar -- --check
cargo +1.88.0 fmt -p cmux-sdk -p cmux-sidebar -- --check
cargo +1.88.0 test \
-p cmux-client \
-p cmux-sdk \
-p cmux-sidebar \
--all-targets \
--locked
cargo +1.88.0 test \
-p cmux-client \
-p cmux-sdk \
-p cmux-sidebar \
--doc \
--locked
cargo +1.88.0 clippy \
-p cmux-client \
-p cmux-sdk \
-p cmux-sidebar \
--all-targets \
--locked \
-- -D warnings
RUSTDOCFLAGS="-D warnings" \
cargo +1.88.0 doc \
-p cmux-client \
-p cmux-sdk \
-p cmux-sidebar \
--locked \
--no-deps
cargo +1.88.0 package -p cmux-client --locked
cargo +1.88.0 package -p cmux-sdk --locked
# Full sidebar packaging resolves its versioned crates.io dependency.
# Publish cmux-client first; CI still verifies the exact sidebar file set.
# Publish cmux-sdk first; CI still verifies the exact sidebar file set.
cargo +1.88.0 package -p cmux-sidebar --locked --list
- name: Test Go SDK
@@ -447,11 +488,8 @@ jobs:
cache: npm
cache-dependency-path: cmux-tui/bindings/typescript/package-lock.json
- name: Set up Rust 1.95
run: |
rustup toolchain install 1.95.0 --profile minimal
cargo +1.95.0 --version
rustc +1.95.0 --version
- name: Set up pinned cmux-tui Rust
uses: ./.github/actions/setup-cmux-tui-rust
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
@@ -480,7 +518,7 @@ jobs:
working-directory: cmux-tui
run: |
test "$(zig version)" = "0.16.0"
cargo +1.95.0 build -p cmux-tui --bin cmux-tui --locked
cargo build -p cmux-tui --bin cmux-tui --locked
- name: Set up Zig for SDK conformance
uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # v2.0.5
@@ -493,7 +531,6 @@ jobs:
CXX: clang++
CMUX_ZIG: zig
NODE_OPTIONS: --experimental-websocket
RUSTUP_TOOLCHAIN: 1.95.0
run: |
test "$(zig version)" = "0.15.2"
test "$(node -p 'typeof WebSocket')" = "function"
@@ -0,0 +1,366 @@
name: cmux-tui Rust Testbox setup
# Main-controlled broker. This workflow runs only from refs/heads/main and
# hydrates only main, so no candidate branch can edit the guards that run
# before begin-testbox exposes its auth token, and no candidate build script
# executes inside this token-bearing job. A candidate revision reaches the
# Testbox later, through `blacksmith testbox run`, which synchronizes a
# maintainer's local worktree onto the already-warm VM.
on:
workflow_dispatch:
inputs:
testbox_id:
description: "Testbox session ID supplied by blacksmith testbox warmup"
required: true
type: string
permissions: {}
concurrency:
# A Testbox is a mutable shared workspace. Serialize every request for the
# same ID so two dispatches cannot corrupt one cache.
group: cmux-tui-testbox-${{ inputs.testbox_id }}
cancel-in-progress: false
jobs:
cmux-tui-rust:
name: cmux-tui Rust setup
runs-on: blacksmith-32vcpu-ubuntu-2404
environment:
# Configure this environment with required reviewers, no secrets, and a
# deployment branch rule of exactly `main`. Approval is evaluated before
# the first step, so it precedes begin-testbox.
name: blacksmith-testbox-trusted
permissions:
contents: read
# Hydration plus three sequential 20-minute bounded remote builds happen
# after setup. Keep the GitHub job alive long enough for all stages and
# cleanup; the Testbox itself still has its separate idle timeout.
timeout-minutes: 120
steps:
# Every check here is main-controlled. `blacksmith testbox warmup`
# resolves both this file and the hydrated source from the same --ref, so
# refusing any ref other than refs/heads/main is what keeps a candidate
# branch outside the trust boundary.
- name: Validate broker ref
env:
DISPATCH_REF: ${{ github.ref }}
EVENT_NAME: ${{ github.event_name }}
REPOSITORY: ${{ github.repository }}
TESTBOX_ID: ${{ inputs.testbox_id }}
shell: bash
run: |
set -euo pipefail
[[ "$REPOSITORY" == "manaflow-ai/cmux" ]] || {
echo "::error::this Testbox lane is only valid for manaflow-ai/cmux" >&2
exit 1
}
[[ "$EVENT_NAME" == "workflow_dispatch" ]] || {
echo "::error::Testbox setup must be dispatched manually, never from a PR event" >&2
exit 1
}
[[ "$DISPATCH_REF" == "refs/heads/main" ]] || {
echo "::error::this broker lane runs only from refs/heads/main, got $DISPATCH_REF; warm up with --ref main and sync the candidate through blacksmith testbox run" >&2
exit 1
}
[[ "$TESTBOX_ID" =~ ^tbx_[A-Za-z0-9_-]+$ ]] || {
echo "::error::malformed Testbox ID" >&2
exit 1
}
- name: Begin Testbox
uses: useblacksmith/begin-testbox@233448af4bfdc6fca509a7f0974411ac6d8a8043 # v2
with:
testbox_id: ${{ inputs.testbox_id }}
# main can move while a reviewer approves the deployment. Record which
# commit this job actually hydrates and fail closed if the dispatch ref
# changed under it.
- name: Revalidate broker identity after token exposure
env:
DISPATCH_REF: ${{ github.ref }}
DISPATCH_SHA: ${{ github.sha }}
shell: bash
run: |
set -euo pipefail
[[ "$DISPATCH_REF" == "refs/heads/main" ]] || {
echo "::error::broker ref changed during setup" >&2
exit 1
}
[[ "$DISPATCH_SHA" =~ ^[0-9a-f]{40}$ ]] || {
echo "::error::dispatch SHA is malformed" >&2
exit 1
}
printf 'hydrating main at %s\n' "$DISPATCH_SHA"
- name: Checkout hydration commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.sha }}
- name: Require exact checkout and clean source
env:
EXPECTED_SHA: ${{ github.sha }}
shell: bash
run: |
set -euo pipefail
actual_sha="$(git rev-parse HEAD)"
[[ "$actual_sha" == "$EXPECTED_SHA" ]] || {
echo "::error::checked out $actual_sha, expected hydration SHA $EXPECTED_SHA" >&2
exit 1
}
[[ -z "$(git status --porcelain=v1 --untracked-files=normal)" ]] || {
echo "::error::source checkout is dirty before hydration" >&2
git status --short >&2
exit 1
}
source_tree_sha="$(git rev-parse 'HEAD^{tree}')"
ghostty_entry="$(git ls-tree HEAD ghostty)"
[[ "$ghostty_entry" =~ ^160000[[:space:]]commit[[:space:]][0-9a-f]{40}[[:space:]]ghostty$ ]] || {
echo "::error::HEAD:ghostty is not a gitlink" >&2
exit 1
}
ghostty_gitlink_sha="$(git rev-parse 'HEAD:ghostty')"
printf 'source_sha=%s\nsource_tree_sha=%s\nghostty_gitlink_sha=%s\n' \
"$actual_sha" "$source_tree_sha" "$ghostty_gitlink_sha"
- name: Initialize Ghostty source submodule
shell: bash
run: |
set -euo pipefail
git submodule update --init --depth 1 ghostty
[[ "$(git -C ghostty rev-parse --show-toplevel)" == "$GITHUB_WORKSPACE/ghostty" ]] || {
echo "::error::ghostty did not initialize as its own submodule checkout" >&2
exit 1
}
ghostty_entry="$(git ls-tree HEAD ghostty)"
[[ "$ghostty_entry" =~ ^160000[[:space:]]commit[[:space:]][0-9a-f]{40}[[:space:]]ghostty$ ]] || {
echo "::error::HEAD:ghostty is not a gitlink" >&2
exit 1
}
expected_ghostty_sha="$(git rev-parse HEAD:ghostty)"
actual_ghostty_sha="$(git -C ghostty rev-parse HEAD)"
[[ "$actual_ghostty_sha" == "$expected_ghostty_sha" ]] || {
echo "::error::Ghostty checkout $actual_ghostty_sha does not match gitlink $expected_ghostty_sha" >&2
exit 1
}
[[ -z "$(git -C ghostty status --porcelain=v1 --untracked-files=normal)" ]] || {
echo "::error::Ghostty submodule is dirty after initialization" >&2
git -C ghostty status --short >&2
exit 1
}
test -f ghostty/build.zig.zon
- name: Install Linux build dependencies
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y clang libclang-dev pkg-config
- name: Cache Zig package downloads
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/zig
key: cmux-tui-zig-${{ hashFiles('ghostty/build.zig.zon', 'ghostty/build.zig.zon.json') }}
restore-keys: |
cmux-tui-zig-
- name: Install repository-pinned Zig
shell: bash
run: ./scripts/install-zig-ci.sh
- name: Fetch Ghostty Zig dependencies without compiling
working-directory: ghostty
shell: bash
run: |
set -euo pipefail
# `--fetch` hydrates the package cache and exits before a build.
"$CMUX_ZIG" build --fetch
- name: Cache Cargo registry and git dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry
~/.cargo/git
key: cmux-tui-cargo-${{ runner.os }}-${{ hashFiles('cmux-tui/Cargo.lock', 'cmux-tui/rust-toolchain.toml') }}
restore-keys: |
cmux-tui-cargo-${{ runner.os }}-
- name: Set up repository-pinned cmux-tui Rust
uses: ./.github/actions/setup-cmux-tui-rust
- name: Fetch Cargo dependencies without compiling
working-directory: cmux-tui
shell: bash
run: |
set -euo pipefail
cargo fetch --locked
- name: Record runner, toolchain, and Ghostty identity
env:
SOURCE_SHA: ${{ github.sha }}
SOURCE_REF: ${{ github.ref }}
TESTBOX_ID: ${{ inputs.testbox_id }}
RUNNER_LABEL: blacksmith-32vcpu-ubuntu-2404
shell: bash
run: |
set -euo pipefail
source_tree_sha="$(git rev-parse 'HEAD^{tree}')"
ghostty_entry="$(git ls-tree HEAD ghostty)"
[[ "$ghostty_entry" =~ ^160000[[:space:]]commit[[:space:]][0-9a-f]{40}[[:space:]]ghostty$ ]] || exit 1
ghostty_gitlink_sha="$(git rev-parse 'HEAD:ghostty')"
ghostty_head_sha="$(git -C ghostty rev-parse HEAD)"
[[ "$SOURCE_SHA" == "$(git rev-parse HEAD)" ]] || exit 1
[[ "$ghostty_gitlink_sha" == "$ghostty_head_sha" ]] || exit 1
[[ -z "$(git status --porcelain=v1 --untracked-files=normal)" ]] || exit 1
[[ -z "$(git -C ghostty status --porcelain=v1 --untracked-files=normal)" ]] || exit 1
pushd cmux-tui >/dev/null
cargo metadata --locked --no-deps --format-version 1 > "$RUNNER_TEMP/cmux-tui-cargo-metadata.json"
test -s "$RUNNER_TEMP/cmux-tui-cargo-metadata.json"
RUST_TOOLCHAIN="$(rustup show active-toolchain)"
RUSTC_VERSION="$(rustc --version)"
CARGO_VERSION="$(cargo --version)"
popd >/dev/null
mkdir -p testbox-benchmark
SOURCE_TREE_SHA="$source_tree_sha"
GHOSTTY_GITLINK_SHA="$ghostty_gitlink_sha"
GHOSTTY_HEAD_SHA="$ghostty_head_sha"
ZIG_VERSION="$("$CMUX_ZIG" version)"
ZIG_PATH="$CMUX_ZIG"
RUNNER_UNAME="$(uname -a)"
RUNNER_CPU_COUNT="$(nproc)"
[[ "$RUNNER_ARCH" == "X64" && "$RUNNER_CPU_COUNT" == "32" ]] || {
echo "::error::expected x64 32-vCPU runner, got arch=$RUNNER_ARCH cpu_count=$RUNNER_CPU_COUNT" >&2
exit 1
}
CARGO_METADATA_SHA256="$(sha256sum "$RUNNER_TEMP/cmux-tui-cargo-metadata.json" | cut -d ' ' -f 1)"
RUST_TOOLCHAIN_FILE_SHA256="$(sha256sum cmux-tui/rust-toolchain.toml | cut -d ' ' -f 1)"
CARGO_LOCK_SHA256="$(sha256sum cmux-tui/Cargo.lock | cut -d ' ' -f 1)"
GHOSTTY_ZON_SHA256="$(sha256sum ghostty/build.zig.zon | cut -d ' ' -f 1)"
export SOURCE_TREE_SHA GHOSTTY_GITLINK_SHA GHOSTTY_HEAD_SHA RUST_TOOLCHAIN RUSTC_VERSION CARGO_VERSION ZIG_VERSION ZIG_PATH RUNNER_UNAME RUNNER_CPU_COUNT CARGO_METADATA_SHA256 RUST_TOOLCHAIN_FILE_SHA256 CARGO_LOCK_SHA256 GHOSTTY_ZON_SHA256
python3 - <<'PY' > testbox-benchmark/setup-identity.json
import json
import os
import platform
print(json.dumps({
"schema": 3,
"source": {
"ref": os.environ["SOURCE_REF"],
"commit_sha": os.environ["SOURCE_SHA"],
"tree_sha": os.environ["SOURCE_TREE_SHA"],
"ghostty_gitlink_sha": os.environ["GHOSTTY_GITLINK_SHA"],
"ghostty_head_sha": os.environ["GHOSTTY_HEAD_SHA"],
},
"broker": {
"workflow_ref": os.environ["GITHUB_REF"],
"workflow_sha": os.environ["SOURCE_SHA"],
},
"testbox": {
"id": os.environ["TESTBOX_ID"],
"setup_workflow_run_id": os.environ["GITHUB_RUN_ID"],
},
"runner": {
"label": os.environ["RUNNER_LABEL"],
"name": os.environ.get("RUNNER_NAME"),
"os": os.environ.get("RUNNER_OS"),
"arch": os.environ.get("RUNNER_ARCH"),
"hostname": platform.node(),
"uname": os.environ["RUNNER_UNAME"],
"cpu_count": int(os.environ["RUNNER_CPU_COUNT"]),
},
"toolchain": {
"rust_toolchain": os.environ["RUST_TOOLCHAIN"],
"rustc": os.environ["RUSTC_VERSION"],
"cargo": os.environ["CARGO_VERSION"],
"rust_toolchain_file_sha256": os.environ["RUST_TOOLCHAIN_FILE_SHA256"],
"cargo_lock_sha256": os.environ["CARGO_LOCK_SHA256"],
"cargo_metadata_sha256": os.environ["CARGO_METADATA_SHA256"],
"zig_path": os.environ["ZIG_PATH"],
"zig": os.environ["ZIG_VERSION"],
"ghostty_build_zig_zon_sha256": os.environ["GHOSTTY_ZON_SHA256"],
},
}, sort_keys=True, indent=2))
PY
test -s testbox-benchmark/setup-identity.json
cat testbox-benchmark/setup-identity.json
- name: Require clean hydrated source
shell: bash
run: |
set -euo pipefail
[[ -z "$(git status --porcelain=v1 --untracked-files=normal)" ]] || {
echo "::error::source became dirty during hydration" >&2
git status --short >&2
exit 1
}
[[ -z "$(git -C ghostty status --porcelain=v1 --untracked-files=normal)" ]] || {
echo "::error::Ghostty became dirty during hydration" >&2
git -C ghostty status --short >&2
exit 1
}
- name: Upload setup identity JSON
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cmux-tui-testbox-setup-${{ github.run_id }}
path: testbox-benchmark/setup-identity.json
if-no-files-found: warn
retention-days: 14
# Missing `/tmp/.testbox` means begin-testbox could not register the VM.
# No auth token or API URL exists, so neither this workflow nor the pinned
# action can report hydration_failed. Fail explicitly; the CLI's bounded
# readiness wait and receipt-aware cleanup remain the only safe recovery.
- name: Require Testbox registration state
if: always()
shell: bash
run: |
set -euo pipefail
if [[ ! -d /tmp/.testbox ]]; then
echo "::error::begin-testbox produced no registration state; hydration failure cannot be reported from this runner" >&2
exit 1
fi
- name: Confirm Testbox registration and mark setup ready
if: success()
env:
EXPECTED_TESTBOX_ID: ${{ inputs.testbox_id }}
shell: bash
run: |
set -euo pipefail
state=/tmp/.testbox
rm -f /tmp/.testbox/cmux-tui-rust-setup-identity.json
for required_file in testbox_id installation_model_id auth_token api_url runner_host runner_ssh_port adopted_run_id ssh_public_key; do
test -s "$state/$required_file"
done
test -s "$state/working_directory"
test "$(<"$state/testbox_id")" = "$EXPECTED_TESTBOX_ID"
installation_model_id="$(<"$state/installation_model_id")"
[[ "$installation_model_id" =~ ^[0-9]+$ ]]
test -s "$state/ssh_public_key"
# begin-testbox deliberately continues after degraded phone-home.
# Validate the SSH handoff and install the identity marker before the
# pinned run-testbox action publishes ready. That action remains the
# sole readiness publisher, so a CLI run cannot race this marker.
marker="$state/cmux-tui-rust-setup-identity.json"
rm -f "$marker"
install -m 600 testbox-benchmark/setup-identity.json "$marker"
test -s "$marker"
# Keepalive is checked into this main-controlled workflow instead of
# importing an upstream composite that performs an unbounded duplicate
# ready curl. It reports
# hydration_failed on any failed setup or readiness path and bounds every
# phone-home request before entering its idle loop.
- name: Run trusted Testbox keepalive
if: always()
env:
JOB_STATUS: ${{ job.status }}
shell: bash
run: ./scripts/blacksmith-testbox-keepalive.sh
+433 -95
View File
@@ -1,24 +1,99 @@
name: cmux-tui
run-name: cmux-tui ${{ inputs.mode }} ${{ inputs.request_id }} @ ${{ inputs.commit }}
on:
# Temporarily manual-only beginning 2026-07-13 to pause automatic CI.
workflow_dispatch:
inputs:
commit:
description: "Exact pushed 40-character commit SHA to verify"
required: true
type: string
mode:
description: "Focused Rust tests or the full cross-platform merge gate"
required: true
type: choice
options:
- focused
- full
test_filter:
description: "Rust test-name substring; required in focused mode"
required: false
default: ""
type: string
request_id:
description: "Unique caller token used to find this run"
required: true
type: string
concurrency:
group: cmux-tui-${{ github.workflow }}-${{ github.ref }}
# Every caller waits for one exact commit. A newer request must not cancel it.
group: cmux-tui-${{ inputs.request_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
validate-inputs:
name: validate exact commit request
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5
steps:
- name: Validate dispatch inputs
env:
EXACT_COMMIT: ${{ inputs.commit }}
WORKFLOW_COMMIT: ${{ github.sha }}
MODE: ${{ inputs.mode }}
TEST_FILTER: ${{ inputs.test_filter }}
REQUEST_ID: ${{ inputs.request_id }}
shell: bash
run: |
if [[ ! "$EXACT_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::commit must be a lowercase 40-character SHA" >&2
exit 1
fi
if [[ "$WORKFLOW_COMMIT" != "$EXACT_COMMIT" ]]; then
echo "::error::workflow revision $WORKFLOW_COMMIT does not match requested commit $EXACT_COMMIT" >&2
exit 1
fi
if [[ ! "$REQUEST_ID" =~ ^[A-Za-z0-9._-]{1,100}$ ]]; then
echo "::error::request_id contains unsupported characters" >&2
exit 1
fi
case "$MODE" in
focused)
if [[ ! "$TEST_FILTER" =~ ^[A-Za-z0-9_][A-Za-z0-9_:.-]{0,199}$ ]]; then
echo "::error::focused mode needs one safe Rust test-name substring" >&2
exit 1
fi
;;
full)
if [[ -n "$TEST_FILTER" ]]; then
echo "::error::full mode does not accept test_filter" >&2
exit 1
fi
;;
*)
echo "::error::unsupported mode: $MODE" >&2
exit 1
;;
esac
web-frontend:
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
needs: validate-inputs
if: inputs.mode == 'full'
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ inputs.commit }}
- name: Require exact checkout
env:
EXACT_COMMIT: ${{ inputs.commit }}
run: test "$(git rev-parse HEAD)" = "$EXACT_COMMIT"
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -42,13 +117,28 @@ jobs:
npm run build
npm test
valgrind-leak-check:
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
valgrind-leak-check-shard:
name: valgrind-leak-check (${{ matrix.shard }})
needs: validate-inputs
if: inputs.mode == 'full'
runs-on: blacksmith-4vcpu-ubuntu-2404
# Full behavior stays in the normal macOS and Linux suites. Valgrind owns
# the bounded startup parsers and terminal replay state only.
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
shard: [startup]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ inputs.commit }}
- name: Require exact checkout
env:
EXACT_COMMIT: ${{ inputs.commit }}
run: test "$(git rev-parse HEAD)" = "$EXACT_COMMIT"
- name: Init ghostty submodule
run: git submodule update --init --depth 1 ghostty
@@ -61,13 +151,8 @@ jobs:
- name: Install zig
run: ./scripts/install-zig-ci.sh
- name: Rust version
run: |
rustc --version || true
if ! command -v cargo >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
- name: Set up pinned Rust
uses: ./.github/actions/setup-cmux-tui-rust
- name: Build test binaries
working-directory: cmux-tui
@@ -76,70 +161,181 @@ jobs:
# SIMD codegen stays within what valgrind's instruction emulation
# supports (see crates/ghostty-vt-sys/build.rs).
CMUX_GHOSTTY_VT_ZIG_CPU: baseline
VALGRIND_SHARD: ${{ matrix.shard }}
run: |
mkdir -p target
cargo test --workspace --locked --no-run --message-format=json > target/cargo-test-binaries.jsonl
python3 <<'PY'
import json
import os
import re
import sys
shard = os.environ["VALGRIND_SHARD"]
known_shards = {"startup"}
if shard not in known_shards:
raise SystemExit(f"unknown Valgrind shard: {shard}")
def shard_for(executable):
name = os.path.basename(executable)
if re.fullmatch(r"(?:cmux_tui|terminal)-[0-9a-f]+", name):
return "startup"
return None
seen = set()
selected = []
with open("target/cargo-test-binaries.jsonl", "r", encoding="utf-8") as messages:
with open("target/valgrind-test-binaries.txt", "w", encoding="utf-8") as output:
for line in messages:
try:
message = json.loads(line)
except json.JSONDecodeError:
continue
if not message.get("profile", {}).get("test"):
continue
executable = message.get("executable")
if not executable or executable in seen:
continue
seen.add(executable)
print(executable, file=output)
for line in messages:
try:
message = json.loads(line)
except json.JSONDecodeError:
continue
if not message.get("profile", {}).get("test"):
continue
executable = message.get("executable")
if not executable or executable in seen:
continue
seen.add(executable)
if shard_for(executable) == shard:
selected.append(executable)
if not seen:
raise SystemExit("cargo did not report any test binaries")
print(f"Collected {len(seen)} test binaries", file=sys.stderr)
if not selected:
raise SystemExit(f"Valgrind shard {shard} selected no test binaries")
with open("target/valgrind-test-binaries.txt", "w", encoding="utf-8") as output:
for executable in selected:
print(executable, file=output)
print(
f"Valgrind shard {shard} selected {len(selected)} of {len(seen)} test binaries",
file=sys.stderr,
)
PY
- name: Verify baseline terminal replay behavior
if: matrix.shard == 'startup'
working-directory: cmux-tui
run: |
matches=0
while IFS= read -r bin; do
[ -n "$bin" ] || continue
case "$(basename "$bin")" in
terminal-*)
if ! "$bin" --list \
| grep -Fqx 'pending_wrap_replay_preserves_cursor_with_origin_mode: test'; then
echo "::error::terminal replay test binary does not contain the exact baseline test" >&2
exit 1
fi
"$bin" pending_wrap_replay_preserves_cursor_with_origin_mode \
--exact \
--test-threads=1
matches=$((matches + 1))
;;
esac
done < target/valgrind-test-binaries.txt
if [[ "$matches" -ne 1 ]]; then
echo "::error::expected one terminal integration test binary, found $matches" >&2
exit 1
fi
- name: Run test binaries under valgrind
working-directory: cmux-tui
env:
# Valgrind's ~30x slowdown breaks the 50ms ws-latency budget on an
# otherwise-correct build; the guarded regression (events serialized
# behind a 100ms read poll) inflates far past this bound anyway.
CMUX_TEST_WS_LATENCY_BUDGET_MS: "2000"
# Process-exit and PTY-reader tests also use bounded polling. Keep
# their normal deadlines strict while allowing for instrumentation.
CMUX_TEST_TIMEOUT_SCALE: "4"
run: |
while IFS= read -r bin; do
[ -n "$bin" ] || continue
echo "Running valgrind for $bin"
if ! valgrind \
run_valgrind() {
local bin="$1"
shift
valgrind \
"${valgrind_args[@]}" \
--error-exitcode=1 \
--leak-check=full \
--show-leak-kinds=definite \
--errors-for-leak-kinds=definite \
--track-origins=yes \
-- "$bin"; then
echo "Valgrind failed for $bin" >&2
-- "$bin" "$@"
}
require_exact_test() {
local bin="$1"
local test_name="$2"
if ! "$bin" --list | grep -Fqx "$test_name: test"; then
echo "::error::$(basename "$bin") does not contain exact test '$test_name'" >&2
exit 1
fi
}
cmux_tui_bin=""
terminal_bin=""
while IFS= read -r bin; do
case "$(basename "$bin")" in
cmux_tui-*) cmux_tui_bin="$bin" ;;
terminal-*) terminal_bin="$bin" ;;
esac
done < target/valgrind-test-binaries.txt
if [[ -z "$cmux_tui_bin" || -z "$terminal_bin" ]]; then
echo "::error::startup memory check did not find both test binaries" >&2
exit 1
fi
valgrind_args=(--track-origins=yes)
require_exact_test \
"$terminal_bin" \
pending_wrap_replay_preserves_cursor_with_origin_mode
run_valgrind \
"$terminal_bin" \
pending_wrap_replay_preserves_cursor_with_origin_mode \
--exact \
--test-threads=1
startup_tests=(
config::tests::load_uses_file_ghostty_defaults_without_invoking_external_resolver
config::tests::ghostty_file_reader_enforces_byte_limit_during_read
config::tests::ghostty_config_helper_output_reader_enforces_byte_limit
)
for test_name in "${startup_tests[@]}"; do
require_exact_test "$cmux_tui_bin" "$test_name"
run_valgrind \
"$cmux_tui_bin" \
"$test_name" \
--exact \
--test-threads=1
done
valgrind-leak-check:
name: valgrind-leak-check
if: always() && inputs.mode == 'full'
needs: valgrind-leak-check-shard
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 2
steps:
- name: Require every Valgrind shard
env:
SHARD_RESULT: ${{ needs.valgrind-leak-check-shard.result }}
run: test "$SHARD_RESULT" = success
test:
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os == 'macos' && (vars.MACOS_RUNNER_15 || 'blacksmith-6vcpu-macos-15') || 'ubuntu-latest' }}
needs: validate-inputs
runs-on: ${{ matrix.runner }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
os: [macos, linux]
include:
- os: macos
runner: blacksmith-6vcpu-macos-15
- os: linux
runner: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ inputs.commit }}
- name: Require exact checkout
env:
EXACT_COMMIT: ${{ inputs.commit }}
run: test "$(git rev-parse HEAD)" = "$EXACT_COMMIT"
- name: Init ghostty submodule
run: git submodule update --init --depth 1 ghostty
@@ -153,47 +349,144 @@ jobs:
- name: Install zig
run: ./scripts/install-zig-ci.sh
- name: Rust version
- name: Ghostty VT cursor replay checks
if: inputs.mode == 'full'
working-directory: ghostty
run: |
rustc --version || true
if ! command -v cargo >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
"$CMUX_ZIG" build test-lib-vt -Dtest-filter="Terminal vt"
- name: Set up pinned Rust
uses: ./.github/actions/setup-cmux-tui-rust
- name: cargo fmt
id: rustfmt-check
working-directory: cmux-tui
run: cargo fmt --check
- name: Prepare hosted rustfmt patch
if: ${{ failure() && steps.rustfmt-check.outcome == 'failure' }}
working-directory: cmux-tui
run: |
cargo fmt
git diff --binary -- . > "$RUNNER_TEMP/rustfmt.patch"
test -s "$RUNNER_TEMP/rustfmt.patch"
- name: Upload hosted rustfmt patch
if: ${{ failure() && steps.rustfmt-check.outcome == 'failure' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustfmt-${{ runner.os }}-${{ github.sha }}
path: ${{ runner.temp }}/rustfmt.patch
if-no-files-found: error
retention-days: 1
- name: cargo clippy
if: inputs.mode == 'full'
working-directory: cmux-tui
run: cargo clippy --workspace --all-targets --locked -- -D warnings
- name: focused Linux journal process-fence test
if: runner.os == 'Linux'
working-directory: cmux-tui
run: >-
cargo test -p cmux-tui-core --lib
journal_hooks::tests::unix_hook_tree_kills_a_descendant_that_created_a_new_session
-- --exact
- name: focused macOS journal process-fence tests
if: runner.os == 'macOS'
working-directory: cmux-tui
run: cargo test -p cmux-tui-core --lib unix_process_scope::tests::mac_process_scope_
- name: focused journal writer shutdown test
working-directory: cmux-tui
run: >-
cargo test -p cmux-tui-core --lib
journal_ingress::tests::shutdown_returns_when_an_admitted_commit_stays_blocked
-- --exact
- name: focused final journal ownership tests
working-directory: cmux-tui
run: |
cargo test -p cmux-tui-core --lib \
journal_ingress::tests::shutdown_closes_admission_while_a_terminal_producer_waits_for_space \
-- --exact
cargo test -p cmux-tui-core --lib \
journal_checkpoint::tests::replaying_an_old_producer_put_keeps_the_current_validator \
-- --exact
cargo test -p cmux-tui-core --test browser_runtime \
socket_browser_attach_streams_frames_input_and_cell_pixels \
-- --exact
- name: cargo test
working-directory: cmux-tui
run: cargo test --workspace --locked
env:
MODE: ${{ inputs.mode }}
TEST_FILTER: ${{ inputs.test_filter }}
shell: bash
run: |
if [[ "$MODE" == "full" ]]; then
cargo test --workspace --exclude cmux-tui-core --locked
mkdir -p target
cargo test -p cmux-tui-core --locked --no-run --message-format=json \
> target/cmux-tui-core-test-binaries.jsonl
python3 ../.github/scripts/run-cmux-tui-core-tests-isolated.py \
target/cmux-tui-core-test-binaries.jsonl \
crates/cmux-tui-core
cargo test -p cmux-tui-core --doc --locked
exit 0
fi
cargo test --workspace --locked "$TEST_FILTER" -- --list \
| tee "$RUNNER_TEMP/cmux-tui-focused-tests.txt"
if ! grep -Eq ': test$' "$RUNNER_TEMP/cmux-tui-focused-tests.txt"; then
echo "::error::test_filter '$TEST_FILTER' selected no Rust tests" >&2
exit 1
fi
cargo test --workspace --locked "$TEST_FILTER"
- name: crossterm parser tests
if: inputs.mode == 'full'
working-directory: cmux-tui
run: cargo test --manifest-path vendor/crossterm/Cargo.toml --lib
- name: TUI smoke test (scripted pty)
if: inputs.mode == 'full'
working-directory: cmux-tui
run: |
cargo build -p cmux-tui
python3 scripts/smoke-tui.py
- name: Detach/reattach smoke test
if: inputs.mode == 'full'
working-directory: cmux-tui
run: python3 scripts/smoke-attach.py
bindings-e2e:
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
needs: validate-inputs
if: inputs.mode == 'full'
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 40
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ inputs.commit }}
- name: Require exact checkout
env:
EXACT_COMMIT: ${{ inputs.commit }}
run: test "$(git rev-parse HEAD)" = "$EXACT_COMMIT"
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.14.0"
cache: npm
cache-dependency-path: cmux-tui/bindings/typescript/package-lock.json
- name: Install TypeScript binding dependencies
working-directory: cmux-tui/bindings/typescript
run: npm ci --no-audit --no-fund
- name: Init ghostty submodule
run: git submodule update --init --depth 1 ghostty
@@ -206,13 +499,8 @@ jobs:
- name: Install zig
run: ./scripts/install-zig-ci.sh
- name: Rust version
run: |
rustc --version || true
if ! command -v cargo >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
- name: Set up pinned Rust
uses: ./.github/actions/setup-cmux-tui-rust
- name: Java version
run: |
@@ -226,48 +514,98 @@ jobs:
working-directory: cmux-tui
run: cargo build -p cmux-tui
- name: Python conformance fixtures
run: python3 cmux-tui/bindings/conformance/runner.py
- name: Binding e2e
run: bash cmux-tui/bindings/conformance/e2e.sh --require python,typescript,rust,go,java
windows-experimental:
name: windows experimental (x86_64-gnu)
runs-on: windows-latest
timeout-minutes: 40
continue-on-error: true
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Init ghostty submodule
run: git submodule update --init --depth 1 ghostty
- name: Resolve Ghostty Zig version
id: ghostty-zig-version
- name: Resolve Zig SDK version
id: zig-sdk-version
shell: bash
run: |
version="$(bash ./scripts/ghostty-zig-version.sh)"
version="$(
sed -nE 's/^[[:space:]]*\.minimum_zig_version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' \
cmux-tui/bindings/zig/build.zig.zon | head -1
)"
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid Zig SDK version: $version" >&2
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Install zig
uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # v2.0.5
with:
version: ${{ steps.ghostty-zig-version.outputs.version }}
- name: Install Zig for SDK conformance
env:
ZIG_REQUIRED: ${{ steps.zig-sdk-version.outputs.version }}
ZIG_FORCE_LOCAL_INSTALL: "1"
run: ./scripts/install-zig-ci.sh
- name: Install Rust GNU target
- name: Install TypeScript adapter build tools
working-directory: cmux-tui/bindings/typescript
run: npm ci --no-audit --no-fund
- name: Python conformance fixtures
run: |
test "$("$CMUX_ZIG" version)" = "${{ steps.zig-sdk-version.outputs.version }}"
python3 cmux-tui/bindings/conformance/runner.py
- name: Binding e2e
run: |
test "$("$CMUX_ZIG" version)" = "${{ steps.zig-sdk-version.outputs.version }}"
bash cmux-tui/bindings/conformance/e2e.sh --require python,typescript,rust,go,java
build-artifacts:
name: release-path dogfood artifacts
needs: validate-inputs
permissions:
contents: read
uses: ./.github/workflows/cmux-tui-build-package.yml
with:
version: 0.0.0-nightly.19700101.0
pypi_version: 0.0.0.dev197001010
package_npm: ${{ inputs.mode == 'full' }}
package_pypi: ${{ inputs.mode == 'full' }}
include_windows: ${{ inputs.mode == 'full' }}
target_set: ${{ inputs.mode == 'full' && 'all' || 'macos-arm64' }}
build_cloudflare_relay: false
verify_linux_arm64: ${{ inputs.mode == 'full' }}
macos_runner: blacksmith-6vcpu-macos-15
linux_runner: blacksmith-4vcpu-ubuntu-2404
windows_runner: windows-latest
checkout_ref: ${{ inputs.commit }}
hosted-verification:
name: ${{ inputs.mode == 'full' && 'hosted verification' || 'focused hosted verification' }}
if: always()
needs:
- validate-inputs
- web-frontend
- valgrind-leak-check
- test
- bindings-e2e
- build-artifacts
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5
env:
MODE: ${{ inputs.mode }}
VALIDATE_RESULT: ${{ needs.validate-inputs.result }}
WEB_RESULT: ${{ needs.web-frontend.result }}
VALGRIND_RESULT: ${{ needs.valgrind-leak-check.result }}
TEST_RESULT: ${{ needs.test.result }}
BINDINGS_RESULT: ${{ needs.bindings-e2e.result }}
ARTIFACT_RESULT: ${{ needs.build-artifacts.result }}
steps:
- name: Require selected verification jobs
shell: bash
run: |
rustup target add x86_64-pc-windows-gnu
printf '%s\n' 'C:\msys64\mingw64\bin' >> "$GITHUB_PATH"
require_success() {
local name="$1"
local result="$2"
if [[ "$result" != "success" ]]; then
echo "::error::$name finished with result '$result'" >&2
exit 1
fi
}
- name: Build libghostty-vt for Windows GNU
shell: bash
working-directory: ghostty
run: |
zig build -Demit-lib-vt=true -Demit-xcframework=false -Doptimize=ReleaseFast -Dtarget=x86_64-windows-gnu --prefix "$RUNNER_TEMP/ghostty-vt-win-gnu"
zig ar t "$RUNNER_TEMP/ghostty-vt-win-gnu/lib/ghostty-vt-static.lib" | head
- name: cargo build cmux-tui for Windows GNU
working-directory: cmux-tui
run: cargo build -p cmux-tui --target x86_64-pc-windows-gnu --locked
require_success "input validation" "$VALIDATE_RESULT"
require_success "Rust tests" "$TEST_RESULT"
require_success "dogfood artifact build" "$ARTIFACT_RESULT"
if [[ "$MODE" == "full" ]]; then
require_success "web frontend" "$WEB_RESULT"
require_success "Valgrind" "$VALGRIND_RESULT"
require_success "binding end-to-end tests" "$BINDINGS_RESULT"
fi
+9 -2
View File
@@ -452,7 +452,7 @@ jobs:
# The demo profile is fetched from the ASC API by name instead of a
# repository secret, so regenerating it in the developer portal
# needs no secret rotation. Same credentials the upload uses.
PROFILE_BASE64="$(python3 ./ios/scripts/asc_download_profile.py --name "cmux Demo Distribution")"
PROFILE_BASE64="$(python3 ./ios/scripts/asc_download_profile.py --name "cmux Demo Distribution Push")"
elif [ "$IOS_BETA_PROFILE_TYPE" = "internal" ]; then
PROFILE_BASE64="${IOS_BETA_PROVISIONING_PROFILE_INTERNAL_BASE64}"
else
@@ -475,9 +475,16 @@ jobs:
echo "$IOS_BETA_PROFILE_TYPE provisioning profile targets unexpected app ID: $APP_ID (expected $IOS_BETA_EXPECTED_APP_ID)" >&2
exit 1
fi
# TestFlight uses production APNs. Both capabilities must be present
# in the installed profile or export can silently strip them.
APS_ENVIRONMENT="$(/usr/libexec/PlistBuddy -c "Print :Entitlements:aps-environment" "$TMP_PLIST" 2>/dev/null || echo "")"
if [ -z "$APS_ENVIRONMENT" ] || [ "$APS_ENVIRONMENT" != "production" ]; then
echo "$IOS_BETA_PROFILE_TYPE provisioning profile aps-environment is '$APS_ENVIRONMENT', expected 'production'" >&2
echo "$IOS_BETA_PROFILE_TYPE provisioning profile aps-environment is '${APS_ENVIRONMENT:-<absent>}', expected 'production'" >&2
exit 1
fi
TIME_SENSITIVE="$(/usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.developer.usernotifications.time-sensitive" "$TMP_PLIST" 2>/dev/null || echo "")"
if [ "$TIME_SENSITIVE" != "true" ]; then
echo "$IOS_BETA_PROFILE_TYPE provisioning profile com.apple.developer.usernotifications.time-sensitive is '${TIME_SENSITIVE:-<absent>}', expected 'true'" >&2
exit 1
fi
PROFILE_NAME="$(/usr/libexec/PlistBuddy -c "Print :Name" "$TMP_PLIST")"
+14 -2
View File
@@ -55,9 +55,19 @@ jobs:
run: ./scripts/ci/run-iroh-tailscale-compatibility-gate.sh
simulator-e2e:
needs: tailscale-version-skew
env:
# The app gates compile optimized Swift for both endpoints. Xcode 26's
# AArch64 GlobalISel path is not reliable for that exact build shape, so
# use the same supported workaround as tagged cloud reloads and streamed
# validation instead of allowing a compiler failure to masquerade as a
# transport verdict.
CMUX_SWIFT_FRONTEND_WORKAROUND: "1"
strategy:
fail-fast: false
# The three app-backed modes share one staging account. Serial execution
# prevents one mode's zero-touch registration from replacing another
# mode's active Mac route while its authenticated RPC probe is running.
max-parallel: 1
matrix:
mode: ${{ fromJSON(inputs.mode == 'all' && '["automatic","relay-only","relay-expiry","direct-only","private-path"]' || format('["{0}"]', inputs.mode)) }}
runs-on: ${{ vars.MACOS_RUNNER_STREAMED_VALIDATION || 'warp-macos-15-arm64-6x' }}
@@ -120,6 +130,8 @@ jobs:
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: iroh-release-gate-${{ matrix.mode }}
path: ${{ runner.temp }}/iroh-release-gate-${{ matrix.mode }}.json
path: |
${{ runner.temp }}/iroh-release-gate-${{ matrix.mode }}.json
${{ runner.temp }}/iroh-release-gate-${{ matrix.mode }}-mac.cmuxdiag
if-no-files-found: warn
retention-days: 7
+1
View File
@@ -59,6 +59,7 @@ jobs:
name: cmux-ghostty-cli-helper
path: ghostty-cli-helper/ghostty
if-no-files-found: error
retention-days: 3
build-sign-notarize:
needs: build-ghostty-cli-helper
+639 -29
View File
@@ -63,6 +63,9 @@ jobs:
env:
# The ghostty CLI helper zig build is skipped; GhosttyKit comes prebuilt.
CMUX_SKIP_ZIG_BUILD: "1"
# A degraded cache must lose to a cold build instead of stalling this
# latency-sensitive lane for actions/cache's ten-minute default.
SEGMENT_DOWNLOAD_TIMEOUT_MINS: "2"
SWIFT_BACKTRACE: "interactive=no,timeout=0s,symbolicate=off,color=no"
steps:
- name: Checkout source ref
@@ -80,12 +83,333 @@ jobs:
set -euo pipefail
./scripts/select-ci-xcode.sh
- name: Install zig
run: ./scripts/install-zig-ci.sh
- name: Provision GhosttyKit (macOS)
- name: Prepare macOS cache metadata
if: ${{ inputs.platform == 'macos' }}
run: ./scripts/download-prebuilt-ghosttykit.sh || ./scripts/ensure-ghosttykit.sh
id: cache_meta
env:
SOURCE_REF: ${{ inputs.ref || github.ref_name }}
run: |
set -euo pipefail
xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
xcode_build="$(xcodebuild -version | awk 'NR == 2 { print $3 }')"
xcode_key="$(printf 'xcode-%s-%s' "$xcode_version" "$xcode_build" | tr -c 'A-Za-z0-9._-' '-')"
branch_name="${SOURCE_REF#refs/heads/}"
branch_name="${branch_name#refs/tags/}"
if [ -z "$branch_name" ]; then
branch_name="$(git branch --show-current)"
fi
if [ -z "$branch_name" ]; then
branch_name="detached"
fi
branch_slug="$(printf '%s' "$branch_name" | tr -c 'A-Za-z0-9._-' '-' | sed 's/-\{2,\}/-/g; s/^-//; s/-$//')"
branch_digest="$(printf '%s' "$branch_name" | shasum -a 256 | awk '{ print substr($1, 1, 12) }')"
if [ "$branch_name" = "main" ]; then
branch_key="main"
else
branch_key="${branch_slug:0:48}-${branch_digest}"
fi
# Xcode's SourcePackages workspace state and DerivedData build
# database contain absolute checkout paths. Keep exact-state caches
# tied to the path contract; a different runner workspace must cold
# resolve/build instead of consuming stale absolute paths.
workspace_digest="$(printf '%s' "$GITHUB_WORKSPACE" | shasum -a 256 | awk '{ print substr($1, 1, 12) }')"
workspace_key="workspace-${workspace_digest}"
runner_os="$(printf '%s' "$(uname -s)" | tr -c 'A-Za-z0-9._-' '-')"
runner_os_version="$(sw_vers -productVersion 2>/dev/null || uname -r)"
runner_os_version="$(printf '%s' "$runner_os_version" | tr -c 'A-Za-z0-9._-' '-')"
runner_arch="$(printf '%s' "$(uname -m)" | tr -c 'A-Za-z0-9._-' '-')"
runner_key="${runner_os}-${runner_os_version}-${runner_arch}"
resolved_file="cmux.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
source_sha="$(git rev-parse HEAD)"
# Cache entries are immutable. A run-scoped save key plus a stable
# source prefix lets a cold retry publish a newer repaired entry.
cache_run_key="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
spm_prefix="reload-build-macos-spm-v2-${xcode_key}-${workspace_key}-"
# DerivedData contains ONLY_ACTIVE_ARCH products and object files;
# never restore those across OS/architecture changes.
derived_prefix="reload-build-macos-deriveddata-v3-${xcode_key}-${runner_key}-${workspace_key}-"
if [ -f "$resolved_file" ]; then
resolved_hash="$(shasum -a 256 "$resolved_file" | awk '{ print $1 }')"
spm_cache_enabled=true
spm_key_base="${spm_prefix}${resolved_hash}"
spm_key="${spm_key_base}-run-${cache_run_key}"
spm_source_prefix="${spm_key_base}-run-"
else
echo "::warning::$resolved_file absent in source ref; disabling the SPM cache"
spm_cache_enabled=false
spm_key_base=""
spm_key=""
spm_source_prefix=""
fi
derived_data_key_base="${derived_prefix}${branch_key}-${source_sha}"
{
echo "xcode_key=$xcode_key"
echo "branch_key=$branch_key"
echo "workspace_key=$workspace_key"
echo "runner_key=$runner_key"
echo "source_sha=$source_sha"
echo "spm_cache_enabled=$spm_cache_enabled"
echo "spm_prefix=$spm_prefix"
echo "spm_key_base=$spm_key_base"
echo "spm_key=$spm_key"
echo "spm_source_prefix=$spm_source_prefix"
echo "derived_data_key_base=$derived_data_key_base"
echo "derived_data_key=${derived_data_key_base}-run-${cache_run_key}"
echo "derived_data_source_prefix=${derived_data_key_base}-run-"
echo "derived_data_branch_prefix=${derived_prefix}${branch_key}-"
echo "derived_data_main_prefix=${derived_prefix}main-"
} >> "$GITHUB_OUTPUT"
# The verified prebuilt is the happy path for both platforms. Zig is
# installed only after this attempt fails, immediately before the local
# ensure-ghosttykit.sh fallback that needs it.
- name: Download prebuilt GhosttyKit
id: ghosttykit_prebuilt
run: |
set -euo pipefail
start=$(date +%s)
# Keep a transient release outage from consuming the whole 60-minute
# job before the Zig-backed fallback gets a chance to run. The script
# still owns checksum/archive validation and preserves its normal
# defaults for local and fleet callers.
if GHOSTTYKIT_DOWNLOAD_RETRIES=2 \
GHOSTTYKIT_DOWNLOAD_RETRY_DELAY=5 \
GHOSTTYKIT_DOWNLOAD_MAX_TIME=60 \
./scripts/download-prebuilt-ghosttykit.sh; then
available=true
else
available=false
echo "::warning::Prebuilt GhosttyKit download failed; enabling the Zig-backed fallback"
fi
end=$(date +%s)
echo "available=$available" >> "$GITHUB_OUTPUT"
echo "seconds=$(( end - start ))" >> "$GITHUB_OUTPUT"
- name: Install zig for GhosttyKit fallback
if: ${{ steps.ghosttykit_prebuilt.outputs.available != 'true' }}
id: zig_fallback
run: |
set -euo pipefail
start=$(date +%s)
./scripts/install-zig-ci.sh
end=$(date +%s)
seconds="$(awk -v start="$start" -v end="$end" 'BEGIN { print end - start }')"
echo "seconds=$seconds" >> "$GITHUB_OUTPUT"
- name: Build fallback GhosttyKit
if: ${{ steps.ghosttykit_prebuilt.outputs.available != 'true' }}
id: ghosttykit_fallback
env:
# The bounded download step already proved the prebuilt path failed;
# do not let ensure-ghosttykit retry that URL with its long defaults.
CMUX_GHOSTTYKIT_NO_PREBUILT: "1"
run: |
set -euo pipefail
start=$(date +%s)
./scripts/ensure-ghosttykit.sh
end=$(date +%s)
echo "seconds=$(( end - start ))" >> "$GITHUB_OUTPUT"
- name: Verify provisioned GhosttyKit
run: test -d GhosttyKit.xcframework
- name: Start SPM cache restore timer
if: ${{ inputs.platform == 'macos' && steps.cache_meta.outputs.spm_cache_enabled == 'true' }}
id: spm_restore_start
run: echo "epoch=$(date +%s)" >> "$GITHUB_OUTPUT"
- name: Restore SPM SourcePackages cache
if: ${{ inputs.platform == 'macos' && steps.cache_meta.outputs.spm_cache_enabled == 'true' }}
id: restore_spm
continue-on-error: true
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .ci-source-packages
key: ${{ steps.cache_meta.outputs.spm_key }}
restore-keys: ${{ steps.cache_meta.outputs.spm_source_prefix }}
- name: Finish SPM cache restore
if: ${{ always() && inputs.platform == 'macos' }}
id: spm_restore
env:
CACHE_ENABLED: ${{ steps.cache_meta.outputs.spm_cache_enabled }}
CACHE_SOURCE_PREFIX: ${{ steps.cache_meta.outputs.spm_source_prefix }}
START_EPOCH: ${{ steps.spm_restore_start.outputs.epoch }}
CACHE_OUTCOME: ${{ steps.restore_spm.outcome }}
CACHE_HIT: ${{ steps.restore_spm.outputs.cache-hit }}
CACHE_MATCHED_KEY: ${{ steps.restore_spm.outputs.cache-matched-key }}
run: |
set -euo pipefail
now=$(date +%s)
start=${START_EPOCH:-0}
if ! [[ "$start" =~ ^[0-9]+$ ]] || [ "$start" -eq 0 ]; then
start=$now
fi
cache_hit=${CACHE_HIT:-false}
matched_key=${CACHE_MATCHED_KEY:-}
exact_hit=false
if [ "${CACHE_ENABLED:-false}" != "true" ]; then
status=disabled_missing_resolved
rm -rf "$GITHUB_WORKSPACE/.ci-source-packages"
elif [ "${CACHE_OUTCOME:-failure}" != "success" ]; then
status=restore_error
rm -rf "$GITHUB_WORKSPACE/.ci-source-packages"
elif [ "$cache_hit" = "true" ] || { [ -n "${CACHE_SOURCE_PREFIX:-}" ] && [[ "$matched_key" == "$CACHE_SOURCE_PREFIX"* ]]; }; then
status=exact_hit
exact_hit=true
elif [ -n "$matched_key" ]; then
status=fallback_hit
else
status=miss
fi
mkdir -p "$GITHUB_WORKSPACE/.ci-source-packages"
{
echo "seconds=$(( now - start ))"
echo "status=$status"
echo "exact_hit=$exact_hit"
echo "matched_key=$matched_key"
} >> "$GITHUB_OUTPUT"
- name: Sanitize restored SPM cache
if: ${{ inputs.platform == 'macos' }}
id: spm_sanitize
env:
CACHE_ENABLED: ${{ steps.cache_meta.outputs.spm_cache_enabled }}
CACHE_STATUS: ${{ steps.spm_restore.outputs.status }}
run: |
set -euo pipefail
status=${CACHE_STATUS:-restore_error}
discarded=false
sanitizer=scripts/ci/sanitize-xcode-source-packages-cache.py
if [ "${CACHE_ENABLED:-false}" != "true" ]; then
status=disabled_missing_resolved
elif [ "$status" = "exact_hit" ]; then
# The exact key is tied to Package.resolved, Xcode, source SHA, and
# the fixed Blacksmith checkout path. Preserve its workspace state
# so Xcode can reuse the resolved package graph; a bad state still
# trips the guarded cold retry below.
echo "preserving exact-hit SourcePackages workspace state"
elif [ ! -f "$sanitizer" ]; then
echo "::warning::$sanitizer absent in source ref; discarding the restored SPM cache"
status=sanitize_error
discarded=true
elif ! python3 "$sanitizer" .ci-source-packages; then
echo "::warning::SPM cache sanitize failed; discarding it and building cold"
status=sanitize_error
discarded=true
fi
if [ "$discarded" = "true" ]; then
rm -rf "$GITHUB_WORKSPACE/.ci-source-packages"
mkdir -p "$GITHUB_WORKSPACE/.ci-source-packages"
fi
echo "status=$status" >> "$GITHUB_OUTPUT"
echo "discarded=$discarded" >> "$GITHUB_OUTPUT"
- name: Start DerivedData cache restore timer
if: ${{ inputs.platform == 'macos' }}
id: derived_data_restore_start
run: echo "epoch=$(date +%s)" >> "$GITHUB_OUTPUT"
- name: Restore DerivedData cache
if: ${{ inputs.platform == 'macos' }}
id: restore_derived_data
continue-on-error: true
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
# Keep the build database, compiler outputs, and Debug products that
# Xcode needs for a true incremental build. Archives, indexes, logs,
# SourcePackages, and every non-Debug product remain excluded.
path: |
.ci-reload-derived-data/Build/Intermediates.noindex
.ci-reload-derived-data/Build/Products/Debug
.ci-reload-derived-data/ModuleCache.noindex
.ci-reload-derived-data/SDKStatCaches.noindex
key: ${{ steps.cache_meta.outputs.derived_data_key }}
restore-keys: |
${{ steps.cache_meta.outputs.derived_data_source_prefix }}
${{ steps.cache_meta.outputs.derived_data_branch_prefix }}
${{ steps.cache_meta.outputs.derived_data_main_prefix }}
- name: Finish DerivedData cache restore
if: ${{ always() && inputs.platform == 'macos' }}
id: derived_data_restore
env:
CACHE_SOURCE_PREFIX: ${{ steps.cache_meta.outputs.derived_data_source_prefix }}
START_EPOCH: ${{ steps.derived_data_restore_start.outputs.epoch }}
CACHE_OUTCOME: ${{ steps.restore_derived_data.outcome }}
CACHE_HIT: ${{ steps.restore_derived_data.outputs.cache-hit }}
CACHE_MATCHED_KEY: ${{ steps.restore_derived_data.outputs.cache-matched-key }}
run: |
set -euo pipefail
now=$(date +%s)
start=${START_EPOCH:-0}
if ! [[ "$start" =~ ^[0-9]+$ ]] || [ "$start" -eq 0 ]; then
start=$now
fi
cache_hit=${CACHE_HIT:-false}
matched_key=${CACHE_MATCHED_KEY:-}
exact_hit=false
if [ "${CACHE_OUTCOME:-failure}" != "success" ]; then
status=restore_error
rm -rf "$GITHUB_WORKSPACE/.ci-reload-derived-data"
elif [ "$cache_hit" = "true" ] || { [ -n "${CACHE_SOURCE_PREFIX:-}" ] && [[ "$matched_key" == "$CACHE_SOURCE_PREFIX"* ]]; }; then
status=exact_hit
exact_hit=true
elif [ -n "$matched_key" ]; then
status=fallback_hit
else
status=miss
fi
mkdir -p "$GITHUB_WORKSPACE/.ci-reload-derived-data"
{
echo "seconds=$(( now - start ))"
echo "status=$status"
echo "exact_hit=$exact_hit"
echo "matched_key=$matched_key"
} >> "$GITHUB_OUTPUT"
# actions/checkout gives every tracked source a fresh mtime, newer than
# restored compiler outputs. Xcode then recompiles an exact cache hit as
# if it were cold. Derive a stable, pre-2020 mtime from each Git blob id:
# unchanged files stay unchanged across runs, while changed blobs receive
# a different file signature and are still invalidated correctly.
- name: Normalize tracked source mtimes for Xcode
if: ${{ inputs.platform == 'macos' }}
run: |
set -euo pipefail
python3 - <<'PY'
import os
import subprocess
# 2001-01-01 plus at most fifteen years keeps source mtimes safely
# behind newly generated outputs while preserving nanosecond entropy.
base_seconds = 978_307_200
span_seconds = 15 * 365 * 24 * 60 * 60
records = subprocess.check_output(
["git", "ls-files", "--recurse-submodules", "--stage", "-z"]
).split(b"\0")
normalized = 0
for record in records:
if not record:
continue
metadata, raw_path = record.split(b"\t", 1)
mode, object_id, stage = metadata.split()
if mode == b"160000" or stage != b"0":
continue
path = os.fsdecode(raw_path)
if not os.path.lexists(path):
continue
digest = object_id.decode("ascii")
seconds = base_seconds + int(digest[:12], 16) % span_seconds
nanoseconds = int(digest[12:20], 16) % 1_000_000_000
mtime_ns = seconds * 1_000_000_000 + nanoseconds
os.utime(path, ns=(mtime_ns, mtime_ns), follow_symlinks=False)
normalized += 1
print(f"normalized {normalized} Git-tracked source mtimes")
PY
- name: Mark deps-ready
id: t1
@@ -95,12 +419,64 @@ jobs:
- name: Build tagged macOS app
if: ${{ inputs.platform == 'macos' }}
id: build_macos
env:
BUILD_TAG: ${{ inputs.tag }}
CMUX_GHOSTTYKIT_PREPROVISIONED: "1"
CMUX_SOURCE_PACKAGES_DIR: ${{ github.workspace }}/.ci-source-packages
SPM_CACHE_STATUS: ${{ steps.spm_sanitize.outputs.status }}
DERIVED_DATA_CACHE_STATUS: ${{ steps.derived_data_restore.outputs.status }}
run: |
set -euo pipefail
# Per-run log path: /tmp/reload.log collides across tenants on the
# multi-tenant self-hosted fleet Macs (tee: Permission denied).
log="$RUNNER_TEMP/reload.log"
./scripts/reload.sh --tag "${{ inputs.tag }}" --swift-frontend-workaround 2>&1 | tee "$log"
derived_data="$GITHUB_WORKSPACE/.ci-reload-derived-data"
echo "cache_retry=false" >> "$GITHUB_OUTPUT"
run_reload() {
if [ "$SPM_CACHE_STATUS" = "exact_hit" ]; then
# The exact Package.resolved/state pair is safe to reuse. If it
# is stale or corrupt, the caller clears both caches and changes
# this marker before retrying from a normal cold resolve.
export CMUX_DISABLE_AUTOMATIC_PACKAGE_RESOLUTION=1
else
export CMUX_DISABLE_AUTOMATIC_PACKAGE_RESOLUTION=0
fi
./scripts/reload.sh \
--tag "$BUILD_TAG" \
--derived-data "$derived_data" \
--swift-frontend-workaround
}
set +e
run_reload 2>&1 | tee "$log"
build_status=${PIPESTATUS[0]}
set -e
# A successfully restored but stale/corrupt cache must cost at most
# one failed attempt: clear both cache roots and retry once from cold.
if [ "$build_status" -ne 0 ] && {
[ "$SPM_CACHE_STATUS" = "exact_hit" ] ||
[ "$SPM_CACHE_STATUS" = "fallback_hit" ] ||
[ "$DERIVED_DATA_CACHE_STATUS" = "exact_hit" ] ||
[ "$DERIVED_DATA_CACHE_STATUS" = "fallback_hit" ];
}; then
echo "::warning::Cached macOS build failed; clearing SPM and DerivedData caches and retrying cold"
rm -rf "$CMUX_SOURCE_PACKAGES_DIR" "$derived_data"
mkdir -p "$CMUX_SOURCE_PACKAGES_DIR" "$derived_data"
SPM_CACHE_STATUS=miss
DERIVED_DATA_CACHE_STATUS=miss
: > "$log"
set +e
run_reload 2>&1 | tee "$log"
build_status=${PIPESTATUS[0]}
set -e
echo "cache_retry=true" >> "$GITHUB_OUTPUT"
fi
if [ "$build_status" -ne 0 ]; then
exit "$build_status"
fi
app_path="$(awk '/^App path:/{getline; sub(/^ /,""); print; exit}' "$log")"
[ -n "$app_path" ] && [ -d "$app_path" ] || { echo "could not locate built app" >&2; exit 1; }
echo "app_path=$app_path" >> "$GITHUB_OUTPUT"
@@ -133,6 +509,7 @@ jobs:
id: build_ios
env:
BUILD_TAG: ${{ inputs.tag }}
CMUX_XCODEBUILD_NONINTERACTIVE_HEARTBEAT_SECONDS: "60"
run: |
set -euo pipefail
slug="$(printf '%s' "$BUILD_TAG" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/-\{2,\}/-/g; s/^-//; s/-$//')"
@@ -144,6 +521,16 @@ jobs:
api_base_url="${CMUX_IOS_API_BASE_URL:-${CMUX_DEV_API_BASE_URL:-https://cmux-staging.vercel.app}}"
iroh_broker_base_url="${CMUX_IOS_IROH_BROKER_BASE_URL:-${CMUX_IROH_BROKER_BASE_URL:-https://cmux-staging.vercel.app}}"
# Match ios/scripts/reload.sh --swift-frontend-workaround. Optimized
# Debug builds need this Xcode 26 codegen path disabled on every lane.
# shellcheck disable=SC2016 # Xcode expands $(inherited), not this shell.
swift_workaround_args=(
SWIFT_ENABLE_BATCH_MODE=NO
DEBUG_INFORMATION_FORMAT=
GCC_GENERATE_DEBUGGING_SYMBOLS=NO
'OTHER_SWIFT_FLAGS=$(inherited) -Xllvm -aarch64-enable-global-isel-at-O=-1'
)
# Register the iOS platform if the runner only has macOS provisioned.
ios_ready() { xcrun simctl runtime list 2>/dev/null | grep -qiE "iOS [0-9].*\(Ready\)"; }
if ! ios_ready; then
@@ -152,12 +539,12 @@ jobs:
ios_ready || { echo "iOS platform still not registered; archive would fail" >&2; exit 1; }
fi
./scripts/ensure-ghosttykit.sh
test -d GhosttyKit.xcframework
out="$GITHUB_WORKSPACE/build"
mkdir -p "$out"
archive="$out/cmux-ios-$slug.xcarchive"
rm -rf "$archive"
xcodebuild archive \
scripts/ci/xcodebuild_noninteractive.py xcodebuild archive \
-workspace ios/cmux.xcworkspace \
-scheme cmux-ios \
-configuration Debug \
@@ -176,18 +563,22 @@ jobs:
CODE_SIGN_IDENTITY="" \
SWIFT_OPTIMIZATION_LEVEL=-O \
SWIFT_COMPILATION_MODE=wholemodule \
GCC_OPTIMIZATION_LEVEL=s
GCC_OPTIMIZATION_LEVEL=s \
"${swift_workaround_args[@]}"
[ -d "$archive" ] || { echo "archive not produced: $archive" >&2; exit 1; }
# Keep Blacksmith device reloads equivalent to the fleet path: one
# build supplies both the unsigned phone archive and the exact same
# source revision for an isolated Simulator verification.
xcodebuild build \
# The prebuilt GhosttyKit simulator slice is arm64-only.
scripts/ci/xcodebuild_noninteractive.py xcodebuild build \
-workspace ios/cmux.xcworkspace \
-scheme cmux-ios \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$RUNNER_TEMP/cmux-ios-dd" \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES \
PRODUCT_BUNDLE_IDENTIFIER="$bundle_id" \
PRODUCT_DISPLAY_NAME="$display_name" \
CMUX_GIT_SHA="$(git rev-parse --short HEAD)" \
@@ -200,7 +591,8 @@ jobs:
CODE_SIGN_IDENTITY="" \
SWIFT_OPTIMIZATION_LEVEL=-O \
SWIFT_COMPILATION_MODE=wholemodule \
GCC_OPTIMIZATION_LEVEL=s
GCC_OPTIMIZATION_LEVEL=s \
"${swift_workaround_args[@]}"
sim_app="$RUNNER_TEMP/cmux-ios-dd/Build/Products/Debug-iphonesimulator/cmux.app"
[ -d "$sim_app" ] || { echo "simulator app not produced: $sim_app" >&2; exit 1; }
@@ -212,39 +604,257 @@ jobs:
ditto "$sim_app" "$pkg/simulator/cmux.app"
ditto -c -k "$pkg" "$GITHUB_WORKSPACE/artifact/archive.zip"
- name: Mark build-ready
id: t2
run: echo "epoch=$(date +%s)" >> "$GITHUB_OUTPUT"
- name: Trim tag-specific products before DerivedData cache save
if: ${{ success() && inputs.platform == 'macos' }}
run: |
set -euo pipefail
products="$GITHUB_WORKSPACE/.ci-reload-derived-data/Build/Products/Debug"
[ -d "$products" ] || exit 0
# reload.sh stages the requested tagged app beside the stable base
# product. Keep only the base product in the cache; otherwise every
# tag accumulates another app bundle under the branch/SHA key.
for product in "$products"/cmux\ DEV\ *.app "$products"/.cmux\ DEV\ *.reload-*.app; do
[ -d "$product" ] || continue
[ "$(basename "$product")" = "cmux DEV.app" ] || rm -rf -- "$product"
done
- name: Start SPM cache save timer
if: ${{ success() && inputs.platform == 'macos' && steps.cache_meta.outputs.spm_cache_enabled == 'true' && (steps.spm_restore.outputs.exact_hit != 'true' || steps.spm_sanitize.outputs.discarded == 'true' || steps.build_macos.outputs.cache_retry == 'true') }}
id: spm_save_start
run: echo "epoch=$(date +%s)" >> "$GITHUB_OUTPUT"
- name: Save SPM SourcePackages cache
if: ${{ success() && inputs.platform == 'macos' && steps.cache_meta.outputs.spm_cache_enabled == 'true' && (steps.spm_restore.outputs.exact_hit != 'true' || steps.spm_sanitize.outputs.discarded == 'true' || steps.build_macos.outputs.cache_retry == 'true') }}
id: save_spm
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .ci-source-packages
key: ${{ steps.cache_meta.outputs.spm_key }}
- name: Finish SPM cache save
if: ${{ always() && inputs.platform == 'macos' }}
id: spm_save
env:
CACHE_ENABLED: ${{ steps.cache_meta.outputs.spm_cache_enabled }}
START_EPOCH: ${{ steps.spm_save_start.outputs.epoch }}
SAVE_OUTCOME: ${{ steps.save_spm.outcome }}
RESTORE_EXACT_HIT: ${{ steps.spm_restore.outputs.exact_hit }}
run: |
set -euo pipefail
now=$(date +%s)
start=${START_EPOCH:-0}
if [[ "$start" =~ ^[0-9]+$ ]] && [ "$start" -gt 0 ]; then
seconds=$(( now - start ))
else
seconds=0
fi
case "${SAVE_OUTCOME:-skipped}" in
success) status=saved ;;
failure) status=save_error ;;
*)
if [ "${CACHE_ENABLED:-false}" != "true" ]; then
status=skipped_disabled
elif [ "${RESTORE_EXACT_HIT:-false}" = "true" ]; then
status=skipped_exact_hit
else
status=skipped
fi
;;
esac
echo "seconds=$seconds" >> "$GITHUB_OUTPUT"
echo "status=$status" >> "$GITHUB_OUTPUT"
- name: Start DerivedData cache save timer
if: ${{ success() && inputs.platform == 'macos' && (steps.derived_data_restore.outputs.exact_hit != 'true' || steps.build_macos.outputs.cache_retry == 'true') }}
id: derived_data_save_start
run: echo "epoch=$(date +%s)" >> "$GITHUB_OUTPUT"
- name: Save DerivedData cache
if: ${{ success() && inputs.platform == 'macos' && (steps.derived_data_restore.outputs.exact_hit != 'true' || steps.build_macos.outputs.cache_retry == 'true') }}
id: save_derived_data
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
# Match the bounded restore set; only Debug products are included.
path: |
.ci-reload-derived-data/Build/Intermediates.noindex
.ci-reload-derived-data/Build/Products/Debug
.ci-reload-derived-data/ModuleCache.noindex
.ci-reload-derived-data/SDKStatCaches.noindex
key: ${{ steps.cache_meta.outputs.derived_data_key }}
- name: Finish DerivedData cache save
if: ${{ always() && inputs.platform == 'macos' }}
id: derived_data_save
env:
START_EPOCH: ${{ steps.derived_data_save_start.outputs.epoch }}
SAVE_OUTCOME: ${{ steps.save_derived_data.outcome }}
RESTORE_EXACT_HIT: ${{ steps.derived_data_restore.outputs.exact_hit }}
run: |
set -euo pipefail
now=$(date +%s)
start=${START_EPOCH:-0}
if [[ "$start" =~ ^[0-9]+$ ]] && [ "$start" -gt 0 ]; then
seconds=$(( now - start ))
else
seconds=0
fi
case "${SAVE_OUTCOME:-skipped}" in
success) status=saved ;;
failure) status=save_error ;;
*)
if [ "${RESTORE_EXACT_HIT:-false}" = "true" ]; then
status=skipped_exact_hit
else
status=skipped
fi
;;
esac
echo "seconds=$seconds" >> "$GITHUB_OUTPUT"
echo "status=$status" >> "$GITHUB_OUTPUT"
- name: Write timings.json
if: ${{ always() }}
env:
TIMING_TAG: ${{ inputs.tag }}
TIMING_PLATFORM: ${{ inputs.platform }}
TIMING_RUNNER: ${{ inputs.runner }}
TIMING_REF: ${{ inputs.ref || github.ref }}
TIMING_T0: ${{ steps.t0.outputs.epoch || '0' }}
TIMING_T1: ${{ steps.t1.outputs.epoch || '0' }}
TIMING_T2: ${{ steps.t2.outputs.epoch || '0' }}
GHOSTTYKIT_PREBUILT: ${{ steps.ghosttykit_prebuilt.outputs.available || 'false' }}
GHOSTTYKIT_DOWNLOAD_SECONDS: ${{ steps.ghosttykit_prebuilt.outputs.seconds || '0' }}
ZIG_INSTALL_SECONDS: ${{ steps.zig_fallback.outputs.seconds || '0' }}
GHOSTTYKIT_FALLBACK_SECONDS: ${{ steps.ghosttykit_fallback.outputs.seconds || '0' }}
CACHE_XCODE_KEY: ${{ steps.cache_meta.outputs.xcode_key }}
CACHE_BRANCH_KEY: ${{ steps.cache_meta.outputs.branch_key }}
CACHE_WORKSPACE_KEY: ${{ steps.cache_meta.outputs.workspace_key }}
CACHE_RUNNER_KEY: ${{ steps.cache_meta.outputs.runner_key }}
SPM_CACHE_PRIMARY_KEY: ${{ steps.cache_meta.outputs.spm_key }}
SPM_CACHE_STATUS: ${{ steps.spm_sanitize.outputs.status || 'not_applicable' }}
SPM_CACHE_EXACT_HIT: ${{ steps.spm_sanitize.outputs.status == 'exact_hit' }}
SPM_CACHE_MATCHED_KEY: ${{ steps.spm_restore.outputs.matched_key }}
SPM_CACHE_RESTORE_OUTCOME: ${{ steps.restore_spm.outcome || 'not_applicable' }}
SPM_CACHE_RESTORE_SECONDS: ${{ steps.spm_restore.outputs.seconds || '0' }}
SPM_CACHE_SAVE_STATUS: ${{ steps.spm_save.outputs.status || 'not_applicable' }}
SPM_CACHE_SAVE_SECONDS: ${{ steps.spm_save.outputs.seconds || '0' }}
DERIVED_DATA_CACHE_PRIMARY_KEY: ${{ steps.cache_meta.outputs.derived_data_key }}
DERIVED_DATA_CACHE_STATUS: ${{ steps.derived_data_restore.outputs.status || 'not_applicable' }}
DERIVED_DATA_CACHE_EXACT_HIT: ${{ steps.derived_data_restore.outputs.exact_hit || 'false' }}
DERIVED_DATA_CACHE_MATCHED_KEY: ${{ steps.derived_data_restore.outputs.matched_key }}
DERIVED_DATA_CACHE_RESTORE_OUTCOME: ${{ steps.restore_derived_data.outcome || 'not_applicable' }}
DERIVED_DATA_CACHE_RESTORE_SECONDS: ${{ steps.derived_data_restore.outputs.seconds || '0' }}
DERIVED_DATA_CACHE_SAVE_STATUS: ${{ steps.derived_data_save.outputs.status || 'not_applicable' }}
DERIVED_DATA_CACHE_SAVE_SECONDS: ${{ steps.derived_data_save.outputs.seconds || '0' }}
CACHE_COLD_RETRY: ${{ steps.build_macos.outputs.cache_retry || 'false' }}
run: |
set -euo pipefail
mkdir -p artifact
now=$(date +%s)
t0=${{ steps.t0.outputs.epoch }}
t1=${{ steps.t1.outputs.epoch || 0 }}
cat > artifact/timings.json <<JSON
{
"tag": "${{ inputs.tag }}",
"platform": "${{ inputs.platform }}",
"runner": "${{ inputs.runner }}",
"ref": "${{ inputs.ref }}",
"deps_seconds": $(( t1 > t0 ? t1 - t0 : 0 )),
"build_seconds": $(( t1 > 0 ? now - t1 : 0 )),
"post_checkout_total_seconds": $(( now - t0 ))
export TIMING_NOW=$now
python3 - <<'PY' > artifact/timings.json
import json
import os
import sys
def integer(name: str) -> int:
try:
return int(os.environ.get(name, "0"))
except ValueError:
return 0
def boolean(name: str) -> bool:
return os.environ.get(name, "false").lower() == "true"
t0 = integer("TIMING_T0")
t1 = integer("TIMING_T1")
t2 = integer("TIMING_T2")
now = integer("TIMING_NOW")
timing_clock_valid = t0 > 0
spm_status = os.environ["SPM_CACHE_STATUS"]
derived_status = os.environ["DERIVED_DATA_CACHE_STATUS"]
payload = {
"tag": os.environ["TIMING_TAG"],
"platform": os.environ["TIMING_PLATFORM"],
"runner": os.environ["TIMING_RUNNER"],
"ref": os.environ["TIMING_REF"],
"deps_seconds": t1 - t0 if timing_clock_valid and t1 > t0 else 0,
"build_seconds": t2 - t1 if timing_clock_valid and t1 > t0 and t2 > t1 else 0,
"post_checkout_total_seconds": now - t0 if timing_clock_valid and now > t0 else 0,
"ghosttykit_prebuilt": boolean("GHOSTTYKIT_PREBUILT"),
"ghosttykit_download_seconds": integer("GHOSTTYKIT_DOWNLOAD_SECONDS"),
"zig_install_seconds": integer("ZIG_INSTALL_SECONDS"),
"ghosttykit_fallback_seconds": integer("GHOSTTYKIT_FALLBACK_SECONDS"),
"cache_xcode_key": os.environ.get("CACHE_XCODE_KEY", ""),
"cache_branch_key": os.environ.get("CACHE_BRANCH_KEY", ""),
"cache_workspace_key": os.environ.get("CACHE_WORKSPACE_KEY", ""),
"cache_runner_key": os.environ.get("CACHE_RUNNER_KEY", ""),
"spm_cache_primary_key": os.environ.get("SPM_CACHE_PRIMARY_KEY", ""),
"spm_cache_status": spm_status,
"spm_cache_hit": spm_status in {"exact_hit", "fallback_hit"},
"spm_cache_exact_hit": boolean("SPM_CACHE_EXACT_HIT"),
"spm_cache_matched_key": os.environ.get("SPM_CACHE_MATCHED_KEY", ""),
"spm_cache_restore_outcome": os.environ["SPM_CACHE_RESTORE_OUTCOME"],
"spm_cache_restore_seconds": integer("SPM_CACHE_RESTORE_SECONDS"),
"spm_cache_save_status": os.environ["SPM_CACHE_SAVE_STATUS"],
"spm_cache_save_seconds": integer("SPM_CACHE_SAVE_SECONDS"),
"derived_data_cache_primary_key": os.environ.get("DERIVED_DATA_CACHE_PRIMARY_KEY", ""),
"derived_data_cache_status": derived_status,
"derived_data_cache_hit": derived_status in {"exact_hit", "fallback_hit"},
"derived_data_cache_exact_hit": boolean("DERIVED_DATA_CACHE_EXACT_HIT"),
"derived_data_cache_matched_key": os.environ.get("DERIVED_DATA_CACHE_MATCHED_KEY", ""),
"derived_data_cache_restore_outcome": os.environ["DERIVED_DATA_CACHE_RESTORE_OUTCOME"],
"derived_data_cache_restore_seconds": integer("DERIVED_DATA_CACHE_RESTORE_SECONDS"),
"derived_data_cache_save_status": os.environ["DERIVED_DATA_CACHE_SAVE_STATUS"],
"derived_data_cache_save_seconds": integer("DERIVED_DATA_CACHE_SAVE_SECONDS"),
"cache_cold_retry": boolean("CACHE_COLD_RETRY"),
}
JSON
json.dump(payload, fp=sys.stdout, indent=2, sort_keys=True)
print()
PY
t0=${TIMING_T0:-0}
t1=${TIMING_T1:-0}
t2=${TIMING_T2:-0}
if (( t0 > 0 && t1 > t0 )); then
deps_seconds=$(( t1 - t0 ))
else
deps_seconds=0
fi
if (( t0 > 0 && t1 > t0 && t2 > t1 )); then
build_seconds=$(( t2 - t1 ))
else
build_seconds=0
fi
if (( t0 > 0 && now > t0 )); then
total_seconds=$(( now - t0 ))
else
total_seconds=0
fi
{
echo "### reload-build timings"
echo ""
echo "- runner: \`${{ inputs.runner }}\`"
echo "- platform: \`${{ inputs.platform }}\`"
echo "- deps: $(( t1 > t0 ? t1 - t0 : 0 ))s"
echo "- build: $(( t1 > 0 ? now - t1 : 0 ))s"
echo "- post-checkout total: $(( now - t0 ))s"
echo "- runner: \`$TIMING_RUNNER\`"
echo "- platform: \`$TIMING_PLATFORM\`"
echo "- deps: ${deps_seconds}s"
echo "- build: ${build_seconds}s"
echo "- SPM cache: ${SPM_CACHE_STATUS} (restore ${SPM_CACHE_RESTORE_SECONDS}s, save ${SPM_CACHE_SAVE_SECONDS}s / ${SPM_CACHE_SAVE_STATUS})"
echo "- DerivedData cache: ${DERIVED_DATA_CACHE_STATUS} (restore ${DERIVED_DATA_CACHE_RESTORE_SECONDS}s, save ${DERIVED_DATA_CACHE_SAVE_SECONDS}s / ${DERIVED_DATA_CACHE_SAVE_STATUS})"
echo "- cache-triggered cold retry: ${CACHE_COLD_RETRY}"
echo "- GhosttyKit prebuilt: ${GHOSTTYKIT_PREBUILT} (download ${GHOSTTYKIT_DOWNLOAD_SECONDS}s, Zig fallback ${ZIG_INSTALL_SECONDS}s)"
echo "- post-checkout total: ${total_seconds}s"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload artifact
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: reload-${{ inputs.tag }}-${{ inputs.platform }}
path: artifact/
retention-days: 3
retention-days: 1
if-no-files-found: error
+630
View File
@@ -0,0 +1,630 @@
name: sdk bootstrap crates
on:
repository_dispatch:
types: [sdk-bootstrap-crates]
permissions: {}
concurrency:
group: sdk-bootstrap-crates
cancel-in-progress: false
env:
BOOTSTRAP_VERSION: "0.0.0-bootstrap.0"
RUST_TOOLCHAIN: "1.95.0"
jobs:
build:
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 15
permissions:
contents: read
outputs:
sdk_sha256: ${{ steps.package.outputs.sdk_sha256 }}
sidebar_sha256: ${{ steps.package.outputs.sidebar_sha256 }}
steps:
- name: Require explicit bootstrap confirmation
if: github.event.client_payload.confirm_bootstrap != true
run: |
echo "Refusing to reserve the Rust SDK crates without confirm_bootstrap=true." >&2
exit 1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 0
- name: Require current protected main
run: |
set -euo pipefail
[[ "$GITHUB_REF" == "refs/heads/main" ]] || {
echo "Dispatch sdk-bootstrap-crates.yml from main, found $GITHUB_REF." >&2
exit 1
}
git fetch --force origin main
main_sha="$(git rev-parse origin/main)"
[[ "$GITHUB_SHA" == "$main_sha" ]] || {
echo "workflow commit $GITHUB_SHA is not current main $main_sha" >&2
exit 1
}
- name: Install pinned Rust toolchain
run: |
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
rustup default "$RUST_TOOLCHAIN"
cargo --version
rustc --version
- name: Build and test the ownership bootstrap
id: package
run: |
set -euo pipefail
for specification in \
"cmux-sdk:rust-sdk:sdk_sha256" \
"cmux-sidebar:rust-sidebar:sidebar_sha256"; do
IFS=: read -r package source output_name <<< "$specification"
source_dir="cmux-tui/bindings/bootstrap/$source"
bootstrap_dir="$RUNNER_TEMP/$package-bootstrap"
cp -R "$source_dir" "$bootstrap_dir"
manifest="$bootstrap_dir/Cargo.toml"
cargo test --manifest-path "$manifest" --locked
cargo package --manifest-path "$manifest" --locked --no-verify
artifact="$bootstrap_dir/target/package/$package-$BOOTSTRAP_VERSION.crate"
[[ -f "$artifact" ]] || {
echo "$package bootstrap crate was not created" >&2
exit 1
}
verify_dir="$RUNNER_TEMP/$package-bootstrap-verify"
mkdir -p "$verify_dir"
tar -xzf "$artifact" -C "$verify_dir"
cargo test \
--manifest-path \
"$verify_dir/$package-$BOOTSTRAP_VERSION/Cargo.toml" \
--locked
artifact_dir="$RUNNER_TEMP/$package-bootstrap-artifact"
mkdir -p "$artifact_dir"
cp "$artifact" "$artifact_dir/"
artifact_sha256="$(sha256sum "$artifact" | cut -d ' ' -f 1)"
[[ "$artifact_sha256" =~ ^[0-9a-f]{64}$ ]] || {
echo "$package bootstrap crate digest is malformed" >&2
exit 1
}
echo "$output_name=$artifact_sha256" >> "$GITHUB_OUTPUT"
done
- name: Upload the cmux-sdk bootstrap crate
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cmux-sdk-bootstrap-crate
path: ${{ runner.temp }}/cmux-sdk-bootstrap-artifact
if-no-files-found: error
overwrite: true
- name: Upload the cmux-sidebar bootstrap crate
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cmux-sidebar-bootstrap-crate
path: ${{ runner.temp }}/cmux-sidebar-bootstrap-artifact
if-no-files-found: error
overwrite: true
preflight:
needs: build
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
- package: cmux-sdk
artifact: cmux-sdk-bootstrap-crate
decision: cmux-sdk-bootstrap-decision
- package: cmux-sidebar
artifact: cmux-sidebar-bootstrap-crate
decision: cmux-sidebar-bootstrap-decision
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: ${{ matrix.artifact }}
path: bootstrap-crate
- name: Inspect the crates.io bootstrap state
id: project
env:
PACKAGE: ${{ matrix.package }}
run: |
set -euo pipefail
metadata="$RUNNER_TEMP/$PACKAGE-bootstrap-registry.json"
status="$(
curl \
--silent \
--show-error \
--location \
--retry 5 \
--retry-delay 1 \
--retry-all-errors \
--user-agent 'cmux-sdk-bootstrap/1 (https://github.com/manaflow-ai/cmux; contact: https://github.com/manaflow-ai/cmux/issues)' \
--output "$metadata" \
--write-out '%{http_code}' \
"https://crates.io/api/v1/crates/$PACKAGE"
)"
case "$status" in
404)
echo "$PACKAGE is unclaimed; bootstrap may continue."
project_status=missing
;;
200)
echo "$PACKAGE exists; bootstrap bytes must match."
project_status=exists
;;
*)
echo "crates.io returned HTTP $status; refusing to infer availability." >&2
exit 1
;;
esac
echo "status=$project_status" >> "$GITHUB_OUTPUT"
sleep 1
- name: Reconcile an existing crates.io ownership bootstrap
if: steps.project.outputs.status == 'exists'
env:
PACKAGE: ${{ matrix.package }}
run: |
set -euo pipefail
shopt -s nullglob
artifacts=(bootstrap-crate/*.crate)
[[ "${#artifacts[@]}" == 1 ]] || {
echo "expected one tested crate, found ${#artifacts[@]}" >&2
exit 1
}
python3 cmux-tui/bindings/reconcile_registry_artifact.py check \
--registry crates \
--package "$PACKAGE" \
--version "$BOOTSTRAP_VERSION" \
--artifact "${artifacts[0]}" \
--require-match
sleep 1
python3 cmux-tui/bindings/verify_crates_ownership.py \
--package "$PACKAGE" \
--repository https://github.com/manaflow-ai/cmux \
--owner-id 431397 \
--owner-login lawrencecchen \
--bootstrap-ownership-only
- name: Record the credential-job decision
env:
PACKAGE: ${{ matrix.package }}
PROJECT_STATUS: ${{ steps.project.outputs.status }}
run: |
set -euo pipefail
case "$PROJECT_STATUS" in
missing) decision=publish ;;
exists) decision=skip ;;
*)
echo "unexpected $PACKAGE project state: $PROJECT_STATUS" >&2
exit 1
;;
esac
decision_dir="$RUNNER_TEMP/$PACKAGE-bootstrap-decision"
mkdir -p "$decision_dir"
printf '%s\n' "$decision" > "$decision_dir/decision.txt"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.decision }}
path: ${{ runner.temp }}/${{ matrix.package }}-bootstrap-decision
if-no-files-found: error
overwrite: true
decisions:
needs:
- preflight
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 5
permissions:
actions: read
outputs:
sdk_need_publish: ${{ steps.read.outputs.sdk_need_publish }}
sidebar_need_publish: ${{ steps.read.outputs.sidebar_need_publish }}
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: cmux-sdk-bootstrap-decision
path: sdk-decision
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: cmux-sidebar-bootstrap-decision
path: sidebar-decision
- name: Export protected-environment decisions
id: read
run: |
set -euo pipefail
read_decision() {
local path="$1"
local output_name="$2"
local decision
decision="$(cat "$path")"
case "$decision" in
publish) need_publish=true ;;
skip) need_publish=false ;;
*)
echo "invalid bootstrap publication decision: $decision" >&2
exit 1
;;
esac
echo "$output_name=$need_publish" >> "$GITHUB_OUTPUT"
}
read_decision sdk-decision/decision.txt sdk_need_publish
read_decision sidebar-decision/decision.txt sidebar_need_publish
publish-sdk:
needs:
- build
- preflight
- decisions
if: needs.decisions.outputs.sdk_need_publish == 'true'
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 10
permissions: {}
environment:
name: crates-bootstrap
url: https://crates.io/crates/cmux-sdk
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: cmux-sdk-bootstrap-crate
path: bootstrap-crate
- name: Install pinned Rust toolchain
run: |
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
rustup default "$RUST_TOOLCHAIN"
cargo --version
rustc --version
- name: Verify protected source and reproduce the tested crate
id: prepare
env:
EXPECTED_SHA256: ${{ needs.build.outputs.sdk_sha256 }}
PACKAGE: cmux-sdk
run: |
set -euo pipefail
[[ "$GITHUB_REPOSITORY" == "manaflow-ai/cmux" ]] || {
echo "bootstrap repository must be manaflow-ai/cmux" >&2
exit 1
}
[[ "$GITHUB_REF" == "refs/heads/main" ]] || {
echo "bootstrap credential job must run from main" >&2
exit 1
}
main_sha="$(
git ls-remote \
https://github.com/manaflow-ai/cmux.git \
refs/heads/main |
awk 'NR == 1 { print $1 }'
)"
[[ "$main_sha" == "$GITHUB_SHA" ]] || {
echo "workflow commit $GITHUB_SHA is not current main $main_sha" >&2
exit 1
}
[[ "$EXPECTED_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo "validated crate digest is malformed" >&2
exit 1
}
shopt -s nullglob
artifacts=(bootstrap-crate/*.crate)
[[ "${#artifacts[@]}" == 1 ]] || {
echo "expected one tested crate, found ${#artifacts[@]}" >&2
exit 1
}
BOOTSTRAP_ARTIFACT="${artifacts[0]}"
[[ "$(basename "$BOOTSTRAP_ARTIFACT")" == "$PACKAGE-$BOOTSTRAP_VERSION.crate" ]] || {
echo "unexpected bootstrap crate filename" >&2
exit 1
}
actual_sha256="$(sha256sum "$BOOTSTRAP_ARTIFACT" | cut -d ' ' -f 1)"
[[ "$actual_sha256" == "$EXPECTED_SHA256" ]] || {
echo "downloaded crates.io bootstrap artifact digest mismatch" >&2
exit 1
}
package_prefix="$PACKAGE-$BOOTSTRAP_VERSION"
publish_root="$RUNNER_TEMP/$PACKAGE-publish"
mkdir -p "$publish_root"
python3 - \
"$BOOTSTRAP_ARTIFACT" \
"$publish_root" \
"$package_prefix" <<'PY'
import pathlib
import shutil
import sys
import tarfile
archive_path = pathlib.Path(sys.argv[1])
publish_root = pathlib.Path(sys.argv[2])
package_prefix = sys.argv[3]
expected = {
f"{package_prefix}/Cargo.lock",
f"{package_prefix}/Cargo.toml",
f"{package_prefix}/Cargo.toml.orig",
f"{package_prefix}/README.md",
f"{package_prefix}/src/lib.rs",
}
with tarfile.open(archive_path, "r:gz") as archive:
members = archive.getmembers()
names = [member.name for member in members]
if len(names) != len(expected) or set(names) != expected:
raise SystemExit(
f"bootstrap crate paths differ from the allowlist: {names!r}"
)
for member in members:
if not member.isfile():
raise SystemExit(
f"bootstrap crate member is not a regular file: {member.name}"
)
source = archive.extractfile(member)
if source is None:
raise SystemExit(
f"bootstrap crate member cannot be read: {member.name}"
)
destination = publish_root / member.name
destination.parent.mkdir(parents=True, exist_ok=True)
with source, destination.open("wb") as output:
shutil.copyfileobj(source, output)
destination.chmod(member.mode & 0o777)
PY
package_root="$publish_root/$package_prefix"
cp "$package_root/Cargo.toml.orig" "$package_root/Cargo.toml"
cargo package \
--manifest-path "$package_root/Cargo.toml" \
--locked \
--no-verify
REPACKED_ARTIFACT="$package_root/target/package/$package_prefix.crate"
cmp "$BOOTSTRAP_ARTIFACT" "$REPACKED_ARTIFACT"
echo "manifest=$package_root/Cargo.toml" >> "$GITHUB_OUTPUT"
- name: Publish the exact tested ownership bootstrap
continue-on-error: true
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_BOOTSTRAP_TOKEN }}
PUBLISH_MANIFEST: ${{ steps.prepare.outputs.manifest }}
run: |
set -euo pipefail
[[ -n "$CARGO_REGISTRY_TOKEN" ]] || {
echo "crates-bootstrap environment secret CARGO_BOOTSTRAP_TOKEN is required." >&2
exit 1
}
cargo publish \
--manifest-path "$PUBLISH_MANIFEST" \
--locked \
--no-verify
publish-sidebar:
needs:
- build
- preflight
- decisions
- publish-sdk
if: >-
always() &&
!cancelled() &&
needs.build.result == 'success' &&
needs.preflight.result == 'success' &&
needs.decisions.result == 'success' &&
needs.decisions.outputs.sidebar_need_publish == 'true'
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 10
permissions: {}
environment:
name: crates-bootstrap
url: https://crates.io/crates/cmux-sidebar
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: cmux-sidebar-bootstrap-crate
path: bootstrap-crate
- name: Install pinned Rust toolchain
run: |
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
rustup default "$RUST_TOOLCHAIN"
cargo --version
rustc --version
- name: Verify protected source and reproduce the tested crate
id: prepare
env:
EXPECTED_SHA256: ${{ needs.build.outputs.sidebar_sha256 }}
PACKAGE: cmux-sidebar
run: |
set -euo pipefail
[[ "$GITHUB_REPOSITORY" == "manaflow-ai/cmux" ]] || {
echo "bootstrap repository must be manaflow-ai/cmux" >&2
exit 1
}
[[ "$GITHUB_REF" == "refs/heads/main" ]] || {
echo "bootstrap credential job must run from main" >&2
exit 1
}
main_sha="$(
git ls-remote \
https://github.com/manaflow-ai/cmux.git \
refs/heads/main |
awk 'NR == 1 { print $1 }'
)"
[[ "$main_sha" == "$GITHUB_SHA" ]] || {
echo "workflow commit $GITHUB_SHA is not current main $main_sha" >&2
exit 1
}
[[ "$EXPECTED_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo "validated crate digest is malformed" >&2
exit 1
}
shopt -s nullglob
artifacts=(bootstrap-crate/*.crate)
[[ "${#artifacts[@]}" == 1 ]] || {
echo "expected one tested crate, found ${#artifacts[@]}" >&2
exit 1
}
BOOTSTRAP_ARTIFACT="${artifacts[0]}"
[[ "$(basename "$BOOTSTRAP_ARTIFACT")" == "$PACKAGE-$BOOTSTRAP_VERSION.crate" ]] || {
echo "unexpected bootstrap crate filename" >&2
exit 1
}
actual_sha256="$(sha256sum "$BOOTSTRAP_ARTIFACT" | cut -d ' ' -f 1)"
[[ "$actual_sha256" == "$EXPECTED_SHA256" ]] || {
echo "downloaded crates.io bootstrap artifact digest mismatch" >&2
exit 1
}
package_prefix="$PACKAGE-$BOOTSTRAP_VERSION"
publish_root="$RUNNER_TEMP/$PACKAGE-publish"
mkdir -p "$publish_root"
python3 - \
"$BOOTSTRAP_ARTIFACT" \
"$publish_root" \
"$package_prefix" <<'PY'
import pathlib
import shutil
import sys
import tarfile
archive_path = pathlib.Path(sys.argv[1])
publish_root = pathlib.Path(sys.argv[2])
package_prefix = sys.argv[3]
expected = {
f"{package_prefix}/Cargo.lock",
f"{package_prefix}/Cargo.toml",
f"{package_prefix}/Cargo.toml.orig",
f"{package_prefix}/README.md",
f"{package_prefix}/src/lib.rs",
}
with tarfile.open(archive_path, "r:gz") as archive:
members = archive.getmembers()
names = [member.name for member in members]
if len(names) != len(expected) or set(names) != expected:
raise SystemExit(
f"bootstrap crate paths differ from the allowlist: {names!r}"
)
for member in members:
if not member.isfile():
raise SystemExit(
f"bootstrap crate member is not a regular file: {member.name}"
)
source = archive.extractfile(member)
if source is None:
raise SystemExit(
f"bootstrap crate member cannot be read: {member.name}"
)
destination = publish_root / member.name
destination.parent.mkdir(parents=True, exist_ok=True)
with source, destination.open("wb") as output:
shutil.copyfileobj(source, output)
destination.chmod(member.mode & 0o777)
PY
package_root="$publish_root/$package_prefix"
cp "$package_root/Cargo.toml.orig" "$package_root/Cargo.toml"
cargo package \
--manifest-path "$package_root/Cargo.toml" \
--locked \
--no-verify
REPACKED_ARTIFACT="$package_root/target/package/$package_prefix.crate"
cmp "$BOOTSTRAP_ARTIFACT" "$REPACKED_ARTIFACT"
echo "manifest=$package_root/Cargo.toml" >> "$GITHUB_OUTPUT"
- name: Publish the exact tested ownership bootstrap
continue-on-error: true
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_BOOTSTRAP_TOKEN }}
PUBLISH_MANIFEST: ${{ steps.prepare.outputs.manifest }}
run: |
set -euo pipefail
[[ -n "$CARGO_REGISTRY_TOKEN" ]] || {
echo "crates-bootstrap environment secret CARGO_BOOTSTRAP_TOKEN is required." >&2
exit 1
}
cargo publish \
--manifest-path "$PUBLISH_MANIFEST" \
--locked \
--no-verify
verify:
needs:
- build
- preflight
- decisions
- publish-sdk
- publish-sidebar
if: >-
always() &&
needs.build.result == 'success' &&
needs.preflight.result == 'success' &&
needs.decisions.result == 'success'
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
- package: cmux-sdk
artifact: cmux-sdk-bootstrap-crate
- package: cmux-sidebar
artifact: cmux-sidebar-bootstrap-crate
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: ${{ matrix.artifact }}
path: bootstrap-crate
- name: Reconcile the exact crates.io ownership bootstrap
env:
PACKAGE: ${{ matrix.package }}
run: |
set -euo pipefail
shopt -s nullglob
artifacts=(bootstrap-crate/*.crate)
[[ "${#artifacts[@]}" == 1 ]] || {
echo "expected one tested crate, found ${#artifacts[@]}" >&2
exit 1
}
retry_missing_project=()
if [[ "$PACKAGE" == "cmux-sidebar" ]]; then
retry_missing_project=(--retry-missing-project)
fi
python3 cmux-tui/bindings/reconcile_registry_artifact.py check \
--registry crates \
--package "$PACKAGE" \
--version "$BOOTSTRAP_VERSION" \
--artifact "${artifacts[0]}" \
"${retry_missing_project[@]}" \
--wait-seconds 300 \
--require-match
sleep 1
python3 cmux-tui/bindings/verify_crates_ownership.py \
--package "$PACKAGE" \
--repository https://github.com/manaflow-ai/cmux \
--owner-id 431397 \
--owner-login lawrencecchen \
--bootstrap-ownership-only
+347
View File
@@ -0,0 +1,347 @@
name: sdk bootstrap npm
on:
repository_dispatch:
types: [sdk-bootstrap-npm]
permissions: {}
concurrency:
group: sdk-bootstrap-npm
cancel-in-progress: false
env:
BOOTSTRAP_VERSION: "0.0.0-bootstrap.0"
jobs:
build:
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 15
permissions:
contents: read
outputs:
artifact_sha256: ${{ steps.package.outputs.artifact_sha256 }}
steps:
- name: Require explicit bootstrap confirmation
if: github.event.client_payload.confirm_bootstrap != true
run: |
echo "Refusing to reserve cmux-sdk without confirm_bootstrap=true." >&2
exit 1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 0
- name: Require current protected main
run: |
set -euo pipefail
[[ "$GITHUB_REF" == "refs/heads/main" ]] || {
echo "Dispatch sdk-bootstrap-npm.yml from main, found $GITHUB_REF." >&2
exit 1
}
git fetch --force origin main
main_sha="$(git rev-parse origin/main)"
[[ "$GITHUB_SHA" == "$main_sha" ]] || {
echo "workflow commit $GITHUB_SHA is not current main $main_sha" >&2
exit 1
}
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.14.0"
package-manager-cache: false
- name: Install pinned npm
run: npm install --global --ignore-scripts [email protected]
- name: Build, test, and pack the bootstrap prerelease
id: package
working-directory: cmux-tui/bindings/typescript
run: |
set -euo pipefail
npm ci --no-audit --no-fund
npm version "$BOOTSTRAP_VERSION" --no-git-tag-version
npm test
mkdir -p "$RUNNER_TEMP/cmux-npm-bootstrap"
npm pack --pack-destination "$RUNNER_TEMP/cmux-npm-bootstrap"
shopt -s nullglob
packages=("$RUNNER_TEMP"/cmux-npm-bootstrap/*.tgz)
[[ "${#packages[@]}" == 1 ]] || {
echo "expected one bootstrap artifact, found ${#packages[@]}" >&2
exit 1
}
CMUX_NPM_PACKAGE="${packages[0]}" \
node scripts/verify-packaged-consumer.mjs
artifact_sha256="$(sha256sum "${packages[0]}" | cut -d ' ' -f 1)"
[[ "$artifact_sha256" =~ ^[0-9a-f]{64}$ ]] || {
echo "bootstrap package digest is malformed" >&2
exit 1
}
echo "artifact_sha256=$artifact_sha256" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cmux-npm-bootstrap-package
path: ${{ runner.temp }}/cmux-npm-bootstrap/*.tgz
if-no-files-found: error
overwrite: true
preflight:
needs: build
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 10
permissions:
contents: read
outputs:
need_publish: ${{ steps.decision.outputs.need_publish }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: cmux-npm-bootstrap-package
path: bootstrap-package
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.14.0"
package-manager-cache: false
- name: Install pinned npm
run: npm install --global --ignore-scripts [email protected]
- name: Inspect the npm bootstrap state
id: project
run: |
set -euo pipefail
metadata="$RUNNER_TEMP/cmux-sdk-bootstrap-registry.json"
status="$(
curl \
--silent \
--show-error \
--location \
--retry 5 \
--retry-all-errors \
--output "$metadata" \
--write-out '%{http_code}' \
https://registry.npmjs.org/cmux-sdk
)"
case "$status" in
404)
echo "cmux-sdk is unclaimed; bootstrap may continue."
project_status=missing
;;
200)
echo "cmux-sdk exists; bootstrap bytes and provenance must match."
project_status=exists
;;
*)
echo "npm registry returned HTTP $status; refusing to infer availability." >&2
exit 1
;;
esac
echo "status=$project_status" >> "$GITHUB_OUTPUT"
- name: Reconcile an existing npm ownership bootstrap
if: steps.project.outputs.status == 'exists'
run: |
set -euo pipefail
shopt -s nullglob
packages=(bootstrap-package/*.tgz)
[[ "${#packages[@]}" == 1 ]] || {
echo "expected one tested npm artifact, found ${#packages[@]}" >&2
exit 1
}
python3 cmux-tui/bindings/verify_npm_provenance.py \
--package cmux-sdk \
--version "$BOOTSTRAP_VERSION" \
--repository-url git+https://github.com/manaflow-ai/cmux.git \
--repository-directory cmux-tui/bindings/typescript \
--owner lawrencechen \
--workflow .github/workflows/sdk-bootstrap-npm.yml \
--workflow-ref refs/heads/main \
--dist-tag bootstrap \
--require-dist-tag latest \
--publisher owner \
--artifact "${packages[0]}"
- name: Request publication for an unclaimed project
id: decision
if: steps.project.outputs.status == 'missing'
run: echo "need_publish=true" >> "$GITHUB_OUTPUT"
publish:
needs:
- build
- preflight
if: needs.preflight.outputs.need_publish == 'true'
runs-on: ubuntu-latest # github-hosted-required: npm provenance publishing
timeout-minutes: 10
permissions:
id-token: write
environment:
name: npm-bootstrap
url: https://www.npmjs.com/package/cmux-sdk
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: cmux-npm-bootstrap-package
path: bootstrap-package
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.14.0"
registry-url: https://registry.npmjs.org
package-manager-cache: false
- name: Install pinned npm
run: npm install --global --ignore-scripts [email protected]
- name: Verify protected source and the exact tested package
env:
EXPECTED_SHA256: ${{ needs.build.outputs.artifact_sha256 }}
run: |
set -euo pipefail
[[ "$GITHUB_REPOSITORY" == "manaflow-ai/cmux" ]] || {
echo "bootstrap repository must be manaflow-ai/cmux" >&2
exit 1
}
[[ "$GITHUB_REF" == "refs/heads/main" ]] || {
echo "bootstrap credential job must run from main" >&2
exit 1
}
main_sha="$(
git ls-remote \
https://github.com/manaflow-ai/cmux.git \
refs/heads/main |
awk 'NR == 1 { print $1 }'
)"
[[ "$main_sha" == "$GITHUB_SHA" ]] || {
echo "workflow commit $GITHUB_SHA is not current main $main_sha" >&2
exit 1
}
[[ "$EXPECTED_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo "validated npm artifact digest is malformed" >&2
exit 1
}
shopt -s nullglob
packages=(bootstrap-package/*.tgz)
[[ "${#packages[@]}" == 1 ]] || {
echo "expected one tested npm artifact, found ${#packages[@]}" >&2
exit 1
}
actual_sha256="$(sha256sum "${packages[0]}" | cut -d ' ' -f 1)"
[[ "$actual_sha256" == "$EXPECTED_SHA256" ]] || {
echo "downloaded npm bootstrap artifact digest mismatch" >&2
exit 1
}
- name: Publish the exact tested prerelease artifact
continue-on-error: true
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_BOOTSTRAP_TOKEN }}
run: |
set -euo pipefail
[[ -n "$NODE_AUTH_TOKEN" ]] || {
echo "npm-bootstrap environment secret NPM_BOOTSTRAP_TOKEN is required." >&2
exit 1
}
shopt -s nullglob
packages=(bootstrap-package/*.tgz)
[[ "${#packages[@]}" == 1 ]] || {
echo "expected one tested npm artifact, found ${#packages[@]}" >&2
exit 1
}
echo "npm lifecycle scripts are disabled in the credentialed publisher"
npm publish "$(realpath "${packages[0]}")" \
--ignore-scripts \
--tag bootstrap \
--provenance \
--access public
verify:
needs:
- build
- preflight
- publish
if: >-
always() &&
needs.build.result == 'success' &&
needs.preflight.result == 'success' &&
(needs.publish.result == 'success' || needs.publish.result == 'skipped')
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: cmux-npm-bootstrap-package
path: bootstrap-package
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.14.0"
package-manager-cache: false
- name: Install pinned npm
run: npm install --global --ignore-scripts [email protected]
- name: Verify npm-required bootstrap tags
run: |
set -euo pipefail
tags="$RUNNER_TEMP/cmux-sdk-bootstrap-tags.json"
deadline=$((SECONDS + 300))
until npm view cmux-sdk dist-tags --json > "$tags"; do
(( SECONDS < deadline )) || {
echo "cmux-sdk bootstrap tags did not become visible within 300 seconds." >&2
exit 1
}
sleep 15
done
node - "$tags" "$BOOTSTRAP_VERSION" <<'NODE'
const fs = require("node:fs");
const [path, expected] = process.argv.slice(2);
const tags = JSON.parse(fs.readFileSync(path, "utf8"));
if (
tags.bootstrap !== expected ||
typeof tags.latest !== "string" ||
tags.latest.length === 0
) {
console.error("cmux-sdk dist-tag validation failed.");
process.exit(1);
}
NODE
- name: Verify the npm ownership bootstrap
run: |
set -euo pipefail
shopt -s nullglob
packages=(bootstrap-package/*.tgz)
[[ "${#packages[@]}" == 1 ]] || {
echo "expected one tested npm artifact, found ${#packages[@]}" >&2
exit 1
}
python3 cmux-tui/bindings/verify_npm_provenance.py \
--package cmux-sdk \
--version "$BOOTSTRAP_VERSION" \
--repository-url git+https://github.com/manaflow-ai/cmux.git \
--repository-directory cmux-tui/bindings/typescript \
--owner lawrencechen \
--workflow .github/workflows/sdk-bootstrap-npm.yml \
--workflow-ref refs/heads/main \
--dist-tag bootstrap \
--require-dist-tag latest \
--publisher owner \
--artifact "${packages[0]}"
+413
View File
@@ -0,0 +1,413 @@
name: sdk bootstrap pypi
on:
repository_dispatch:
types: [sdk-bootstrap-pypi]
permissions: {}
env:
BOOTSTRAP_VERSION: "0.0.0a0"
concurrency:
group: sdk-bootstrap-pypi
cancel-in-progress: false
jobs:
build:
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
permissions:
contents: read
outputs:
artifact_id: ${{ steps.upload.outputs.artifact-id }}
artifact_sha256: ${{ steps.package.outputs.artifact_sha256 }}
steps:
- name: Require explicit bootstrap confirmation
if: github.event.client_payload.confirm_bootstrap != true
run: |
echo "Refusing to reserve cmux-sdk without confirm_bootstrap=true." >&2
exit 1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 0
- name: Require current protected main
run: |
set -euo pipefail
[[ "$GITHUB_REF" == "refs/heads/main" ]] || {
echo "Dispatch sdk-bootstrap-pypi.yml from main." >&2
exit 1
}
git fetch --force origin main
main_sha="$(git rev-parse origin/main)"
[[ "$GITHUB_SHA" == "$main_sha" ]] || {
echo "workflow commit is not current main" >&2
exit 1
}
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12.8"
- name: Install pinned packaging tools
run: |
python3 -m pip install \
--disable-pip-version-check \
"build==1.3.0" \
"setuptools==80.9.0" \
"wheel==0.45.1"
- name: Prepare the prerelease source tree
env:
CMUX_BOOTSTRAP_VERSION: ${{ env.BOOTSTRAP_VERSION }}
run: |
python3 - <<'PY'
import os
from pathlib import Path
import re
import shutil
source = Path("cmux-tui/bindings/python")
target = Path(os.environ["RUNNER_TEMP"]) / "cmux-python-bootstrap"
shutil.copytree(source, target)
manifest = target / "pyproject.toml"
contents = manifest.read_text(encoding="utf-8")
contents, count = re.subn(
r'(?m)^version = "[^"]+"$',
f'version = "{os.environ["CMUX_BOOTSTRAP_VERSION"]}"',
contents,
)
if count != 1:
raise SystemExit("expected one static project version")
manifest.write_text(contents, encoding="utf-8")
PY
- name: Test the prerelease source tree
run: |
cd "$RUNNER_TEMP/cmux-python-bootstrap"
PYTHONPATH=. python3 -m unittest discover -s tests -v
- name: Build deterministic bootstrap distributions
run: |
export SOURCE_DATE_EPOCH
SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$GITHUB_SHA")"
python3 -m build --no-isolation --sdist --wheel \
--outdir "$GITHUB_WORKSPACE/bootstrap-dist" \
"$RUNNER_TEMP/cmux-python-bootstrap"
python3 cmux-tui/bindings/normalize_python_sdist.py \
--archive bootstrap-dist/*.tar.gz \
--epoch "$SOURCE_DATE_EPOCH"
- name: Test the exact bootstrap distributions
env:
CMUX_PYTHON_DIST_DIR: ${{ github.workspace }}/bootstrap-dist
run: |
cd "$RUNNER_TEMP/cmux-python-bootstrap"
PYTHONPATH=. python3 -m unittest tests.test_package_consumer -v
- name: Fingerprint the bootstrap distributions
id: package
run: |
set -euo pipefail
shopt -s nullglob
wheels=(bootstrap-dist/*.whl)
sdists=(bootstrap-dist/*.tar.gz)
[[ "${#wheels[@]}" == 1 && "${#sdists[@]}" == 1 ]] || {
echo "expected one bootstrap wheel and one source distribution" >&2
exit 1
}
artifact_sha256="$(
cd bootstrap-dist
sha256sum *.whl *.tar.gz | sort -k2 | sha256sum | cut -d ' ' -f 1
)"
[[ "$artifact_sha256" =~ ^[0-9a-f]{64}$ ]] || exit 1
echo "artifact_sha256=$artifact_sha256" >> "$GITHUB_OUTPUT"
- id: upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cmux-python-bootstrap-dist-${{ github.run_attempt }}
path: bootstrap-dist/*
if-no-files-found: error
preflight:
needs: build
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
permissions:
contents: read
outputs:
need_publish: ${{ steps.decision.outputs.need_publish }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
artifact-ids: ${{ needs.build.outputs.artifact_id }}
path: bootstrap-dist
- name: Check whether the PyPI project exists
id: project
run: |
python3 - <<'PY'
import json
import os
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
request = Request(
"https://pypi.org/pypi/cmux-sdk/json",
headers={"Accept": "application/json"},
)
try:
with urlopen(request, timeout=20) as response:
metadata = json.loads(response.read())
except HTTPError as error:
if error.code != 404:
raise SystemExit("PyPI project lookup failed") from error
status = "missing"
except (OSError, URLError, json.JSONDecodeError) as error:
raise SystemExit("PyPI project lookup failed") from error
else:
if not isinstance(metadata, dict) or not isinstance(
metadata.get("info"), dict
):
raise SystemExit("PyPI project metadata is malformed")
status = "exists"
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"status={status}\n")
PY
- name: Check the existing bootstrap wheel
if: steps.project.outputs.status == 'exists'
id: wheel_state
run: |
python3 cmux-tui/bindings/reconcile_registry_artifact.py check \
--registry pypi \
--package cmux-sdk \
--version "$BOOTSTRAP_VERSION" \
--artifact bootstrap-dist/*.whl \
--allowed-artifact bootstrap-dist/*.whl \
--allowed-artifact bootstrap-dist/*.tar.gz \
--write-github-output
- name: Check the existing bootstrap source distribution
if: steps.project.outputs.status == 'exists'
id: sdist_state
run: |
python3 cmux-tui/bindings/reconcile_registry_artifact.py check \
--registry pypi \
--package cmux-sdk \
--version "$BOOTSTRAP_VERSION" \
--artifact bootstrap-dist/*.tar.gz \
--allowed-artifact bootstrap-dist/*.whl \
--allowed-artifact bootstrap-dist/*.tar.gz \
--write-github-output
- name: Decide whether publishing is required
id: decision
env:
PROJECT_STATUS: ${{ steps.project.outputs.status }}
WHEEL_STATUS: ${{ steps.wheel_state.outputs.status }}
SDIST_STATUS: ${{ steps.sdist_state.outputs.status }}
run: |
set -euo pipefail
if [[ "$PROJECT_STATUS" == "missing" ]]; then
need_publish=true
elif [[ "$WHEEL_STATUS" == "match" && "$SDIST_STATUS" == "match" ]]; then
need_publish=false
elif [[ "$WHEEL_STATUS" == "missing" && "$SDIST_STATUS" == "missing" ]]; then
echo "cmux-sdk exists without the expected bootstrap release" >&2
exit 1
elif { [[ "$WHEEL_STATUS" == "match" && "$SDIST_STATUS" == "missing" ]] ||
[[ "$WHEEL_STATUS" == "missing" && "$SDIST_STATUS" == "match" ]]; }; then
need_publish=true
else
echo "unexpected bootstrap registry state" >&2
exit 1
fi
echo "need_publish=$need_publish" >> "$GITHUB_OUTPUT"
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
if: steps.project.outputs.status == 'exists'
with:
python-version: "3.12.8"
- name: Install the pinned provenance verifier
if: steps.project.outputs.status == 'exists'
run: |
python3 -m pip install \
--disable-pip-version-check \
"pypi-attestations==0.0.29"
- name: Verify existing bootstrap with pypi-attestations verify pypi
if: steps.project.outputs.status == 'exists'
env:
WHEEL_STATUS: ${{ steps.wheel_state.outputs.status }}
SDIST_STATUS: ${{ steps.sdist_state.outputs.status }}
run: |
set -euo pipefail
shopt -s nullglob
filenames=()
if [[ "$WHEEL_STATUS" == "match" ]]; then
wheels=(bootstrap-dist/*.whl)
filenames+=(--filename "$(basename "${wheels[0]}")")
fi
if [[ "$SDIST_STATUS" == "match" ]]; then
sdists=(bootstrap-dist/*.tar.gz)
filenames+=(--filename "$(basename "${sdists[0]}")")
fi
python3 cmux-tui/bindings/verify_pypi_provenance.py \
--package cmux-sdk \
--version "$BOOTSTRAP_VERSION" \
--repository https://github.com/manaflow-ai/cmux \
--owner lawrencecchen \
--workflow sdk-bootstrap-pypi.yml \
--environment pypi-bootstrap \
"${filenames[@]}"
publish:
needs:
- build
- preflight
if: needs.preflight.outputs.need_publish == 'true'
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
permissions:
id-token: write
environment:
name: pypi-bootstrap
url: https://pypi.org/p/cmux-sdk
outputs:
outcome: ${{ steps.publish.outcome }}
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
artifact-ids: ${{ needs.build.outputs.artifact_id }}
path: bootstrap-dist
- name: Verify the immutable bootstrap distributions
env:
EXPECTED_ARTIFACT_SHA256: ${{ needs.build.outputs.artifact_sha256 }}
run: |
set -euo pipefail
[[ "$EXPECTED_ARTIFACT_SHA256" =~ ^[0-9a-f]{64}$ ]] || exit 1
actual_sha256="$(
cd bootstrap-dist
sha256sum *.whl *.tar.gz | sort -k2 | sha256sum | cut -d ' ' -f 1
)"
[[ "$actual_sha256" == "$EXPECTED_ARTIFACT_SHA256" ]] || {
echo "downloaded Python bootstrap artifact digest mismatch" >&2
exit 1
}
- name: Revalidate protected source before bootstrap publication
run: |
set -euo pipefail
[[ "$GITHUB_REPOSITORY" == "manaflow-ai/cmux" ]] || {
echo "bootstrap repository must be manaflow-ai/cmux" >&2
exit 1
}
[[ "$GITHUB_REF" == "refs/heads/main" ]] || {
echo "bootstrap credential job must run from main" >&2
exit 1
}
[[ "$GITHUB_SHA" =~ ^[0-9a-f]{40}$ ]] || {
echo "bootstrap commit is malformed" >&2
exit 1
}
main_sha="$(
git ls-remote \
https://github.com/manaflow-ai/cmux.git \
refs/heads/main |
awk 'NR == 1 { print $1 }'
)"
[[ "$main_sha" == "$GITHUB_SHA" ]] || {
echo "workflow commit $GITHUB_SHA is not current main $main_sha" >&2
exit 1
}
- name: Publish the attested bootstrap distributions
id: publish
continue-on-error: true
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
packages-dir: bootstrap-dist
attestations: true
skip-existing: true
verify:
needs:
- build
- preflight
- publish
if: >-
always() &&
needs.build.result == 'success' &&
needs.preflight.result == 'success' &&
(needs.publish.result == 'success' || needs.publish.result == 'skipped')
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12.8"
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
artifact-ids: ${{ needs.build.outputs.artifact_id }}
path: bootstrap-dist
- name: Install the pinned provenance verifier
run: |
python3 -m pip install \
--disable-pip-version-check \
"pypi-attestations==0.0.29"
- name: Reconcile exact bootstrap distributions
run: |
set -euo pipefail
python3 cmux-tui/bindings/reconcile_registry_artifact.py check \
--registry pypi \
--package cmux-sdk \
--version "$BOOTSTRAP_VERSION" \
--artifact bootstrap-dist/*.whl \
--allowed-artifact bootstrap-dist/*.whl \
--allowed-artifact bootstrap-dist/*.tar.gz \
--wait-seconds 300 \
--require-match
python3 cmux-tui/bindings/reconcile_registry_artifact.py check \
--registry pypi \
--package cmux-sdk \
--version "$BOOTSTRAP_VERSION" \
--artifact bootstrap-dist/*.tar.gz \
--allowed-artifact bootstrap-dist/*.whl \
--allowed-artifact bootstrap-dist/*.tar.gz \
--wait-seconds 300 \
--require-match
- name: Verify trusted-publisher provenance with pypi-attestations verify pypi
run: |
set -euo pipefail
shopt -s nullglob
wheels=(bootstrap-dist/*.whl)
sdists=(bootstrap-dist/*.tar.gz)
python3 cmux-tui/bindings/verify_pypi_provenance.py \
--package cmux-sdk \
--version "$BOOTSTRAP_VERSION" \
--filename "$(basename "${wheels[0]}")" \
--filename "$(basename "${sdists[0]}")" \
--repository https://github.com/manaflow-ai/cmux \
--owner lawrencecchen \
--workflow sdk-bootstrap-pypi.yml \
--environment pypi-bootstrap
+75 -68
View File
@@ -1,19 +1,24 @@
name: sdk publish crates
name: sdk preflight crates
on:
push:
tags:
- "mux-sdk-v*"
- "cmux-sdk-v*"
workflow_call:
inputs:
version:
description: "SDK version to validate"
required: true
type: string
workflow_dispatch:
inputs:
version:
description: "SDK version to validate/publish, for example 0.1.0"
description: "SDK version to validate, for example 0.1.0"
required: true
type: string
permissions: {}
env:
RUST_TOOLCHAIN: "1.95.0"
concurrency:
group: sdk-publish-crates-${{ github.ref }}
cancel-in-progress: false
@@ -29,6 +34,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Validate tag and package versions
id: version
@@ -37,13 +43,15 @@ jobs:
run: |
set -euo pipefail
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
[[ "$GITHUB_REF_NAME" =~ ^(mux|cmux)-sdk-v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "tag must match mux-sdk-vX.Y.Z or cmux-sdk-vX.Y.Z" >&2
[[ "$GITHUB_REF_NAME" =~ ^cmux-sdk-v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "tag must match cmux-sdk-vX.Y.Z" >&2
exit 1
}
version="${GITHUB_REF_NAME#cmux-sdk-v}"
[[ "$DISPATCH_VERSION" == "$version" ]] || {
echo "workflow_dispatch version $DISPATCH_VERSION does not match tag version $version" >&2
exit 1
}
version="$GITHUB_REF_NAME"
version="${version#mux-sdk-v}"
version="${version#cmux-sdk-v}"
else
version="$DISPATCH_VERSION"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
@@ -51,6 +59,8 @@ jobs:
exit 1
}
fi
python3 cmux-tui/bindings/validate_release_version.py \
--version "$version"
python3 - "$version" <<'PY'
import json
import pathlib
@@ -72,7 +82,9 @@ jobs:
raise SystemExit(1)
print(f"All package versions match {expected}")
PY
python3 cmux-tui/bindings/check-versions.py --expected "$version"
python3 cmux-tui/bindings/check-versions.py \
--published-only \
--expected "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
bindings-e2e-rust:
@@ -97,18 +109,63 @@ jobs:
- name: Install zig
run: ./scripts/install-zig-ci.sh
- name: Rust version
- name: Install pinned Rust toolchain
run: |
rustc --version || true
if ! command -v cargo >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal
rustup default "$RUST_TOOLCHAIN"
cargo --version
rustc --version
- name: Build cmux-tui server
working-directory: cmux-tui
run: cargo build -p cmux-tui --bin cmux-tui --locked
- name: Test Rust SDK packages
working-directory: cmux-tui
env:
CMUX_SDK_VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
cargo test -p cmux-sdk -p cmux-sidebar --locked
cargo package -p cmux-sdk --locked
cargo package \
-p cmux-sidebar \
--locked \
--no-verify \
--config \
"patch.crates-io.cmux-sdk.path='$GITHUB_WORKSPACE/cmux-tui/bindings/rust'"
verify_root="$RUNNER_TEMP/cmux-rust-package-verify"
mkdir -p "$verify_root"
tar -xzf \
"target/package/cmux-sdk-$CMUX_SDK_VERSION.crate" \
-C "$verify_root"
tar -xzf \
"target/package/cmux-sidebar-$CMUX_SDK_VERSION.crate" \
-C "$verify_root"
cargo test \
--manifest-path \
"$verify_root/cmux-sidebar-$CMUX_SDK_VERSION/Cargo.toml" \
--config \
"patch.crates-io.cmux-sdk.path='$verify_root/cmux-sdk-$CMUX_SDK_VERSION'" \
--all-targets
- name: Upload validated cmux-sdk crate
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cmux-rust-sdk-crate
path: cmux-tui/target/package/cmux-sdk-${{ needs.version.outputs.version }}.crate
if-no-files-found: error
overwrite: true
- name: Upload validated cmux-sidebar crate
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cmux-rust-sidebar-crate
path: cmux-tui/target/package/cmux-sidebar-${{ needs.version.outputs.version }}.crate
if-no-files-found: error
overwrite: true
- name: Rust SDK conformance
run: |
report="$RUNNER_TEMP/cmux-sdk-conformance-rust.txt"
@@ -118,53 +175,3 @@ jobs:
--cmux-tui-bin "$GITHUB_WORKSPACE/cmux-tui/target/debug/cmux-tui" |
tee "$report"
grep -Eq '^PASS +rust +live-creation-exit-restart-unix$' "$report"
publish:
needs:
- version
- bindings-e2e-rust
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
permissions:
contents: read
id-token: write
environment:
name: crates-io
url: https://crates.io/crates/cmux-client
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Authenticate cmux-client with crates.io trusted publishing
id: auth_client
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
- name: Publish cmux-client
working-directory: cmux-tui
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth_client.outputs.token }}
run: cargo publish -p cmux-client --locked
- name: Wait for cmux-client to reach the crates.io index
env:
CMUX_SDK_VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
for _ in $(seq 1 30); do
if cargo info "cmux-client@$CMUX_SDK_VERSION" >/dev/null 2>&1; then
exit 0
fi
sleep 10
done
echo "cmux-client@$CMUX_SDK_VERSION did not reach the crates.io index" >&2
exit 1
- name: Authenticate cmux-sidebar with crates.io trusted publishing
id: auth_sidebar
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
- name: Publish cmux-sidebar
working-directory: cmux-tui
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth_sidebar.outputs.token }}
run: cargo publish -p cmux-sidebar --locked
+146 -12
View File
@@ -1,10 +1,22 @@
name: sdk publish go
name: sdk validate go
on:
push:
tags:
- "mux-sdk-v*"
- "cmux-sdk-v*"
workflow_call:
inputs:
version:
description: "SDK version to validate or verify"
required: true
type: string
verify_tag:
description: "Resolve the coordinated public Go module tag"
required: false
default: false
type: boolean
release_ref:
description: "Exact coordinated Go module tag ref"
required: false
default: ""
type: string
workflow_dispatch:
inputs:
version:
@@ -29,28 +41,66 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Validate tag and package versions
id: version
env:
CALLER_WORKFLOW_REF: ${{ github.workflow_ref }}
DISPATCH_VERSION: ${{ inputs.version }}
RELEASE_REF: ${{ inputs.release_ref }}
VERIFY_TAG: ${{ inputs.verify_tag }}
run: |
set -euo pipefail
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
[[ "$GITHUB_REF_NAME" =~ ^(mux|cmux)-sdk-v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "tag must match mux-sdk-vX.Y.Z or cmux-sdk-vX.Y.Z" >&2
[[ "$GITHUB_REF_NAME" =~ ^cmux-tui/bindings/go/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || {
echo "tag must match cmux-tui/bindings/go/vX.Y.Z" >&2
exit 1
}
version="${GITHUB_REF_NAME#cmux-tui/bindings/go/v}"
[[ "$DISPATCH_VERSION" == "$version" ]] || {
echo "requested version $DISPATCH_VERSION does not match tag version $version" >&2
exit 1
}
version="$GITHUB_REF_NAME"
version="${version#mux-sdk-v}"
version="${version#cmux-sdk-v}"
else
version="$DISPATCH_VERSION"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
[[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || {
echo "workflow_dispatch version must match X.Y.Z" >&2
exit 1
}
fi
python3 cmux-tui/bindings/validate_release_version.py \
--version "$version"
if [[ "$VERIFY_TAG" == "true" ]]; then
expected_caller="$GITHUB_REPOSITORY/.github/workflows/sdk-release-cut.yml@$GITHUB_REF"
[[ "$CALLER_WORKFLOW_REF" == "$expected_caller" ]] || {
echo "Public Go tag verification is only available through sdk-release-cut.yml." >&2
exit 1
}
tag="cmux-tui/bindings/go/v$version"
expected_ref="refs/tags/$tag"
[[ "$RELEASE_REF" == "$expected_ref" ]] || {
echo "Refusing to verify Go ref $RELEASE_REF; expected $expected_ref." >&2
exit 1
}
git fetch --force origin main --tags
git tag --list 'cmux-sdk-v*' | \
python3 cmux-tui/bindings/validate_release_version.py \
--version "$version" \
--require-latest-tag
release_sha="$(git rev-parse "refs/tags/$tag^{commit}")" || {
echo "release tag does not exist: $tag" >&2
exit 1
}
git merge-base --is-ancestor "$release_sha" origin/main || {
echo "release tag $tag is not an ancestor of protected main" >&2
exit 1
}
[[ "$release_sha" == "$GITHUB_SHA" ]] || {
echo "release tag $tag resolves to $release_sha, expected workflow commit $GITHUB_SHA" >&2
exit 1
}
fi
python3 - "$version" <<'PY'
import json
import pathlib
@@ -71,10 +121,13 @@ jobs:
raise SystemExit(1)
print(f"All package versions match {expected}")
PY
python3 cmux-tui/bindings/check-versions.py --expected "$version"
python3 cmux-tui/bindings/check-versions.py \
--published-only \
--expected "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
bindings-e2e-go:
if: inputs.verify_tag != true
needs: version
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 40
@@ -124,6 +177,7 @@ jobs:
grep -Eq '^PASS +go +live-creation-exit-restart-unix$' "$report"
validate-go-module:
if: inputs.verify_tag != true
needs: bindings-e2e-go
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
permissions:
@@ -141,5 +195,85 @@ jobs:
- name: Validate Go module
working-directory: cmux-tui/bindings/go
run: |
go test ./...
go build ./...
go vet ./...
verify-versioned-go-module:
if: inputs.verify_tag == true
needs: version
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 35
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: "1.22.x"
cache: false
- name: Resolve the public module tag from a clean consumer
env:
CMUX_SDK_VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
module="github.com/manaflow-ai/cmux/cmux-tui/bindings/go"
expected="v$CMUX_SDK_VERSION"
scratch="$(mktemp -d)"
trap 'rm -rf "$scratch"' EXIT
export GOENV=off
export GOFLAGS=""
export GOINSECURE=""
export GOPROXY=https://proxy.golang.org
export GOSUMDB=sum.golang.org
export GOPRIVATE=""
export GONOPROXY=none
export GONOSUMDB=none
export GOMODCACHE="$scratch/modcache"
export GOCACHE="$scratch/buildcache"
export GOWORK=off
mkdir "$scratch/consumer"
cd "$scratch/consumer"
go mod init cmux-release-consumer
python3 "$GITHUB_WORKSPACE/cmux-tui/bindings/wait_for_go_module.py" \
--module "$module" \
--version "$expected" \
--wait-seconds 1800 \
--retry-seconds 30
go get "$module@$expected"
go mod download "$module@$expected"
go mod verify
resolved="$(go list -m -f '{{.Version}}' "$module")"
[[ "$resolved" == "$expected" ]] || {
echo "resolved $module@$resolved, expected $expected" >&2
exit 1
}
module_dir="$(go list -m -f '{{.Dir}}' "$module")"
python3 "$GITHUB_WORKSPACE/cmux-tui/bindings/verify_go_module_source.py" \
--repository "$GITHUB_WORKSPACE" \
--commit "$GITHUB_SHA" \
--module-subdir cmux-tui/bindings/go \
--downloaded-root "$module_dir"
cat > release_test.go <<EOF
package consumer
import (
"testing"
cmux "$module"
raw "$module/raw"
)
func TestReleasedPackagesCompile(t *testing.T) {
_ = cmux.ClientOptions{}
_ = raw.Options{}
}
EOF
gofmt -w release_test.go
go test -mod=readonly ./...
+5 -19
View File
@@ -1,10 +1,6 @@
name: sdk publish java
on:
push:
tags:
- "mux-sdk-v*"
- "cmux-sdk-v*"
workflow_dispatch:
inputs:
version:
@@ -36,21 +32,11 @@ jobs:
DISPATCH_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
[[ "$GITHUB_REF_NAME" =~ ^(mux|cmux)-sdk-v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "tag must match mux-sdk-vX.Y.Z or cmux-sdk-vX.Y.Z" >&2
exit 1
}
version="$GITHUB_REF_NAME"
version="${version#mux-sdk-v}"
version="${version#cmux-sdk-v}"
else
version="$DISPATCH_VERSION"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "workflow_dispatch version must match X.Y.Z" >&2
exit 1
}
fi
version="$DISPATCH_VERSION"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "workflow_dispatch version must match X.Y.Z" >&2
exit 1
}
python3 - "$version" <<'PY'
import json
import pathlib
+55 -67
View File
@@ -1,21 +1,25 @@
name: sdk publish npm
name: sdk preflight npm
on:
push:
tags:
- "mux-sdk-v*"
- "cmux-sdk-v*"
workflow_call:
inputs:
version:
description: "SDK version to validate"
required: true
type: string
outputs:
artifact_id:
description: "Immutable ID of the validated npm artifact"
value: ${{ jobs.bindings-e2e-typescript.outputs.artifact_id }}
artifact_sha256:
description: "SHA-256 of the validated npm tarball"
value: ${{ jobs.bindings-e2e-typescript.outputs.artifact_sha256 }}
workflow_dispatch:
inputs:
version:
description: "SDK version to validate/publish, for example 0.1.0"
description: "SDK version to validate, for example 0.1.0"
required: true
type: string
confirm_npm_cmux:
description: "Set true only for the coordinated npm cmux SDK publish"
required: true
default: false
type: boolean
permissions: {}
@@ -37,6 +41,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Validate tag and package versions
id: version
@@ -45,13 +50,15 @@ jobs:
run: |
set -euo pipefail
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
[[ "$GITHUB_REF_NAME" =~ ^(mux|cmux)-sdk-v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "tag must match mux-sdk-vX.Y.Z or cmux-sdk-vX.Y.Z" >&2
[[ "$GITHUB_REF_NAME" =~ ^cmux-sdk-v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "tag must match cmux-sdk-vX.Y.Z" >&2
exit 1
}
version="${GITHUB_REF_NAME#cmux-sdk-v}"
[[ "$DISPATCH_VERSION" == "$version" ]] || {
echo "workflow_dispatch version $DISPATCH_VERSION does not match tag version $version" >&2
exit 1
}
version="$GITHUB_REF_NAME"
version="${version#mux-sdk-v}"
version="${version#cmux-sdk-v}"
else
version="$DISPATCH_VERSION"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
@@ -59,6 +66,8 @@ jobs:
exit 1
}
fi
python3 cmux-tui/bindings/validate_release_version.py \
--version "$version"
python3 - "$version" <<'PY'
import json
import pathlib
@@ -79,7 +88,9 @@ jobs:
raise SystemExit(1)
print(f"All package versions match {expected}")
PY
python3 cmux-tui/bindings/check-versions.py --expected "$version"
python3 cmux-tui/bindings/check-versions.py \
--published-only \
--expected "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
bindings-e2e-typescript:
@@ -88,6 +99,9 @@ jobs:
timeout-minutes: 40
permissions:
contents: read
outputs:
artifact_id: ${{ steps.upload.outputs.artifact-id }}
artifact_sha256: ${{ steps.package.outputs.artifact_sha256 }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -119,7 +133,9 @@ jobs:
- name: Install TypeScript adapter dependencies
working-directory: cmux-tui/bindings/typescript
run: npm ci --no-audit --no-fund
run: |
npm ci --no-audit --no-fund
npm test
- name: Build cmux-tui server
working-directory: cmux-tui
@@ -137,55 +153,27 @@ jobs:
grep -Eq '^PASS +typescript +live-creation-exit-restart-unix$' "$report"
grep -Eq '^PASS +typescript +live-creation-exit-restart-websocket$' "$report"
publish:
# The npm package name "cmux" is currently a different live package
# (the cloud-VM CLI). Publishing the SDK there is a coordinated breaking
# action, so tag pushes never publish to npm and manual runs must opt in.
if: github.event_name == 'workflow_dispatch'
needs: bindings-e2e-typescript
# npm --provenance rejects self-hosted runners; the attestation is only
# verifiable from a GitHub-hosted runner. This one publish job must stay on
# ubuntu-latest (github-hosted), unlike the routed self-hosted jobs above.
runs-on: ubuntu-latest # github-hosted-required: npm provenance needs a github-hosted runner
permissions:
contents: read
id-token: write
environment:
name: npm
url: https://www.npmjs.com/package/cmux
steps:
- name: Require npm cmux confirmation
if: inputs.confirm_npm_cmux != true
run: |
echo "Refusing to publish npm package cmux without confirm_npm_cmux=true." >&2
exit 1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.14.0"
registry-url: https://registry.npmjs.org
- name: Upgrade npm for OIDC trusted publishing
# Node 22 bundles npm 10, which signs provenance but cannot
# authenticate the publish via OIDC trusted publishing (the PUT is
# unauthenticated and 404s). npm >= 11.5.1 performs the OIDC token
# exchange for the publish itself.
run: npm install -g npm@^11.5.1
- name: Build package
- name: Pack the validated npm artifact
id: package
working-directory: cmux-tui/bindings/typescript
run: |
npm ci --no-audit --no-fund
npm run build
set -euo pipefail
mkdir -p "$RUNNER_TEMP/cmux-npm-dist"
npm pack --pack-destination "$RUNNER_TEMP/cmux-npm-dist"
shopt -s nullglob
packages=("$RUNNER_TEMP"/cmux-npm-dist/*.tgz)
[[ "${#packages[@]}" == 1 ]] || {
echo "expected one validated npm artifact" >&2
exit 1
}
artifact_sha256="$(sha256sum "${packages[0]}" | cut -d ' ' -f 1)"
[[ "$artifact_sha256" =~ ^[0-9a-f]{64}$ ]] || exit 1
echo "artifact_sha256=$artifact_sha256" >> "$GITHUB_OUTPUT"
- name: Publish package to npm
working-directory: cmux-tui/bindings/typescript
# The npm `cmux` name still serves the cloud-VM CLI on the `latest`
# dist-tag (0.8.3). The SDK ships on its own `sdk` tag so installing
# bare `cmux` keeps resolving the CLI; use `npm i cmux@sdk` for the SDK.
run: npm publish --provenance --tag sdk
- name: Upload the validated npm artifact
id: upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cmux-npm-dist-${{ github.run_attempt }}
path: ${{ runner.temp }}/cmux-npm-dist/*.tgz
if-no-files-found: error
+87 -37
View File
@@ -1,14 +1,23 @@
name: sdk publish python
name: sdk preflight python
on:
push:
tags:
- "mux-sdk-v*"
- "cmux-sdk-v*"
workflow_call:
inputs:
version:
description: "SDK version to validate"
required: true
type: string
outputs:
artifact_id:
description: "Immutable ID of the validated Python distributions"
value: ${{ jobs.build.outputs.artifact_id }}
artifact_sha256:
description: "SHA-256 of the validated distribution digest manifest"
value: ${{ jobs.build.outputs.artifact_sha256 }}
workflow_dispatch:
inputs:
version:
description: "SDK version to validate/publish, for example 0.1.0"
description: "SDK version to validate, for example 0.1.0"
required: true
type: string
@@ -32,6 +41,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Validate tag and package versions
id: version
@@ -40,13 +50,15 @@ jobs:
run: |
set -euo pipefail
if [[ "${GITHUB_REF_TYPE:-}" == "tag" ]]; then
[[ "$GITHUB_REF_NAME" =~ ^(mux|cmux)-sdk-v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "tag must match mux-sdk-vX.Y.Z or cmux-sdk-vX.Y.Z" >&2
[[ "$GITHUB_REF_NAME" =~ ^cmux-sdk-v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "tag must match cmux-sdk-vX.Y.Z" >&2
exit 1
}
version="${GITHUB_REF_NAME#cmux-sdk-v}"
[[ "$DISPATCH_VERSION" == "$version" ]] || {
echo "workflow_dispatch version $DISPATCH_VERSION does not match tag version $version" >&2
exit 1
}
version="$GITHUB_REF_NAME"
version="${version#mux-sdk-v}"
version="${version#cmux-sdk-v}"
else
version="$DISPATCH_VERSION"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
@@ -54,6 +66,8 @@ jobs:
exit 1
}
fi
python3 cmux-tui/bindings/validate_release_version.py \
--version "$version"
python3 - "$version" <<'PY'
import json
import pathlib
@@ -74,7 +88,9 @@ jobs:
raise SystemExit(1)
print(f"All package versions match {expected}")
PY
python3 cmux-tui/bindings/check-versions.py --expected "$version"
python3 cmux-tui/bindings/check-versions.py \
--published-only \
--expected "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
bindings-e2e-python:
@@ -88,6 +104,10 @@ jobs:
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12.8"
- name: Init ghostty submodule
run: git submodule update --init --depth 1 ghostty
@@ -109,6 +129,16 @@ jobs:
working-directory: cmux-tui
run: cargo build -p cmux-tui --bin cmux-tui --locked
- name: Install declared Python build backend
run: |
python3 -m pip install \
--disable-pip-version-check \
"setuptools==80.9.0"
- name: Test Python SDK package
working-directory: cmux-tui/bindings/python
run: PYTHONPATH=. python3 -m unittest discover -s tests -v
- name: Python SDK conformance
run: |
report="$RUNNER_TEMP/cmux-sdk-conformance-python.txt"
@@ -124,42 +154,62 @@ jobs:
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
permissions:
contents: read
outputs:
artifact_id: ${{ steps.upload.outputs.artifact-id }}
artifact_sha256: ${{ steps.package.outputs.artifact_sha256 }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12.8"
- name: Install pinned Python packaging tools
run: |
python3 -m pip install \
--disable-pip-version-check \
"build==1.3.0" \
"setuptools==80.9.0" \
"wheel==0.45.1"
- name: Build sdist and wheel
working-directory: cmux-tui/bindings/python
run: |
python3 -m pip install --upgrade build
python3 -m build --sdist --wheel
set -euo pipefail
SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$GITHUB_SHA")"
export SOURCE_DATE_EPOCH
python3 -m build --no-isolation --sdist --wheel
python3 ../normalize_python_sdist.py \
--archive dist/*.tar.gz \
--epoch "$SOURCE_DATE_EPOCH"
- name: Test the exact Python distributions
working-directory: cmux-tui/bindings/python
env:
CMUX_PYTHON_DIST_DIR: ${{ github.workspace }}/cmux-tui/bindings/python/dist
run: PYTHONPATH=. python3 -m unittest tests.test_package_consumer -v
- name: Fingerprint the validated Python distributions
id: package
run: |
set -euo pipefail
cd cmux-tui/bindings/python/dist
shopt -s nullglob
files=(*.whl *.tar.gz)
[[ "${#files[@]}" == 2 ]] || {
echo "expected one wheel and one source distribution" >&2
exit 1
}
artifact_sha256="$(sha256sum "${files[@]}" | sort -k2 | sha256sum | cut -d ' ' -f 1)"
[[ "$artifact_sha256" =~ ^[0-9a-f]{64}$ ]] || exit 1
echo "artifact_sha256=$artifact_sha256" >> "$GITHUB_OUTPUT"
- name: Upload distributions
id: upload
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cmux-python-dist
name: cmux-python-dist-${{ github.run_attempt }}
path: cmux-tui/bindings/python/dist/*
if-no-files-found: error
publish:
needs: build
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
permissions:
contents: read
id-token: write
environment:
name: pypi
url: https://pypi.org/p/cmux
steps:
- name: Download distributions
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: cmux-python-dist
path: dist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
packages-dir: dist
attestations: true
File diff suppressed because it is too large Load Diff
+1 -22
View File
@@ -59,28 +59,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD)
BUILD_FLAVOR="crashsubdir-cmux-crash-v1"
TAG="xcframework-$GHOSTTY_SHA-$BUILD_FLAVOR"
URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz"
echo "Downloading xcframework for ghostty $GHOSTTY_SHA"
MAX_RETRIES=30
RETRY_DELAY=20
for i in $(seq 1 $MAX_RETRIES); do
if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then
echo "Download succeeded on attempt $i"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2
exit 1
fi
echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
done
tar xzf GhosttyKit.xcframework.tar.gz
rm GhosttyKit.xcframework.tar.gz
test -d GhosttyKit.xcframework
./scripts/download-prebuilt-ghosttykit.sh
- name: Install zig
run: |
+41 -68
View File
@@ -180,28 +180,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
GHOSTTY_SHA=$(git -C ghostty rev-parse HEAD)
BUILD_FLAVOR="crashsubdir-cmux-crash-v1"
TAG="xcframework-$GHOSTTY_SHA-$BUILD_FLAVOR"
URL="https://github.com/manaflow-ai/ghostty/releases/download/$TAG/GhosttyKit.xcframework.tar.gz"
echo "Downloading xcframework for ghostty $GHOSTTY_SHA"
MAX_RETRIES=30
RETRY_DELAY=20
for i in $(seq 1 $MAX_RETRIES); do
if curl -fSL -o GhosttyKit.xcframework.tar.gz "$URL"; then
echo "Download succeeded on attempt $i"
break
fi
if [ "$i" -eq "$MAX_RETRIES" ]; then
echo "Failed to download xcframework after $MAX_RETRIES attempts" >&2
exit 1
fi
echo "Attempt $i/$MAX_RETRIES failed, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
done
tar xzf GhosttyKit.xcframework.tar.gz
rm GhosttyKit.xcframework.tar.gz
test -d GhosttyKit.xcframework
./scripts/download-prebuilt-ghosttykit.sh
- name: Install zig
run: |
@@ -523,11 +502,10 @@ jobs:
# one xcodebuild invocation; a filter can select a suite living in either.
xctest = [int(value) for value in re.findall(r"Executed\s+(\d+)\s+tests?\b", text)]
swift_testing = [int(value) for value in re.findall(r"Test run with\s+(\d+)\s+tests?\b", text)]
counts = []
if xctest:
counts.append(xctest[-1])
if swift_testing:
counts.append(swift_testing[-1])
# App-hosted Swift Testing can emit a selected run followed by an
# auxiliary zero-test run. Count every summary so the later host
# teardown cannot erase a test that actually executed.
counts = xctest + swift_testing
if counts:
print(max(counts))
PY
@@ -625,17 +603,18 @@ jobs:
| xargs -I{} echo "Duration: {}s"
- name: Upload recording artifact
id: recording
if: ${{ always() && steps.filter.outputs.record_video == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-recording
path: /tmp/test-recording.mp4
if-no-files-found: warn
retention-days: 30
- name: Post results to cmux-dev-artifacts
- name: Publish test summary
if: always()
env:
GH_TOKEN: ${{ secrets.DEV_ARTIFACTS_TOKEN }}
TEST_RESULT: ${{ steps.tests.outputs.test_result || 'failed' }}
TEST_SUMMARY: ${{ steps.tests.outputs.test_summary }}
TEST_OUTPUT: ${{ steps.tests.outputs.test_output }}
@@ -643,10 +622,10 @@ jobs:
COMMIT_SHA: ${{ steps.sha.outputs.sha }}
RUN_ID: ${{ github.run_id }}
RECORD_VIDEO: ${{ steps.filter.outputs.record_video }}
RECORDING_URL: ${{ steps.recording.outputs.artifact-url }}
run: |
set -euo pipefail
LABEL="$TEST_RESULT"
if [ "$TEST_RESULT" = "passed" ]; then
STATUS_EMOJI="PASSED"
else
@@ -655,47 +634,41 @@ jobs:
REF_DISPLAY="${{ inputs.ref || github.ref_name }}"
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/$RUN_ID"
ARTIFACT_URL="$RUN_URL#artifacts"
{
echo "## E2E test result"
echo
echo "**Status:** $STATUS_EMOJI"
echo "**Ref:** \`$REF_DISPLAY\`"
echo "**SHA:** [\`${COMMIT_SHA:0:12}\`](https://github.com/${{ github.repository }}/commit/$COMMIT_SHA)"
echo "**Test:** \`$TEST_FILTER\`"
echo "**Workflow run:** $RUN_URL"
BODY="**Status:** $STATUS_EMOJI
**Ref:** \`$REF_DISPLAY\`
**SHA:** [\`${COMMIT_SHA:0:12}\`](https://github.com/${{ github.repository }}/commit/$COMMIT_SHA)
**Test:** \`$TEST_FILTER\`
**Workflow run:** $RUN_URL"
if [ "$RECORD_VIDEO" = "true" ]; then
if [ -n "$RECORDING_URL" ]; then
echo "**Recording:** [Download artifact]($RECORDING_URL) (retained for 30 days)"
else
echo "**Recording:** unavailable"
fi
fi
if [ "$RECORD_VIDEO" = "true" ]; then
BODY="$BODY
**Recording:** [Download from artifacts]($ARTIFACT_URL)"
fi
if [ -n "$TEST_OUTPUT" ]; then
echo
echo "<details><summary>Test output (last 200 lines)</summary>"
echo
echo '```'
printf '%s\n' "$TEST_OUTPUT"
echo '```'
echo
echo "</details>"
fi
if [ -n "$TEST_OUTPUT" ]; then
BODY="$BODY
<details><summary>Test output (last 200 lines)</summary>
\`\`\`
$TEST_OUTPUT
\`\`\`
</details>"
fi
if [ -n "$TEST_SUMMARY" ]; then
BODY="$BODY
\`\`\`
$TEST_SUMMARY
\`\`\`"
fi
ISSUE_URL=$(gh issue create \
--repo manaflow-ai/cmux-dev-artifacts \
--title "[$STATUS_EMOJI] $TEST_FILTER @ ${COMMIT_SHA:0:7} ($REF_DISPLAY)" \
--body "$BODY" \
--label "$LABEL")
echo "Issue posted: $ISSUE_URL"
echo "::notice title=Test Result Issue::$ISSUE_URL"
if [ -n "$TEST_SUMMARY" ]; then
echo
echo '```'
printf '%s\n' "$TEST_SUMMARY"
echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Clean owned DerivedData
if: always()
+22 -8
View File
@@ -72,7 +72,7 @@ jobs:
fi
# The conventions lint (free-function ban, namespace-type rule, ...)
# covers every package, so it runs for any Packages/ change too.
if grep -Eq '^(ios/|Packages/|Sources/Mobile/|vendor/stack-auth-swift-sdk-prerelease/|scripts/lint-ios-package-conventions\.sh$|scripts/lint-namespace-types-baseline\.txt$)' /tmp/changed-files.txt; then
if grep -Eq '^(ios/|Packages/|Sources/Mobile/|vendor/stack-auth-swift-sdk-prerelease/|scripts/lint-ios-package-conventions\.sh$|scripts/lint-ios-package-conventions-baseline\.txt$|scripts/lint-namespace-types-baseline\.txt$)' /tmp/changed-files.txt; then
echo "should_lint=true" >> "$GITHUB_OUTPUT"
else
echo "No package-owned files changed; skipping conventions lint."
@@ -99,13 +99,15 @@ jobs:
# namespace-enums) plus the repo-wide namespace-type rule (no
# all-static "namespace" types in any package). Exits non-zero on any
# unjustified ERROR; sanctioned exceptions carry a lint:allow /
# TRANSITIONAL / carve-out marker, and pre-existing namespace-type
# debt is grandfathered in scripts/lint-namespace-types-baseline.txt.
# TRANSITIONAL / carve-out marker, and pre-existing debt is
# grandfathered in the lint baseline files under scripts/.
./scripts/lint-ios-package-conventions.sh
mobile-core-package:
needs: detect-ios-changes
if: ${{ needs.detect-ios-changes.outputs.should_run == 'true' }}
if: >-
${{ needs.detect-ios-changes.outputs.should_run == 'true'
&& !(github.event_name == 'workflow_dispatch' && inputs.test_filter != '') }}
runs-on: ${{ (!inputs.runner || inputs.runner == 'auto') && (vars.MACOS_RUNNER_IOS || 'blacksmith-6vcpu-macos-26') || inputs.runner }}
timeout-minutes: 10
steps:
@@ -162,6 +164,10 @@ jobs:
run: |
swift test --package-path Packages/iOS/CmuxMobilePairedMac
- name: Run CmuxMobileChanges package tests
run: |
swift test --package-path Packages/iOS/CmuxMobileChanges
- name: Run CmuxMobileShell package tests
run: |
# iOS shell replay/liveness regressions live in this package target.
@@ -373,6 +379,9 @@ jobs:
-derivedDataPath "$IOS_DERIVED_DATA"
)
if [ -n "${TEST_FILTER:-}" ]; then
if [[ "$TEST_FILTER" == cmuxUITests/* ]]; then
XCODEBUILD_ARGS+=(-testPlan cmux-ui)
fi
XCODEBUILD_ARGS+=(-only-testing:"$TEST_FILTER")
else
# Full UI tests currently exceed the pull-request simulator budget
@@ -389,9 +398,11 @@ jobs:
# section is not always flushed into the tee'd log, and without the
# ✘ patterns a real Swift Testing failure exiting 65 was
# misclassified as a runner cleanup failure and turned the job green.
grep -Eq "Test Suite 'Selected tests' passed|Test Suite 'cmuxUITests' passed" "$log_path" &&
grep -Eq "Executed [1-9][0-9]* tests, with 0 failures \\(0 unexpected\\)" "$log_path" &&
! grep -Eq "Test Suite '.*' failed|Test Case '.*' failed|Assertion Failure|Failing tests:|with [1-9][0-9]* failures|with [0-9]+ failures \\([1-9][0-9]* unexpected\\)|✘ Test|✘ Suite" "$log_path"
(
grep -Eq "Executed [1-9][0-9]* tests, with 0 failures \\(0 unexpected\\)" "$log_path" ||
grep -Eq "Test run with [1-9][0-9]* tests?( in [1-9][0-9]* suites?)? passed" "$log_path"
) &&
! grep -Eq "Test Suite '.*' failed|Test Case '.*' failed|Assertion Failure|Failing tests:|with [1-9][0-9]* failures|with [0-9]+ failures \\([1-9][0-9]* unexpected\\)|✘ Test|✘ Suite|Test run with [0-9]+ tests?.* failed" "$log_path"
}
for attempt in 1 2; do
LOG_PATH="$IOS_DERIVED_DATA/logs/attempt-${attempt}.log"
@@ -401,11 +412,14 @@ jobs:
xcrun simctl boot "$SIMULATOR_ID" >/dev/null 2>&1 || true
xcrun simctl bootstatus "$SIMULATOR_ID" -b
if xcodebuild "${XCODEBUILD_ARGS[@]}" 2>&1 | tee "$LOG_PATH"; then
./scripts/ci/require_selected_test_execution.sh \
"$LOG_PATH" \
"${TEST_FILTER:-}"
exit 0
fi
status="${PIPESTATUS[0]}"
if selected_tests_passed_despite_xcodebuild_status "$LOG_PATH"; then
echo "xcodebuild exited $status after XCTest reported the selected tests passed with zero failures; treating this as a runner cleanup failure."
echo "xcodebuild exited $status after the selected tests passed; treating this as a runner cleanup failure."
exit 0
fi
if [ "$attempt" -lt 2 ] && grep -Eq "Timed out while launching application via Xcode|Failed to send signal 19|DTXMessage" "$LOG_PATH"; then
@@ -0,0 +1,62 @@
name: Testbox broker guard
# The main CI suite is dispatch-only right now, so the trust-boundary guard for
# the Blacksmith Testbox lane gets its own always-on workflow. It must run on
# every pull request, with no path filter: a path filter is exactly the thing a
# change that moves the guard could slip past.
on:
pull_request:
push:
branches:
- main
permissions: {}
concurrency:
group: testbox-broker-guard-${{ github.ref }}
cancel-in-progress: true
jobs:
guard:
name: Testbox broker trust boundary
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
- name: Install guard dependencies
run: python3 -m pip install --disable-pip-version-check --no-input PyYAML==6.0.3
- name: Validate Blacksmith Testbox broker trust boundary
run: python3 tests/test_ci_testbox_broker_guard.py
- name: Validate receipt-bound cleanup ownership check
run: ./tests/test_testbox_cleanup_receipt_ref.sh
- name: Lint the Testbox lane helpers
run: shellcheck scripts/blacksmith-bounded-command.sh scripts/blacksmith-cmux-tui-testbox-stage.sh scripts/blacksmith-testbox-cleanup.sh scripts/blacksmith-testbox-keepalive.sh
- name: Lint the Testbox warmup workflow
env:
ACTIONLINT_VERSION: "1.7.7"
ACTIONLINT_SHA256: "023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757"
run: |
set -euo pipefail
archive="$RUNNER_TEMP/actionlint.tar.gz"
curl -fsSL -o "$archive" \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
echo "${ACTIONLINT_SHA256} ${archive}" | sha256sum --check --strict
tar -xzf "$archive" -C "$RUNNER_TEMP" actionlint
"$RUNNER_TEMP/actionlint" \
.github/workflows/cmux-tui-testbox-warmup.yml \
.github/workflows/testbox-broker-guard.yml
+1 -1
View File
@@ -92,7 +92,7 @@ jobs:
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: GhosttyKit.xcframework
key: ghosttykit-${{ hashFiles('.gitmodules', 'ghostty/**') }}
key: ghosttykit-sentry-off-v1-${{ hashFiles('.gitmodules', 'ghostty/**') }}
- name: Download pre-built GhosttyKit.xcframework
if: steps.cache-ghosttykit.outputs.cache-hit != 'true'
+1 -164
View File
@@ -3,36 +3,19 @@ name: cmux-tui publish npm
on:
workflow_dispatch:
inputs:
publish_target:
description: "Package contents to publish under the shared npm cmux name"
required: true
default: tui
type: choice
options:
- tui
- sdk
version:
description: "Package version to publish, for example 0.1.0"
required: true
type: string
artifact_run_id:
description: "Successful cmux-tui release run containing verified packages"
required: false
type: string
sdk_verification_run_id:
description: "SDK npm workflow run whose TypeScript end-to-end job passed"
required: false
required: true
type: string
confirm_tui_cmux:
description: "Set true only for the coordinated npm cmux TUI publish"
required: true
default: false
type: boolean
confirm_sdk_cmux:
description: "Set true only for the coordinated npm cmux SDK publish"
required: true
default: false
type: boolean
permissions: {}
@@ -42,7 +25,6 @@ concurrency:
jobs:
validate-version:
if: inputs.publish_target == 'tui'
# This workflow's launcher publish deliberately omits --tag so the version
# becomes npm `latest`. Only strict stable X.Y.Z may go through here; a
# nightly-form version on latest would put a nightly in front of every
@@ -153,109 +135,7 @@ jobs:
exit 1
fi
validate-sdk-version:
if: inputs.publish_target == 'sdk'
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 5
permissions:
actions: read
contents: read
outputs:
release_sha: ${{ steps.release.outputs.release_sha }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 0
- name: Require protected main and verified SDK run
id: release
env:
GH_TOKEN: ${{ github.token }}
DISPATCH_VERSION: ${{ inputs.version }}
SDK_VERIFICATION_RUN_ID: ${{ inputs.sdk_verification_run_id }}
run: |
set -euo pipefail
[[ "$GITHUB_REF" == "refs/heads/main" ]] || {
echo "Refusing to publish the SDK from $GITHUB_REF; dispatch this workflow on main." >&2
exit 1
}
[[ "$DISPATCH_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "workflow_dispatch version must match X.Y.Z" >&2
exit 1
}
[[ "$SDK_VERIFICATION_RUN_ID" =~ ^[0-9]+$ ]] || {
echo "sdk_verification_run_id must be a GitHub Actions run ID" >&2
exit 1
}
git fetch --force origin main
current_main="$(git rev-parse origin/main)"
if ! git merge-base --is-ancestor "$GITHUB_SHA" "$current_main"; then
echo "workflow commit $GITHUB_SHA is not contained in protected main $current_main" >&2
exit 1
fi
python3 - "$DISPATCH_VERSION" <<'PY'
import json
import pathlib
import sys
import tomllib
expected = sys.argv[1]
root = pathlib.Path.cwd()
versions = {
"typescript package.json": json.loads((root / "cmux-tui/bindings/typescript/package.json").read_text())["version"],
"python pyproject.toml": tomllib.loads((root / "cmux-tui/bindings/python/pyproject.toml").read_text())["project"]["version"],
"rust Cargo.toml": tomllib.loads((root / "cmux-tui/bindings/rust/Cargo.toml").read_text())["package"]["version"],
}
mismatches = {name: got for name, got in versions.items() if got != expected}
if mismatches:
for name, got in mismatches.items():
print(f"{name}: expected {expected}, got {got}", file=sys.stderr)
raise SystemExit(1)
print(f"All package versions match {expected}")
PY
IFS=$'\t' read -r actual_path verified_sha event status <<<"$(
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SDK_VERIFICATION_RUN_ID" \
--jq '[.path, .head_sha, .event, .status] | @tsv'
)"
if [[ "$actual_path" != ".github/workflows/sdk-publish-npm.yml" ]]; then
echo "verification run $SDK_VERIFICATION_RUN_ID came from $actual_path" >&2
exit 1
fi
if [[ "$event" != "workflow_dispatch" || "$status" != "completed" ]]; then
echo "verification run must be a completed workflow_dispatch run; got $event/$status" >&2
exit 1
fi
IFS=$'\t' read -r job_count job_status job_conclusion <<<"$(
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SDK_VERIFICATION_RUN_ID/jobs" \
--jq '[.jobs[] | select(.name == "bindings-e2e-typescript")] as $jobs |
[($jobs | length), ($jobs[0].status // ""), ($jobs[0].conclusion // "")] | @tsv'
)"
if [[ "$job_count" != "1" || "$job_status" != "completed" || "$job_conclusion" != "success" ]]; then
echo "verification run TypeScript end-to-end job is not a single completed success" >&2
exit 1
fi
git merge-base --is-ancestor "$verified_sha" "$GITHUB_SHA" || {
echo "verification commit $verified_sha is not an ancestor of $GITHUB_SHA" >&2
exit 1
}
if ! git diff --quiet "$verified_sha" "$GITHUB_SHA" -- \
cmux-tui \
.github/workflows/sdk-publish-npm.yml \
':(exclude)cmux-tui/bindings/RELEASING.md'; then
echo "SDK sources or verification workflow changed after run $SDK_VERIFICATION_RUN_ID" >&2
exit 1
fi
echo "release_sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
publish:
if: inputs.publish_target == 'tui'
needs: validate-version
runs-on: ubuntu-latest # github-hosted-required: npm provenance needs a github-hosted runner
permissions:
@@ -336,46 +216,3 @@ jobs:
# Deliberately do not pass --tag: this coordinated TUI publish takes
# over the cmux latest dist-tag from the old 0.8.3 CLI when version > 0.8.3.
npm publish --provenance dist/npm-packages/cmux
publish-sdk:
if: inputs.publish_target == 'sdk'
needs: validate-sdk-version
runs-on: ubuntu-latest # github-hosted-required: npm provenance needs a github-hosted runner
permissions:
contents: read
id-token: write
environment:
name: npm-tui
url: https://www.npmjs.com/package/cmux
steps:
- name: Require npm cmux SDK confirmation
if: inputs.confirm_sdk_cmux != true
run: |
echo "Refusing to publish npm package cmux for the SDK without confirm_sdk_cmux=true." >&2
exit 1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
ref: ${{ needs.validate-sdk-version.outputs.release_sha }}
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22.14.0"
registry-url: https://registry.npmjs.org
- name: Install npm with OIDC support
run: npm install -g [email protected]
- name: Build and test SDK package
working-directory: cmux-tui/bindings/typescript
run: |
npm ci --no-audit --no-fund
npm test
- name: Publish SDK package
working-directory: cmux-tui/bindings/typescript
# Keep the TUI launcher on `latest`; SDK consumers opt in with
# `npm install cmux@sdk`.
run: npm publish --provenance --tag sdk
+5
View File
@@ -18,6 +18,7 @@ ios/Config/AppStoreConnect.local.plist
# Swift Package Manager
.swiftpm/
.ci-source-packages/
.ci-reload-derived-data/
# GhosttyKit binary (built from ghostty submodule via scripts/setup.sh)
GhosttyKit.xcframework
@@ -73,3 +74,7 @@ artifacts/
# tmux verbose debug logs (tmux -v) that land in the cwd
tmux-*.log
# Remote-only Blacksmith cmux-tui benchmark output
/testbox-benchmark/
/.cmux-scratch/
@@ -1,15 +1,13 @@
{
"images" : [
{
"filename" : "HermesAgent.svg",
"idiom" : "universal"
"filename" : "HermesAgent.png",
"idiom" : "universal",
"scale" : "1x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 561 KiB

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#101820"/>
<path d="M20 47V17h7v12h10V17h7v30h-7V35H27v12h-7Z" fill="#F4F7FA"/>
<path d="M15 13h34v6H15z" fill="#4AD7D1"/>
<path d="M15 45h34v6H15z" fill="#CFA9FF"/>
<path d="M18 13l5-6 5 6M36 13l5-6 5 6M18 51l5 6 5-6M36 51l5 6 5-6" fill="none" stroke="#F4F7FA" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 472 B

+93
View File
@@ -2,6 +2,99 @@
All notable changes to cmux are documented here.
## [0.64.22] - 2026-08-03
### Fixed
- Fix a crash seconds after launch on Intel Macs; cmux is now the only process-wide crash handler, and embedded GhosttyKit no longer links Ghostty's native Sentry initializer ([#9436](https://github.com/manaflow-ai/cmux/pull/9436))
- Fix `cmux ssh <host>` failing immediately with a shell syntax error from the generated startup script ([#9425](https://github.com/manaflow-ai/cmux/pull/9425)) -- thanks @KousukeUchiyama for the report!
- Clear Dock notifications when you focus the pane that raised them ([#9418](https://github.com/manaflow-ai/cmux/pull/9418))
- Keep a restored Claude agent on its own account instead of falling back to the ambient one ([#9419](https://github.com/manaflow-ai/cmux/pull/9419)) -- thanks @seanyoungberg for the report!
- Stop bash shell integration printing `cannot overwrite existing file` on every prompt under `set -o noclobber` ([#9420](https://github.com/manaflow-ai/cmux/pull/9420)) -- thanks @8bit-void for the report!
- Fail closed when `close` or `respawn-pane` is given an explicit `--surface` that no longer exists, instead of acting on a different live surface ([#9422](https://github.com/manaflow-ai/cmux/pull/9422)) -- thanks @PhilipPinckaers for the report!
### Thanks to 5 contributors!
- [@8bit-void](https://github.com/8bit-void)
- [@austinywang](https://github.com/austinywang)
- [@KousukeUchiyama](https://github.com/KousukeUchiyama)
- [@PhilipPinckaers](https://github.com/PhilipPinckaers)
- [@seanyoungberg](https://github.com/seanyoungberg)
## [0.64.21] - 2026-08-02
### Added
- Native iPhone and iPad Simulator panes, with their own commands and automation ([#7857](https://github.com/manaflow-ai/cmux/pull/7857))
- First-class Mosh transport for remote workspaces ([#8442](https://github.com/manaflow-ai/cmux/pull/8442))
- Workspace-wide terminal font zoom on Cmd+Ctrl+= / Cmd+Ctrl+- / Cmd+Ctrl+0 ([#8791](https://github.com/manaflow-ai/cmux/pull/8791)), and per-tab zoom now persists across restarts ([#8543](https://github.com/manaflow-ai/cmux/pull/8543))
- Cmd+Shift+T reopens the last closed item ([#9132](https://github.com/manaflow-ai/cmux/pull/9132))
- Cmd+[ and Cmd+] traverse global workspace focus history, and pane cycling becomes rebindable ([#9329](https://github.com/manaflow-ai/cmux/pull/9329)) -- thanks @azooz2003-bit! -- alongside a workspace-only focus history setting ([#8654](https://github.com/manaflow-ai/cmux/pull/8654))
- Move active surfaces between panes with automatic directional splits ([#8764](https://github.com/manaflow-ai/cmux/pull/8764)); `goto_split:previous` and `goto_split:next` cycle through every pane with wrapping ([#2639](https://github.com/manaflow-ai/cmux/pull/2639)) -- thanks @mykmelez!
- Dock panes persist across session restore ([#8690](https://github.com/manaflow-ai/cmux/pull/8690)), with full Dock surface runtime parity ([#8782](https://github.com/manaflow-ai/cmux/pull/8782))
- Reopen closed workspaces with sticky repo identity ([#8841](https://github.com/manaflow-ai/cmux/pull/8841))
- Target browser profiles from the CLI ([#8874](https://github.com/manaflow-ai/cmux/pull/8874)), and Command-clicked HTML files render in browser panes ([#9096](https://github.com/manaflow-ai/cmux/pull/9096))
- Sidebar account and mobile pairing controls ([#8354](https://github.com/manaflow-ai/cmux/pull/8354)); sidebar metadata renders Markdown links ([#8663](https://github.com/manaflow-ai/cmux/pull/8663)) -- thanks @djova!
- Notification feed read state is a leading swipe with mark-unread ([#8868](https://github.com/manaflow-ai/cmux/pull/8868)) -- thanks @azooz2003-bit!
- Idle background agents hibernate under critical memory pressure even when routine Agent Hibernation is off ([#9090](https://github.com/manaflow-ai/cmux/pull/9090))
- `cmux restore` runs without a shell ([#9265](https://github.com/manaflow-ai/cmux/pull/9265))
- iOS (beta): stream Mac browser panes to the phone, interactive and pixel-perfect, with dialogs mirrored ([#8298](https://github.com/manaflow-ai/cmux/pull/8298)) -- thanks @azooz2003-bit!
- iOS (beta): chronological notification feed ([#8210](https://github.com/manaflow-ai/cmux/pull/8210)) -- thanks @azooz2003-bit!
- iOS (beta): launch agent workspaces straight from the task composer ([#7670](https://github.com/manaflow-ai/cmux/pull/7670))
- iOS (beta): Tailscale connection method opt-in with QR-authorized pairing ([#9247](https://github.com/manaflow-ai/cmux/pull/9247)) -- thanks @azooz2003-bit!
- iOS (beta): haptic feedback setting ([#8797](https://github.com/manaflow-ai/cmux/pull/8797)), Open Folders on Tap ([#8524](https://github.com/manaflow-ai/cmux/pull/8524)), unified animated toasts ([#8376](https://github.com/manaflow-ai/cmux/pull/8376)), and workspace identity customization ([#8636](https://github.com/manaflow-ai/cmux/pull/8636)) -- thanks @azooz2003-bit!
### Changed
- Workspace initial commands launch through your login shell ([#8801](https://github.com/manaflow-ai/cmux/pull/8801)) -- thanks @azooz2003-bit! -- and auto-resume uses the normal terminal shell ([#8837](https://github.com/manaflow-ai/cmux/pull/8837))
- iOS (beta): the phone-to-Mac transport is rebuilt on one connectivity authority, with authenticated discovery, named disconnect reasons, and relay-credential rollover ([#9284](https://github.com/manaflow-ai/cmux/pull/9284), [#8840](https://github.com/manaflow-ai/cmux/pull/8840), [#8716](https://github.com/manaflow-ai/cmux/pull/8716), [#8494](https://github.com/manaflow-ai/cmux/pull/8494)) -- thanks @azooz2003-bit!
- iOS (beta): terminal scrolling is local and smooth on screen-anchored render grids ([#8860](https://github.com/manaflow-ai/cmux/pull/8860)) -- thanks @azooz2003-bit!
- iOS (beta): state sync v2 replaces the invalidate-and-refetch loop with per-record deltas ([#8284](https://github.com/manaflow-ai/cmux/pull/8284)) -- thanks @azooz2003-bit!
- iOS (beta): onboarding is rebuilt around a live agent handoff ([#8418](https://github.com/manaflow-ai/cmux/pull/8418)), as a swipeable tour ([#9158](https://github.com/manaflow-ai/cmux/pull/9158)) with a Game of Life backdrop on every page ([#8880](https://github.com/manaflow-ai/cmux/pull/8880)) -- thanks @azooz2003-bit!
- iOS (beta): removing a Mac from a phone hides it for that phone only, instead of deleting it everywhere ([#8760](https://github.com/manaflow-ai/cmux/pull/8760), [#8778](https://github.com/manaflow-ai/cmux/pull/8778)) -- thanks @azooz2003-bit!
### Fixed
- Fix leaked `openThread` loops burning ~90% of cmux idle CPU ([#8851](https://github.com/manaflow-ai/cmux/pull/8851))
- Fix workspace-switch renderer freezes ([#8793](https://github.com/manaflow-ai/cmux/pull/8793)), reclaim hidden Ghostty renderer memory ([#8998](https://github.com/manaflow-ai/cmux/pull/8998)), and fix the Vault sidebar beachball at large session counts ([#8680](https://github.com/manaflow-ai/cmux/pull/8680))
- Fix Vim Mode cursor and selection rendering ([#8995](https://github.com/manaflow-ai/cmux/pull/8995))
- Fix TextBox IME composition rendering ([#8688](https://github.com/manaflow-ai/cmux/pull/8688))
- Fix zsh prompt wrap spacer lines by letting Ghostty own prompt layout ([#8964](https://github.com/manaflow-ai/cmux/pull/8964))
- Fix Settings and main window zombies under AeroSpace ([#8513](https://github.com/manaflow-ai/cmux/pull/8513)) -- thanks @fml09!
- Fix a Debug-build crash on macOS 26.5 from non-finite sidebar divider coordinates ([#9156](https://github.com/manaflow-ai/cmux/pull/9156)) -- thanks @oscarbrey!
- Fix Mermaid diagrams double-scaling under viewer zoom ([#8914](https://github.com/manaflow-ai/cmux/pull/8914)), restore the focused-read indicator after a surface-scoped mark-read ([#8927](https://github.com/manaflow-ai/cmux/pull/8927)), keep Pi launch arguments when resuming a restored session ([#8912](https://github.com/manaflow-ai/cmux/pull/8912)), and import appearance at Settings store init instead of live-applying it ([#8913](https://github.com/manaflow-ai/cmux/pull/8913)) -- thanks @ejc3!
- Notify only after the Pi agent settles ([#8574](https://github.com/manaflow-ai/cmux/pull/8574)) -- thanks @mrohan-sq!
- Tear down remote daemon PTY sessions once ([#8643](https://github.com/manaflow-ai/cmux/pull/8643)) -- thanks @ejc3! -- and support `respawn-pane` in the Go relay tmux compatibility layer ([#8660](https://github.com/manaflow-ai/cmux/pull/8660)) -- thanks @bencollins2!
- Exclude `.attrib` from watched filesystem events ([#8659](https://github.com/manaflow-ai/cmux/pull/8659)) -- thanks @varomorf!
- Preserve surface IDs in workstream events ([#8703](https://github.com/manaflow-ai/cmux/pull/8703)) -- thanks @revanthreddy-hai!
- Stop the sidebar PR poller from re-downloading every repo's full PR list on each poll ([#8521](https://github.com/manaflow-ai/cmux/pull/8521)) -- thanks @joshfree!
- Restore Codex ([#9370](https://github.com/manaflow-ai/cmux/pull/9370)), Kimi Code ([#8584](https://github.com/manaflow-ai/cmux/pull/8584)), Grok ([#9382](https://github.com/manaflow-ai/cmux/pull/9382)), and Pi ([#9399](https://github.com/manaflow-ai/cmux/pull/9399)) sessions across relaunch, and stop duplicate agent resumes ([#8619](https://github.com/manaflow-ai/cmux/pull/8619))
- ssh-tmux: fix focus after single-pane promotion ([#9020](https://github.com/manaflow-ai/cmux/pull/9020)), named-key encoding for the remote `TERM` ([#9273](https://github.com/manaflow-ai/cmux/pull/9273)), and terminal replies leaking into reattached panes ([#9272](https://github.com/manaflow-ai/cmux/pull/9272)); fix workspace shortcuts from hosted tmux terminals ([#8621](https://github.com/manaflow-ai/cmux/pull/8621))
- Fix SSH relay deadlock after app restart ([#9105](https://github.com/manaflow-ai/cmux/pull/9105)), stale SSH workspace connection status ([#9085](https://github.com/manaflow-ai/cmux/pull/9085)), remote PTY `PATH` inherited from cmuxd ([#8677](https://github.com/manaflow-ai/cmux/pull/8677)), and login-shell resolution before terminal spawn ([#8681](https://github.com/manaflow-ai/cmux/pull/8681))
- Fix sidebar reopen cutoff render ([#8626](https://github.com/manaflow-ai/cmux/pull/8626)), row clipping during height-changing reorder ([#9189](https://github.com/manaflow-ai/cmux/pull/9189)), idle layout livelock ([#8532](https://github.com/manaflow-ai/cmux/pull/8532)), and status URL clicks ([#8528](https://github.com/manaflow-ai/cmux/pull/8528))
- Fix Dock paste routing to the selected terminal ([#9112](https://github.com/manaflow-ai/cmux/pull/9112)), Dock terminal working-directory inheritance ([#8691](https://github.com/manaflow-ai/cmux/pull/8691)), and Cmd-click link opening in Dock terminals ([#8594](https://github.com/manaflow-ai/cmux/pull/8594))
- Browser: fix navigation for terminal-wrapped URL pastes ([#8601](https://github.com/manaflow-ai/cmux/pull/8601)), automation recovery after load failures ([#8548](https://github.com/manaflow-ai/cmux/pull/8548)), partial blank screenshots ([#9281](https://github.com/manaflow-ai/cmux/pull/9281)), and blurred Google Sheets canvas rendering ([#8697](https://github.com/manaflow-ai/cmux/pull/8697))
- Fix inline code escaping in the Markdown viewer ([#9274](https://github.com/manaflow-ai/cmux/pull/9274)) and composer attachment thumbnail re-rasterization ([#8817](https://github.com/manaflow-ai/cmux/pull/8817))
- Fix renderer presentation for background-created surfaces ([#8540](https://github.com/manaflow-ai/cmux/pull/8540)) and stale semantic prompts duplicating inline TUI frames ([#9275](https://github.com/manaflow-ai/cmux/pull/9275))
- Fix workspace group anchor numbering ([#9176](https://github.com/manaflow-ai/cmux/pull/9176)); closing a group's anchor keeps the group instead of scattering its members to the root ([#8925](https://github.com/manaflow-ai/cmux/pull/8925))
- Preserve workspace IDs across session restore ([#8695](https://github.com/manaflow-ai/cmux/pull/8695)) and restored resume workspace titles ([#8687](https://github.com/manaflow-ai/cmux/pull/8687)); fit same-display restored windows to visible bounds ([#8675](https://github.com/manaflow-ai/cmux/pull/8675))
- Fix a `DispatchWorkItem` chain stack overflow ([#8615](https://github.com/manaflow-ai/cmux/pull/8615)) and subprocess pipe descriptor leaks ([#9187](https://github.com/manaflow-ai/cmux/pull/9187))
- iOS (beta): preserve terminal input ordering under fast typing ([#8682](https://github.com/manaflow-ai/cmux/pull/8682)), scroll position across mid-stream verified replays ([#9032](https://github.com/manaflow-ai/cmux/pull/9032)), and keyboard focus after the photo picker ([#9287](https://github.com/manaflow-ai/cmux/pull/9287)) -- thanks @azooz2003-bit!
- iOS (beta): fix a startup crash from sentry-init racing environ mutation ([#9238](https://github.com/manaflow-ai/cmux/pull/9238)) and TestFlight crash paths ([#9034](https://github.com/manaflow-ai/cmux/pull/9034))
- iOS (beta): fix workspace-list scroll stutter from live updates ([#9139](https://github.com/manaflow-ai/cmux/pull/9139)), and make the notification feed scroll fast with thousands of items ([#9141](https://github.com/manaflow-ai/cmux/pull/9141)) -- thanks @azooz2003-bit!
### Thanks to 13 contributors!
- [@austinywang](https://github.com/austinywang)
- [@azooz2003-bit](https://github.com/azooz2003-bit)
- [@bencollins2](https://github.com/bencollins2)
- [@djova](https://github.com/djova)
- [@ejc3](https://github.com/ejc3)
- [@fml09](https://github.com/fml09)
- [@joshfree](https://github.com/joshfree)
- [@lawrencecchen](https://github.com/lawrencecchen)
- [@mrohan-sq](https://github.com/mrohan-sq)
- [@mykmelez](https://github.com/mykmelez)
- [@oscarbrey](https://github.com/oscarbrey)
- [@revanthreddy-hai](https://github.com/revanthreddy-hai)
- [@varomorf](https://github.com/varomorf)
## [0.64.20] - 2026-07-19
### Added
+29 -3
View File
@@ -17,6 +17,10 @@ A tag gives the app its own name, bundle ID, socket, and derived data path, so i
Other variants: `reloadp.sh` (Release), `reloads.sh` (Release as isolated "cmux STAGING"), `reload2.sh --tag <tag>` (both).
## Shared Mac fleet capacity
Every healthy slot in the canonical Mac fleet is general-purpose. Builds, iOS archives, tests, profiling, simulator and UI verification, and any other resource-intensive workload may use any available slot. Do not wait for an AWS-only builder or infer capacity from a workload label. Use the shared lease state and slot-isolated paths supplied by the fleet tooling.
Compile-only check, no launch:
```bash
@@ -44,15 +48,33 @@ The helper refuses to run without `CMUX_TAG`, targets `/tmp/cmux-debug-<tag>.soc
## iOS builds open on the iPhone by default
Any work verified by opening the iOS app installs BOTH an isolated-simulator build AND the same build on the user's iPhone. Never stop at simulator-only. Use `ios/scripts/reload-cloud.sh --tag <tag>` (or `ios/scripts/reload.sh --tag <tag>`); with a default iPhone configured (`CMUX_IPHONE_DEVICE_ID` or `~/.config/cmux/iphone-device-id`) the device leg is automatic, and `--device-id <id>` still overrides (`xcrun devicectl list devices`). Auto sign-in and auto-pair apply as usual; launch the app so it is immediately open on the phone. The simulator leg uses the tag's own isolated device `cmux-dev-<slug>`, created on demand; do not target a shared or user-visible simulator.
Any work verified by opening the iOS app installs BOTH an isolated-simulator build AND the same build on the user's iPhone. Never stop at simulator-only. Use `ios/scripts/reload-cloud.sh --tag <tag>` (or `ios/scripts/reload.sh --tag <tag>`); with a default iPhone configured (`CMUX_IPHONE_DEVICE_ID` or `~/.config/cmux/iphone-device-id`) the device leg is automatic, and `--device-id <id>` still overrides (`xcrun devicectl list devices`). Physical iPhone builds always select the `personal` auth profile. Agent-driven Simulator verification always selects `agent`. Both named profiles live in `~/.secrets/cmuxterm-dev.env`; neither may fall back to the other. The simulator leg uses the tag's own isolated device `cmux-dev-<slug>`, created on demand; do not target a shared or user-visible simulator.
**Every phone install MUST be authenticated before handoff. Installed-but-signed-out is a failed install.** A tagged bundle id can retain an older account, so every authenticated launch clears that tagged session, signs both surfaces into the selected profile, verifies the exact tagged Mac account through `auth status`, then mints the pairing ticket. The iPhone auth gate passes only after the same-account host accepts the phone RPC and emits `mobile.rpc.ready`. `scripts/verify-iphone-auth.sh --tag <tag> [--device-id <id>]` repeats the Mac-account check, relaunches the phone without credentials, and passes only when persisted phone state reconnects. Never install with raw `devicectl device install app`, and never pass `--no-sign-in`/`--no-attach`/`--no-setup` for a dogfood build. The scripts refuse those device paths unless a human sets `CMUX_ALLOW_UNAUTHENTICATED_INSTALL=1`. If setup fails, report the gate reason and exact retry command.
Every phone build requires the same-tag Mac dev build (the iOS app is unusable without its Mac). The reload scripts build the Mac tag first when it is missing and refuse to ship a phone-only build if that fails; do not bypass this with `CMUX_IOS_SKIP_MAC_BUILD_CHECK` in normal work.
If the iPhone is unreachable at build time, the reload still completes: the signed build is parked in the offline install queue (`scripts/iphone-install-queue.sh`, persistent under `~/Library/Application Support/cmux-dev/iphone-install-queue`), and a LaunchAgent auto-installs and launches it within seconds of the phone being plugged back in or reappearing on the network, then sends a `cmux notify` with the installed tags. The LaunchAgent is a one-time per-Mac setup: `scripts/install-iphone-queue-agent.sh install`; it runs a stable copy of the queue script, so re-run the installer after changing that script. In the handoff, report the queued state (`scripts/iphone-install-queue.sh list`) instead of treating an unreachable phone as a failure; `drain` retries manually, `clear` abandons a queued build.
If the iPhone is unreachable at build time, the signed build is parked in `scripts/iphone-install-queue.sh`. Each entry stores the chosen profile, normalized account, and credentials-file path. Drain revalidates that snapshot before device mutation and uses installed stable copies of the launcher and auth helpers, so an old or pruned feature worktree cannot change policy. Install or refresh that control plane with `scripts/install-iphone-queue-agent.sh install`. Report `scripts/iphone-install-queue.sh list` in the handoff; `drain` retries delivery and `clear` abandons a queued build.
## All fleet slots are general-purpose
Agent verification, macOS/iOS builds, archives, tests, profiling, and any other work too resource-intensive for the local Mac use the same Mac fleet. A slot is not a "build slot" or a "verify slot". From the cmuxterm-hq checkout that owns this worktree, every workload leases the canonical `~/.config/macfleet/hosts.json` inventory and shared `maclease` state.
Before waiting for a builder, run `scripts/macfleet-doctor.sh report --probe` from that hq checkout. If it reports `needs-sync`, run `scripts/macfleet-doctor.sh sync --apply`; it backs up the canonical manifest and merges legacy `hosts-verify.json` entries by SSH endpoint. Refresh the hq checkout before diagnosing capacity. Do not infer capacity from a stale checkout, one pool tag, or a remembered host list.
Agent verification runs on the fleet, not on the local Mac. `scripts/verify-remote.sh` leases a general-purpose slot, pushes the tagged build to the leased Mac, drives it there (per-lease uniquely named simulator for iOS; console launch with debug-socket and computer-use evidence for macOS), and fetches screenshots, recordings, and logs back into the hq `artifacts/verify-remote/` directory:
```bash
scripts/verify-remote.sh ios --tag <tag>
scripts/verify-remote.sh mac --tag <tag>
scripts/verify-remote.sh capacity # all-purpose slots
```
Boot a local simulator only when all-purpose `capacity` reports no free slot, and keep at most 3 local sims booted. Scripted XCUITests go through the hosted `test-e2e.yml` lane when appropriate. The physical-iPhone signing/install leg stays local via the install queue; its archive build may use any healthy fleet slot. Verify leases carry a description and TTL, so a crashed agent frees its slot automatically; see `skills/infra/macfleet/references/verify-remote.md` in cmuxterm-hq for the shared-pool contract and host onboarding.
## iOS dev auth
`ios/scripts/reload.sh` and `scripts/mobile-dev-launch.sh` auto-sign-in from `~/.secrets/cmuxterm-dev.env`. If the phone lands on the login screen or the helper reports missing credentials, do not ask the user to authenticate every build. Tell them to run `scripts/setup-team-dev.sh` once; it verifies their Stack login and writes the file chmod 600. Manual fallback: create it with `CMUX_DOGFOOD_STACK_EMAIL=...` and `CMUX_DOGFOOD_STACK_PASSWORD=...`.
`~/.secrets/cmuxterm-dev.env` is the only mobile dev credential file. `CMUX_DOGFOOD_STACK_*` is the `personal` profile for physical iPhone dogfood. `CMUX_UITEST_STACK_*` is the `agent` profile for isolated Simulators. Run `scripts/setup-team-dev.sh` once to verify and merge the personal pair without deleting the agent pair. Use `scripts/mobile-dev-launch.sh --check-auth-contract --auth-profile personal` or `--auth-profile agent` for a mutation-free preflight. Never substitute one profile when the requested profile is incomplete.
## Regression test commits
@@ -109,3 +131,7 @@ Detailed contributor rules live in `skills/`. Use the task-specific skill before
- `cmux-shared-behavior`: shared action paths for multi-entrypoint behavior and optimistic updates.
- `cmux-ghostty`: Ghostty submodule and GhosttyKit workflow.
- `cmux-release`: release, version bump, changelog, pretag guard, release assets.
- Blacksmith Testbox (remote Linux builds for cmux-tui): warm your own box before any cmux-tui Rust or Zig
build, and never compile cmux-tui on the Mac. The skill lives in cmuxterm-hq at
`skills/infra/blacksmith-testbox/SKILL.md`; the workflows, `scripts/blacksmith-*.sh`, and the
`tests/test_testbox_*` guards stay here. Quickest path: `./scripts/blacksmith-testbox-demo.sh`.
+21 -1
View File
@@ -187,7 +187,27 @@ enum AgentHookNotificationClassifier {
}
enum AgentHookNotificationPolicy {
static let dedupeEligibleAgents: Set<String> = ["grok", "antigravity"]
static let dedupeEligibleAgents: Set<String> = ["grok", "antigravity", "hermes-agent"]
static func notificationTitle(
agentName: String,
displayName: String,
surfaceTitle: String?
) -> String {
guard agentName == "pi",
let surfaceTitle = surfaceTitle?.trimmingCharacters(in: .whitespacesAndNewlines),
!surfaceTitle.isEmpty else {
return displayName
}
if surfaceTitle.caseInsensitiveCompare(displayName) == .orderedSame
|| surfaceTitle.range(
of: "\(displayName) · ",
options: [.anchored, .caseInsensitive]
) != nil {
return surfaceTitle
}
return "\(displayName) · \(surfaceTitle)"
}
/// Stable per-session fingerprint. Grok 0.2.91 emits an identical generic
/// "Tool permission requested" Notification for every tool step, even in
+212 -168
View File
@@ -9,7 +9,8 @@ enum CLIExecutableLocator {
if size > 0 {
var buffer = Array<CChar>(repeating: 0, count: Int(size))
if _NSGetExecutablePath(&buffer, &size) == 0 {
return URL(fileURLWithPath: String(cString: buffer))
let pathBytes = buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }
return URL(fileURLWithPath: String(decoding: pathBytes, as: UTF8.self))
.resolvingSymlinksInPath()
.standardizedFileURL
}
@@ -73,13 +74,55 @@ enum CLIExecutableLocator {
}
}
enum CLISocketPathSource {
enum CLISocketPathSource: Equatable, Sendable {
case explicitFlag
case environment
case implicitDefault
}
enum CLISocketPathResolver {
/// The observable result of resolving an implicit CLI socket path.
struct CLISocketPathResolution: Sendable {
let source: CLISocketPathSource
let requestedPath: String
let candidatePaths: [String]
let selectedPath: String?
/// Whether resolution may proceed to the socket client.
///
/// Explicit paths are intentionally not probed here: their identity is
/// pinned and the normal client error must report the requested path.
var hasLiveSocket: Bool {
source != .implicitDefault || selectedPath != nil
}
/// Whether discovery selected a different path than the one the caller expected.
var didReroute: Bool {
guard let selectedPath else { return false }
return !CLISocketPathResolver.pathsMatchForDiagnostics(requestedPath, selectedPath)
}
/// A user-facing diagnostic for an implicit discovery failure.
var failureMessage: String {
let header = String(
localized: "cli.socket.error.discoveryFailed",
defaultValue: "No live cmux socket found. Tried:"
)
let paths = candidatePaths.map { " \($0)" }.joined(separator: "\n")
return paths.isEmpty ? header : "\(header)\n\(paths)"
}
/// A user-facing notice explaining a deterministic implicit reroute.
var rerouteNotice: String? {
guard let selectedPath, didReroute else { return nil }
let template = String(
localized: "cli.socket.notice.rerouted",
defaultValue: "cmux: default socket %@ is unavailable; using %@."
)
return String.localizedStringWithFormat(template, requestedPath, selectedPath)
}
}
struct CLISocketPathResolver {
enum SocketPathEntry {
case missing
case socket(ownerUserID: uid_t)
@@ -93,6 +136,36 @@ enum CLISocketPathResolver {
private static let nightlySocketPath = "/tmp/cmux-nightly.sock"
private static let stagingSocketPath = "/tmp/cmux-staging.sock"
private let environment: [String: String]
private let bundleIdentifier: String?
private let currentUserID: uid_t
private let inspectSocketPathEntry: (String) -> SocketPathEntry
private let socketAcceptsConnections: (String) -> Bool
private let stateDirectory: URL
/// Creates a resolver with explicit discovery inputs and filesystem probes.
///
/// The inputs are captured once so command dispatch uses one deterministic
/// resolution pass and tests can provide an isolated probe implementation.
init(
environment: [String: String] = ProcessInfo.processInfo.environment,
bundleIdentifier: String? = Self.currentAppBundleIdentifier(),
currentUserID: uid_t = getuid(),
inspectSocketPathEntry: @escaping (String) -> SocketPathEntry = Self.inspectSocketPathEntry,
socketAcceptsConnections: @escaping (String) -> Bool = Self.socketAcceptsConnections,
fileManager: FileManager = .default,
stateDirectory: URL? = nil
) {
self.environment = environment
self.bundleIdentifier = bundleIdentifier
self.currentUserID = currentUserID
self.inspectSocketPathEntry = inspectSocketPathEntry
self.socketAcceptsConnections = socketAcceptsConnections
self.stateDirectory = stateDirectory ?? CmuxStateDirectory.url(
homeDirectory: fileManager.homeDirectoryForCurrentUser
)
}
static func defaultSocketPath(
bundleIdentifier: String?,
environment: [String: String] = ProcessInfo.processInfo.environment
@@ -115,6 +188,22 @@ enum CLISocketPathResolver {
return stablePath ?? legacyDefaultSocketPath
}
private var resolvedStableDefaultSocketPath: String {
stateDirectory.appendingPathComponent(Self.stableSocketFileName, isDirectory: false).path
}
private func resolvedDefaultSocketPath() -> String {
SocketPathMarkerFiles.defaultSocketPath(
bundleIdentifier: bundleIdentifier,
environment: environment,
isDebugBuild: false,
stableSocketPath: resolvedStableDefaultSocketPath,
debugSocketPath: Self.fallbackSocketPath,
nightlySocketPath: Self.nightlySocketPath,
stagingSocketPath: Self.stagingSocketPath
)
}
private static func userScopedStableSocketPath(currentUserID: uid_t = getuid()) -> String {
stableSocketDirectoryURL()?
.appendingPathComponent("cmux-\(currentUserID).sock", isDirectory: false)
@@ -136,83 +225,81 @@ enum CLISocketPathResolver {
)
}
static func resolve(
/// Resolves a socket using one ordered, liveness-aware discovery pass.
///
/// Explicit flag and environment paths are deliberately returned verbatim and are
/// never probed or rerouted. Implicit discovery only selects a path after a real
/// non-blocking connect succeeds; a stale socket file is never handed to the client.
func resolve(
requestedPath: String,
source: CLISocketPathSource,
environment: [String: String] = ProcessInfo.processInfo.environment,
bundleIdentifier: String? = currentAppBundleIdentifier(),
currentUserID: uid_t = getuid(),
inspectSocketPathEntry: (String) -> SocketPathEntry = inspectSocketPathEntry
) -> String {
source: CLISocketPathSource
) -> CLISocketPathResolution {
guard source == .implicitDefault else {
return requestedPath
return CLISocketPathResolution(
source: source,
requestedPath: requestedPath,
candidatePaths: [requestedPath],
selectedPath: requestedPath
)
}
let variant = SocketPathMarkerFiles.variant(bundleIdentifier: bundleIdentifier, environment: environment)
if case .stable = variant,
canConnect(to: requestedPath, currentUserID: currentUserID, inspectSocketPathEntry: inspectSocketPathEntry) {
return requestedPath
let candidates = Self.dedupe(candidatePaths(requestedPath: requestedPath))
let selectedPath = candidates.first { path in
canConnect(to: path)
}
let candidates = dedupe(candidatePaths(
return CLISocketPathResolution(
source: source,
requestedPath: requestedPath,
environment: environment,
bundleIdentifier: bundleIdentifier
))
// Prefer sockets that are currently accepting connections.
for path in candidates where canConnect(
to: path,
currentUserID: currentUserID,
inspectSocketPathEntry: inspectSocketPathEntry
) {
return path
}
// If the listener is still starting, prefer existing socket files.
for path in candidates where isOwnedSocketFile(
path,
currentUserID: currentUserID,
inspectSocketPathEntry: inspectSocketPathEntry
) {
return path
}
return candidates.first ?? requestedPath
candidatePaths: candidates,
selectedPath: selectedPath
)
}
private static func candidatePaths(
requestedPath: String,
environment: [String: String],
bundleIdentifier: String?
) -> [String] {
private func candidatePaths(requestedPath: String) -> [String] {
var candidates: [String] = []
let variant = SocketPathMarkerFiles.variant(bundleIdentifier: bundleIdentifier, environment: environment)
let defaultPath = defaultSocketPath(bundleIdentifier: bundleIdentifier, environment: environment)
let ownDefaultPath = resolvedDefaultSocketPath()
candidates.append(defaultPath)
if let last = readLastSocketPath(bundleIdentifier: bundleIdentifier, environment: environment) {
candidates.append(last)
}
// Keep the current variant first. For a tagged debug CLI this is the
// tag-specific socket; for the stable CLI it is the primary stable socket.
candidates.append(ownDefaultPath)
// A dead dev socket must not strand ambient commands. The stable primary
// socket is the deterministic machine-wide fallback before any marker.
candidates.append(resolvedStableDefaultSocketPath)
// Markers are an ordered list, not a single pointer: the state-directory
// marker and its legacy /tmp mirror can disagree after a reload.
candidates.append(contentsOf: readLastSocketPaths(
bundleIdentifier: bundleIdentifier,
environment: environment
))
// A dev process may be the last writer for its own marker while the
// stable app's marker still names a user-scoped stable listener. Walk
// those markers too, in deterministic order, rather than treating one
// variant's file as the entire discovery state.
candidates.append(contentsOf: readLastSocketPaths(
bundleIdentifier: SocketPathMarkerFiles.stableBundleIdentifier,
environment: [:]
))
// Preserve legacy/user-scoped stable aliases after the primary and marker
// candidates. They remain useful on machines migrating from older releases.
candidates.append(contentsOf: implicitFallbackCandidatePaths(for: variant))
// A caller that supplies a non-default implicit path still gets that path
// tried, but it never displaces the current variant's own socket.
if shouldIncludeImplicitRequestedPath(
requestedPath,
defaultPath: defaultPath,
defaultPath: ownDefaultPath,
variant: variant
) {
candidates.append(requestedPath)
}
candidates.append(contentsOf: implicitFallbackCandidatePaths(for: variant))
if shouldDiscoverTaggedSockets(
variant: variant,
bundleIdentifier: bundleIdentifier,
environment: environment
) {
candidates.append(contentsOf: discoverTaggedSockets(limit: 12))
}
return candidates
}
private static func shouldIncludeImplicitRequestedPath(
private func shouldIncludeImplicitRequestedPath(
_ requestedPath: String,
defaultPath: String,
variant: SocketPathVariant
@@ -221,91 +308,43 @@ enum CLISocketPathResolver {
case .stable:
return true
case .nightly, .staging, .dev:
return pathsMatch(requestedPath, defaultPath)
|| !containsPath(stableImplicitDefaultPaths(), requestedPath)
return Self.pathsMatch(requestedPath, defaultPath)
|| !Self.containsPath(resolvedStableImplicitDefaultPaths(), requestedPath)
}
}
private static func implicitFallbackCandidatePaths(for variant: SocketPathVariant) -> [String] {
private func implicitFallbackCandidatePaths(for variant: SocketPathVariant) -> [String] {
switch variant {
case .stable:
return stableImplicitDefaultPaths()
case .nightly, .staging, .dev:
return []
case .stable, .nightly, .staging, .dev:
return resolvedStableImplicitDefaultPaths()
}
}
private static func shouldDiscoverTaggedSockets(
variant: SocketPathVariant,
private func resolvedStableImplicitDefaultPaths() -> [String] {
Self.dedupe([
resolvedStableDefaultSocketPath,
Self.legacyDefaultSocketPath,
stateDirectory.appendingPathComponent("cmux-\(currentUserID).sock", isDirectory: false).path,
Self.legacyUserScopedStableSocketPath(currentUserID: currentUserID),
])
}
private func readLastSocketPaths(
bundleIdentifier: String?,
environment: [String: String]
) -> Bool {
switch variant {
case .dev(slug: nil):
return true
case .dev(slug: .some):
let bundleId = bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return bundleId == SocketPathMarkerFiles.defaultBaseDebugBundleIdentifier
&& normalized(environment["CMUX_TAG"]) != nil
case .stable, .nightly, .staging:
return false
}
}
private static func readLastSocketPath(
bundleIdentifier: String?,
environment: [String: String]
) -> String? {
) -> [String] {
let candidates = lastSocketPathFiles(bundleIdentifier: bundleIdentifier, environment: environment)
var values: [String] = []
for candidate in candidates {
guard let data = try? String(contentsOfFile: candidate, encoding: .utf8) else {
continue
}
if let value = normalized(data) {
return value
guard let contents = boundedMarkerContents(at: candidate) else { continue }
if let value = Self.normalized(contents) {
values.append(value)
}
}
return nil
return values
}
private static func discoverTaggedSockets(limit: Int) -> [String] {
var discovered: [(path: String, mtime: TimeInterval)] = []
for directory in socketDiscoveryDirectories() {
guard let entries = try? FileManager.default.contentsOfDirectory(atPath: directory) else {
continue
}
discovered.reserveCapacity(min(limit, discovered.count + entries.count))
for name in entries where name.hasPrefix("cmux-debug-") && name.hasSuffix(".sock") {
let path = URL(fileURLWithPath: directory)
.appendingPathComponent(name, isDirectory: false)
.path
var st = stat()
guard lstat(path, &st) == 0 else { continue }
guard (st.st_mode & mode_t(S_IFMT)) == mode_t(S_IFSOCK) else { continue }
if isKnownDefaultSocketPath(path) {
continue
}
let modified = TimeInterval(st.st_mtimespec.tv_sec) + TimeInterval(st.st_mtimespec.tv_nsec) / 1_000_000_000
discovered.append((path: path, mtime: modified))
}
}
discovered.sort { $0.mtime > $1.mtime }
return dedupe(discovered.prefix(limit).map(\.path))
}
private static func isSocketFile(_ path: String) -> Bool {
if case .socket = inspectSocketPathEntry(path) {
return true
}
return false
}
private static func isOwnedSocketFile(
_ path: String,
currentUserID: uid_t,
inspectSocketPathEntry: (String) -> SocketPathEntry
) -> Bool {
private func isOwnedSocketFile(_ path: String) -> Bool {
if case .socket(let ownerUserID) = inspectSocketPathEntry(path) {
return ownerUserID == currentUserID
}
@@ -326,18 +365,14 @@ enum CLISocketPathResolver {
return .other(ownerUserID: st.st_uid)
}
private static func canConnect(
to path: String,
currentUserID: uid_t,
inspectSocketPathEntry: (String) -> SocketPathEntry
) -> Bool {
guard isOwnedSocketFile(
path,
currentUserID: currentUserID,
inspectSocketPathEntry: inspectSocketPathEntry
) else {
private func canConnect(to path: String) -> Bool {
guard isOwnedSocketFile(path) else {
return false
}
return socketAcceptsConnections(path)
}
private static func socketAcceptsConnections(_ path: String) -> Bool {
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
guard fd >= 0 else { return false }
defer { Darwin.close(fd) }
@@ -349,6 +384,9 @@ enum CLISocketPathResolver {
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
let maxLength = MemoryLayout.size(ofValue: addr.sun_path)
guard path.utf8.count < maxLength else {
return false
}
path.withCString { ptr in
withUnsafeMutablePointer(to: &addr.sun_path) { pathPtr in
let buf = UnsafeMutableRawPointer(pathPtr).assumingMemoryBound(to: CChar.self)
@@ -387,6 +425,30 @@ enum CLISocketPathResolver {
return optionResult == 0 && socketError == 0
}
/// Reads at most one short socket marker without accepting unbounded input.
private func boundedMarkerContents(at path: String) -> String? {
var info = stat()
guard lstat(path, &info) == 0,
(info.st_mode & mode_t(S_IFMT)) == mode_t(S_IFREG),
info.st_uid == currentUserID,
info.st_nlink == 1,
info.st_size >= 0,
info.st_size <= off_t(SocketPathMarkerStore.maximumMarkerBytes)
else {
return nil
}
let url = URL(fileURLWithPath: path, isDirectory: false)
guard let handle = try? FileHandle(forReadingFrom: url) else { return nil }
defer { try? handle.close() }
guard let data = try? handle.read(upToCount: SocketPathMarkerStore.maximumMarkerBytes + 1),
data.count <= SocketPathMarkerStore.maximumMarkerBytes
else {
return nil
}
return String(data: data, encoding: .utf8)
}
private static func knownImplicitDefaultPaths(
bundleIdentifier: String?,
environment: [String: String]
@@ -410,22 +472,6 @@ enum CLISocketPathResolver {
])
}
private static func allKnownDefaultSocketPaths() -> Set<String> {
Set(dedupe([
stableDefaultSocketPath,
legacyDefaultSocketPath,
userScopedStableSocketPath(),
legacyUserScopedStableSocketPath(),
fallbackSocketPath,
nightlySocketPath,
stagingSocketPath,
]))
}
private static func isKnownDefaultSocketPath(_ path: String) -> Bool {
containsPath(Array(allKnownDefaultSocketPaths()), path)
}
private static func containsPath(_ paths: [String], _ path: String) -> Bool {
paths.contains { pathsMatch($0, path) }
}
@@ -441,6 +487,12 @@ enum CLISocketPathResolver {
}
}
/// Keeps diagnostic value semantics available to the result type without exposing
/// the resolver's path-normalization implementation as public API.
fileprivate static func pathsMatchForDiagnostics(_ lhs: String, _ rhs: String) -> Bool {
pathsMatch(lhs, rhs)
}
private static func pathComparisonForms(_ path: String) -> [String] {
let baseForms = [
(path as NSString).standardizingPath,
@@ -457,14 +509,14 @@ enum CLISocketPathResolver {
return dedupe(forms)
}
private static func lastSocketPathFiles(
private func lastSocketPathFiles(
bundleIdentifier: String?,
environment: [String: String]
) -> [String] {
SocketPathMarkerFiles.paths(
bundleIdentifier: bundleIdentifier,
environment: environment,
directory: stableSocketDirectoryURL()
directory: stateDirectory
)
}
@@ -482,9 +534,9 @@ enum CLISocketPathResolver {
}
#if DEBUG
return "com.cmuxterm.app.debug"
return SocketPathMarkerFiles.defaultBaseDebugBundleIdentifier
#else
return "com.cmuxterm.app"
return SocketPathMarkerFiles.stableBundleIdentifier
#endif
}
@@ -505,14 +557,6 @@ enum CLISocketPathResolver {
CmuxStateDirectory.url(homeDirectory: FileManager.default.homeDirectoryForCurrentUser)
}
private static func socketDiscoveryDirectories() -> [String] {
let stateSocketDirectory: String = stableSocketDirectoryURL()?.path ?? ""
return dedupe([
"/tmp",
stateSocketDirectory,
])
}
private static func dedupe(_ paths: [String]) -> [String] {
var seen: Set<String> = []
var ordered: [String] = []
+3
View File
@@ -39,6 +39,7 @@ extension CMUXCLI {
.init(agentEvent: "Notification", cmuxSubcommand: "notification"),
.init(agentEvent: "SessionEnd", cmuxSubcommand: "session-end"),
],
dispatch: .pinned(marker: "cmux-grok-hook-v2"),
publishesStopNotification: false,
sessionEndIsTurnBoundary: true,
feedHookEvents: ["PreToolUse"]
@@ -142,6 +143,7 @@ extension CMUXCLI {
.init(agentEvent: "SessionEnd", cmuxSubcommand: "session-end"),
],
aliases: ["agy"],
dispatch: .pinned(marker: "cmux-antigravity-hook-v2"),
sessionEndIsTurnBoundary: true
),
AgentHookDef(
@@ -159,6 +161,7 @@ extension CMUXCLI {
AgentHookDef(
name: "hermes-agent", displayName: "Hermes Agent", statusKey: "hermes-agent",
configDir: ".hermes", configFile: "config.yaml", configDirEnvOverride: "HERMES_HOME",
createConfigDirIfMissing: true,
binaryName: "hermes",
sessionStoreSuffix: "hermes-agent", disableEnvVar: "CMUX_HERMES_AGENT_HOOKS_DISABLED",
hookMarker: "cmux hooks hermes-agent", format: .hermesAgentYAML,
+44 -14
View File
@@ -26,6 +26,12 @@ extension CMUXCLI {
let format: HookFormat
let events: [HookEvent]
let aliases: Set<String>
/// How installed hooks find the cmux instance that owns them.
///
/// `.ambient` is appropriate when an agent preserves the launch environment. `.pinned`
/// embeds the installing CLI and socket, which is required for agents that sanitize hook
/// subprocess environments and keeps callbacks attributed to the correct tagged app.
let dispatch: HookDispatch
let publishesStopNotification: Bool
/// Whether this agent's `SessionEnd`/`session-end` hook fires once per
/// conversation turn rather than at a true session teardown.
@@ -62,6 +68,11 @@ extension CMUXCLI {
case tomlArrayTable // ~/.kimi/config.toml [[hooks]] array-of-tables
}
enum HookDispatch {
case ambient
case pinned(marker: String)
}
struct HookEvent {
let agentEvent: String
let cmuxSubcommand: String
@@ -109,6 +120,7 @@ extension CMUXCLI {
sessionStoreSuffix: String, disableEnvVar: String, hookMarker: String,
format: HookFormat, events: [HookEvent],
aliases: Set<String> = [],
dispatch: HookDispatch = .ambient,
publishesStopNotification: Bool = true,
sessionEndIsTurnBoundary: Bool = false,
feedHookEvents: [String] = [],
@@ -123,6 +135,7 @@ extension CMUXCLI {
self.binaryName = binaryName ?? name
self.sessionStoreSuffix = sessionStoreSuffix; self.disableEnvVar = disableEnvVar
self.hookMarker = hookMarker; self.format = format; self.events = events
self.dispatch = dispatch
self.publishesStopNotification = publishesStopNotification
self.sessionEndIsTurnBoundary = sessionEndIsTurnBoundary
self.aliases = Set(aliases.compactMap { alias in
@@ -188,6 +201,20 @@ extension CMUXCLI {
}
static func feedHookCommandString(for def: AgentHookDef, agentEvent: String) -> String {
if def.name == "codex",
let injectedEvent = CodexHookInjectionSchema.current.events.first(where: {
$0.agentEvent == agentEvent
}) {
let inline = codexFireAndForgetAgentHookShellCommand(
"cmux hooks codex \(injectedEvent.cmuxSubcommand)",
for: def
)
return codexPersistentHookScriptCommand(
inline,
eventTag: "feed-\(agentEvent)"
)
}
let inline: String
let noOpCommand = feedHookNoOpShellCommand(for: def, agentEvent: agentEvent)
switch def.format {
@@ -232,15 +259,12 @@ extension CMUXCLI {
return "{ \(command); }"
}
private static let grokPinnedHookMarker = "cmux-grok-hook-v2"
private static let antigravityPinnedHookMarker = "cmux-antigravity-hook-v2"
private static func agentHookShellCommand(
_ command: String,
for def: AgentHookDef,
noOpCommand: String = "echo '{}'"
) -> String {
if usesPinnedHookDispatch(def) {
if case .pinned = def.dispatch {
return pinnedAgentHookShellCommand(command, for: def, noOpCommand: noOpCommand)
}
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
@@ -258,19 +282,14 @@ extension CMUXCLI {
return "cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$\(def.disableEnvVar)\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" \(routedArguments); else \"$cmux_cli\" \(routedArguments); fi; status=$?; if [ \"$status\" -eq 2 ]; then exit 2; fi; if [ \"$status\" -ne 0 ]; then \(noOpSnippet); fi; else \(noOpSnippet); fi"
}
private static func usesPinnedHookDispatch(_ def: AgentHookDef) -> Bool {
def.name == "grok" || def.name == "antigravity"
}
private static func pinnedHookMarker(for def: AgentHookDef) -> String {
def.name == "antigravity" ? antigravityPinnedHookMarker : grokPinnedHookMarker
}
private static func pinnedAgentHookShellCommand(
_ command: String,
for def: AgentHookDef,
noOpCommand: String = "echo '{}'"
) -> String {
guard case .pinned(let marker) = def.dispatch else {
return agentHookShellCommand(command, for: def, noOpCommand: noOpCommand)
}
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
let socketPath = pinnedAgentHookSocketPath()
let noOpSnippet = shellNoOpSnippet(noOpCommand)
@@ -310,7 +329,7 @@ extension CMUXCLI {
} else {
dispatch = "command -v cmux >/dev/null 2>&1 && \(fallbackInvocation) || \(noOpSnippet)"
}
return ": \(pinnedHookMarker(for: def)); \(shellTraceStart); printenv \(def.disableEnvVar) | grep -qx 1 && { \(shellTraceDisabled); \(noOpSnippet); } || { \(dispatch); cmux_hook_status=$?; \(shellTraceExit); exit $cmux_hook_status; }"
return ": \(marker); \(shellTraceStart); printenv \(def.disableEnvVar) | grep -qx 1 && { \(shellTraceDisabled); \(noOpSnippet); } || { \(dispatch); cmux_hook_status=$?; \(shellTraceExit); exit $cmux_hook_status; }"
}
private static func pinnedHookInvocation(
@@ -375,6 +394,17 @@ extension CMUXCLI {
return "/tmp/cmux-debug-\(slug).sock"
}
static func validateHookInstallDispatch(for def: AgentHookDef) throws {
guard case .pinned = def.dispatch,
pinnedAgentHookSocketPath() == nil else {
return
}
throw CLIError(message: String(
localized: "cli.hooks.error.pinnedTargetMissing",
defaultValue: "cmux could not connect this hook installation to a running app. Open a cmux workspace and run this command again."
))
}
private static func pinnedHookShellTraceCommand(
agentName: String,
phase: String,
@@ -420,7 +450,7 @@ extension CMUXCLI {
}
static func isCmuxOwnedHookCommand(_ command: String, for def: AgentHookDef, includeLegacy: Bool = true) -> Bool {
if usesPinnedHookDispatch(def), command.contains(pinnedHookMarker(for: def)) {
if case .pinned(let marker) = def.dispatch, command.contains(marker) {
return true
}
if def.name == "codex", isCmuxOwnedCodexHookScriptCommand(command) {
+20
View File
@@ -141,6 +141,26 @@ struct AutoNamingEnvironmentPolicy: Sendable {
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return override.isEmpty ? "haiku" : override
}
/// Inline MCP configuration passed with `--strict-mcp-config` so the
/// summarizer starts no MCP servers. Claude Code validates this JSON
/// against a schema requiring an `mcpServers` record, so a bare `{}` is
/// rejected during argument parsing and the subprocess exits before it
/// can produce a title (cmux#9457).
static let emptyMCPConfigJSON = #"{"mcpServers":{}}"#
/// Argument vector for the tool-disabled `claude -p` summarizer call.
func claudeSummarizerArguments(from env: [String: String]) -> [String] {
[
"-p",
"--model", claudeModel(from: env),
"--tools", "",
"--disable-slash-commands",
"--no-session-persistence",
"--strict-mcp-config",
"--mcp-config", Self.emptyMCPConfigJSON
]
}
}
/// Pure auto-naming logic: throttle decisions, transcript extraction,
+1 -9
View File
@@ -123,15 +123,7 @@ extension CMUXCLI {
guard let executable else { return nil }
return runAutoNamingSummarizer(
executable: executable,
arguments: [
"-p",
"--model", policy.claudeModel(from: env),
"--tools", "",
"--disable-slash-commands",
"--no-session-persistence",
"--strict-mcp-config",
"--mcp-config", "{}"
],
arguments: policy.claudeSummarizerArguments(from: env),
prompt: prompt,
environment: policy.summarizerEnvironment(from: env),
timeout: timeout
+12 -2
View File
@@ -76,15 +76,25 @@ extension CMUXCLI {
client: SocketClient,
includeAmbientTTY: Bool = true
) -> CallerTerminalBinding? {
guard let ttyName = resolveCallerTTYName(includeAmbientTTY: includeAmbientTTY),
let payload = try? client.sendV2(method: "debug.terminals") else {
guard let ttyName = resolveCallerTTYName(includeAmbientTTY: includeAmbientTTY) else {
return nil
}
return uniqueCallerTerminalBindingByTTY(ttyName: ttyName, client: client)
}
func uniqueCallerTerminalBindingByTTY(
ttyName: String,
client: SocketClient,
workspaceId: String? = nil
) -> CallerTerminalBinding? {
guard let payload = try? client.sendV2(method: "debug.terminals") else { return nil }
let terminals = payload["terminals"] as? [[String: Any]] ?? []
let scopedWorkspaceId = normalizedHandleValue(workspaceId)
var matched: [CallerTerminalBinding] = []
for terminal in terminals {
guard normalizedTTYName(terminal["tty"] as? String) == ttyName,
let workspaceId = normalizedHandleValue(terminal["workspace_id"] as? String),
scopedWorkspaceId == nil || workspaceId == scopedWorkspaceId,
let surfaceId = normalizedHandleValue(terminal["surface_id"] as? String) else {
continue
}
+107 -4
View File
@@ -4,17 +4,22 @@ import Foundation
extension CMUXCLI {
/// Emit, NUL-separated to stdout, the exact codex arg list the wrapper must
/// splice ahead of the user's args to enable + inject cmux's fire-and-forget
/// hooks for one codex invocation. Returns the arg list:
/// hooks for one codex invocation when no persistent cmux channel is
/// installed. Returns the arg list:
/// --enable\0hooks\0--dangerously-bypass-hook-trust\0
/// -c\0hooks.SessionStart=[{hooks=[{type="command",command='''<ff>''',timeout=10000}]}]\0
/// -c\0hooks.UserPromptSubmit=...\0 ... (one `-c` pair per event)
/// where `<ff>` is `codexFireAndForgetAgentHookShellCommand(...)` so each
/// hook returns `{}` to codex instantly and backgrounds the real cmux call.
/// Requires no live socket: pure string construction from the agent def.
/// Before emission, an existing cmux-owned persistent hook channel is
/// reconciled in place and supersedes wrapper injection for this launch.
/// No live socket is required.
func emitCodexWrapperInjectArgs() throws {
guard let codexDef = Self.agentDef(named: "codex") else {
throw CLIError(message: "Codex hook integration is unavailable.")
}
let usesPersistentChannel = reconcileCodexPersistentHooksForWrapper()
let eventsToInject = usesPersistentChannel ? [] : CodexHookInjectionSchema.current.events
// Prefer a #!/bin/sh SCRIPT FILE as the hook command over an inline shell
// snippet. Some codex-compatible runtimes (subrouters, proxies) exec the
// `command` string directly as a program instead of via a shell, so an
@@ -26,8 +31,15 @@ extension CMUXCLI {
// hooks), not the user's ~/.codex. Any write failure falls back to the
// inline snippet so the working path can never regress.
let hooksDir = Self.codexHookScriptsDirectory()
defer {
Self.garbageCollectCodexHookScripts(
retaining: Self.currentCodexWrapperHookScriptFilenames(for: codexDef)
.union(Self.installedCodexHookScriptFilenames(for: codexDef))
)
}
guard !eventsToInject.isEmpty else { return }
var args: [String] = ["--enable", "hooks", "--dangerously-bypass-hook-trust"]
for event in CodexHookInjectionSchema.current.events {
for event in eventsToInject {
let ff = Self.codexFireAndForgetAgentHookShellCommand(
"cmux hooks codex \(event.cmuxSubcommand)", for: codexDef
)
@@ -114,9 +126,100 @@ extension CMUXCLI {
}
}
/// Names that the current wrapper schema may reference from a live session.
static func currentCodexWrapperHookScriptFilenames(for def: AgentHookDef) -> Set<String> {
Set(CodexHookInjectionSchema.current.events.compactMap { event in
let body = codexFireAndForgetAgentHookShellCommand(
"cmux hooks codex \(event.cmuxSubcommand)",
for: def
)
return CodexHookScriptName(
contents: "#!/bin/sh\n\(body)\n",
subcommand: event.cmuxSubcommand
)?.filename
})
}
/// Cmux-generated script names referenced by the active persistent config.
static func installedCodexHookScriptFilenames(for def: AgentHookDef) -> Set<String> {
let fileURL = URL(fileURLWithPath: def.resolvedConfigDir(), isDirectory: true)
.appendingPathComponent(def.configFile, isDirectory: false)
guard let data = try? Data(contentsOf: fileURL),
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let hooks = root["hooks"] as? [String: Any],
let hooksDirectory = codexHookScriptsDirectory()?.standardizedFileURL else {
return []
}
var filenames = Set<String>()
for value in hooks.values {
guard let groups = value as? [[String: Any]] else { continue }
for group in groups {
guard let handlers = group["hooks"] as? [[String: Any]] else { continue }
for handler in handlers {
guard let command = handler["command"] as? String else { continue }
let url = URL(fileURLWithPath: command, isDirectory: false)
guard url.deletingLastPathComponent().standardizedFileURL == hooksDirectory,
CodexHookScriptName(filename: url.lastPathComponent) != nil else {
continue
}
filenames.insert(url.lastPathComponent)
}
}
}
return filenames
}
/// Removes obsolete regular files only when their names prove cmux ownership.
/// Live Codex sessions may still hold paths from another tagged build, and
/// concurrent launches can briefly overlap script generation, so collection
/// waits until no Codex process is running and leaves recent files alone.
static func garbageCollectCodexHookScripts(retaining filenames: Set<String>) {
guard !hasRunningCodexProcess(),
let directory = codexHookScriptsDirectory(),
let contents = try? FileManager.default.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: [.contentModificationDateKey, .isRegularFileKey],
options: [.skipsHiddenFiles]
) else {
return
}
let newestRemovableDate = Date().addingTimeInterval(-60)
for url in contents where !filenames.contains(url.lastPathComponent) {
let values = try? url.resourceValues(forKeys: [
.contentModificationDateKey,
.isRegularFileKey,
])
guard CodexHookScriptName(filename: url.lastPathComponent) != nil,
values?.isRegularFile == true,
let modificationDate = values?.contentModificationDate,
modificationDate < newestRemovableDate else {
continue
}
try? FileManager.default.removeItem(at: url)
}
}
/// Conservatively detects sessions that may still reference an older hook generation.
private static func hasRunningCodexProcess() -> Bool {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/pgrep")
process.arguments = ["-x", "codex"]
process.standardOutput = FileHandle.nullDevice
process.standardError = FileHandle.nullDevice
do {
try process.run()
process.waitUntilExit()
return process.terminationStatus == 0
} catch {
return true
}
}
static func codexFireAndForgetAgentHookShellCommand(_ command: String, for def: AgentHookDef) -> String {
let routedArguments = command.hasPrefix("cmux ") ? String(command.dropFirst("cmux ".count)) : command
let runner = "payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( sleep 30; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\""
let runner = "payload=\"$1\"; shift; \"$@\" <\"$payload\" >/dev/null 2>&1 & child=\"$!\"; ( timer=; trap \"kill \\$timer 2>/dev/null || true; wait \\$timer 2>/dev/null || true; exit 0\" HUP INT TERM; sleep 30 & timer=\"$!\"; wait \"$timer\" 2>/dev/null || true; timer=; kill \"$child\" 2>/dev/null || true ) & watchdog=\"$!\"; wait \"$child\" 2>/dev/null || true; kill \"$watchdog\" 2>/dev/null || true; wait \"$watchdog\" 2>/dev/null || true; rm -f \"$payload\""
let noOp = stdinDrainingHookNoOpShellCommand
return [
"cmux_cli=\"${CMUX_BUNDLED_CLI_PATH:-}\"",
@@ -0,0 +1,82 @@
import Foundation
import CMUXAgentLaunch
import OSLog
nonisolated private let codexResumeBindingLogger = Logger(
subsystem: "com.cmuxterm.cli",
category: "CodexResumeBinding"
)
extension CMUXCLI {
/// Verifies a hook checkpoint before the standalone CLI publishes it.
///
/// Hook publication is intentionally decided in this short-lived CLI
/// process, not on cmux's MainActor or socket worker. The verifier starts
/// with one indexed SQLite lookup and applies hard byte, line, and fallback
/// candidate limits to the legacy rollout path, so the bounded inspection
/// cannot turn an app UI/socket lane into a history loader.
func codexResumeBindingVerification(
sessionId: String,
transcriptPath: String?,
launchCommand: AgentHookLaunchCommandRecord?
) -> CodexSessionResumeVerification {
let environment = ProcessInfo.processInfo.environment
let codexHome = codexResumeBindingEffectiveHome(
launchEnvironment: launchCommand?.environment,
launchVerificationHome: launchCommand?.verificationHome,
ambientEnvironment: environment
)
return CodexSessionResumeVerifier().verify(
sessionId: sessionId,
transcriptPath: transcriptPath,
codexHome: codexHome
)
}
func codexResumeBindingEffectiveHome(
launchEnvironment: [String: String]?,
launchVerificationHome: String? = nil,
ambientEnvironment: [String: String] = ProcessInfo.processInfo.environment
) -> String {
if let launchHome = normalizedHookValue(launchEnvironment?["CODEX_HOME"]) {
return (launchHome as NSString).expandingTildeInPath
}
if let launchHome = normalizedHookValue(launchVerificationHome)
?? normalizedHookValue(launchEnvironment?["HOME"]) {
return URL(fileURLWithPath: (launchHome as NSString).expandingTildeInPath, isDirectory: true)
.appendingPathComponent(".codex", isDirectory: true)
.path
}
if let ambientHome = normalizedHookValue(ambientEnvironment["CODEX_HOME"]) {
return (ambientHome as NSString).expandingTildeInPath
}
let home = normalizedHookValue(ambientEnvironment["HOME"]) ?? NSHomeDirectory()
return URL(fileURLWithPath: home, isDirectory: true)
.appendingPathComponent(".codex", isDirectory: true)
.path
}
func logCodexResumeBindingRejection(
reason: String,
sessionId: String,
incoming: AgentResumeEvidenceProvenance?,
existing: AgentResumeEvidenceProvenance?,
telemetry: CLISocketSentryTelemetry?
) {
let shortSessionID = String(sessionId.prefix(12))
let incomingValue = incoming?.logValue ?? "none"
let existingValue = existing?.logValue ?? "none"
codexResumeBindingLogger.notice(
"Codex resume binding publish rejected reason=\(reason, privacy: .public) session=\(shortSessionID, privacy: .private(mask: .hash)) incoming=\(incomingValue, privacy: .public) existing=\(existingValue, privacy: .public)"
)
telemetry?.breadcrumb(
"codex-resume-binding.publish-rejected",
data: [
"reason": reason,
"incoming_provenance": incomingValue,
"existing_provenance": existingValue,
"has_session_id": !sessionId.isEmpty,
]
)
}
}
+1
View File
@@ -81,6 +81,7 @@ extension CMUXCLI {
"codex",
"codex-hook",
"codex-teams",
"comments",
"config",
"copy-mode",
"current-window",
+181
View File
@@ -0,0 +1,181 @@
import Foundation
/// `cmux comments` read-only access to diff-viewer review comments.
///
/// Strings resolve through `CMUXDiffViewerLocalization`, which reads the enclosing
/// app bundle: the CLI executable carries no string catalog of its own, so
/// `String(localized:)` here would always fall back to its default value.
extension CMUXCLI {
static let commentsUsage = CMUXDiffViewerLocalization.string(
"cli.comments.usage",
defaultValue: """
Usage: cmux comments <subcommand> [options]
Review comments saved from the diff viewer, stored per git repository.
Subcommands:
list [--repo <path>] [--all] [--json]
List review comments for a repository (default: the git repository
containing the current directory). Lists pending comments only;
--all includes comments already delivered to an agent through a
TextBox submission.
"""
)
/// Runs `cmux comments <subcommand>`; `list` is the only subcommand today.
/// Rejects anything unrecognized before it resolves a repository or calls the socket.
func runCommentsNamespace(
commandArgs: [String],
client: SocketClient,
jsonOutput: Bool,
idFormat: CLIIDFormat
) throws {
if hasHelpRequest(beforeSeparator: commandArgs) {
print(Self.commentsUsage)
return
}
guard let sub = commandArgs.first?.lowercased() else {
throw CLIError(message: CMUXDiffViewerLocalization.string(
"cli.comments.error.subcommandRequired",
defaultValue: "comments requires a subcommand. Try: list"
))
}
let rest = Array(commandArgs.dropFirst())
switch sub {
case "list", "ls":
let (repoOption, remainder) = parseOption(rest, name: "--repo")
// `parseOption` takes the next token verbatim, so `--repo --all`
// would resolve a repository named "--all". A path that starts with
// a dash can still be passed as `./-name`.
if let repoOption, repoOption.hasPrefix("--") {
throw CLIError(message: CMUXDiffViewerLocalization.string(
"cli.comments.error.repoRequiresPath",
defaultValue: "--repo requires a path. For a path starting with a dash, pass it as ./-name"
))
}
// Fail closed on anything unrecognized: neither a typo like `--al`
// nor a stray positional may read as a supported request.
if let unexpected = remainder.first(where: { $0 != "--all" }) {
throw CLIError(message: String.localizedStringWithFormat(
CMUXDiffViewerLocalization.string(
"cli.comments.error.unexpectedArgument",
defaultValue: "Unexpected argument '%@' for cmux comments list. Supported: --repo <path>, --all, --json"
),
unexpected
))
}
let includeConsumed = remainder.contains("--all")
let startPath = repoOption ?? FileManager.default.currentDirectoryPath
var params: [String: Any] = ["repo_root": try commentsGitRepoRoot(startingAt: startPath)]
if includeConsumed {
params["include_consumed"] = true
}
let payload = try client.sendV2(method: "comments.list", params: params)
printCommentsListPayload(payload, jsonOutput: jsonOutput, idFormat: idFormat)
default:
throw CLIError(message: String.localizedStringWithFormat(
CMUXDiffViewerLocalization.string(
"cli.comments.error.unknownSubcommand",
defaultValue: "Unknown comments subcommand '%@'. Try: list"
),
sub
))
}
}
/// Resolves the git top level for `--repo` (or the current directory), so the
/// socket receives the same canonical root the store is keyed by.
private func commentsGitRepoRoot(startingAt directory: String) throws -> String {
let result = CLIProcessRunner.runProcess(
executablePath: "/usr/bin/env",
arguments: ["git", "-C", directory, "rev-parse", "--show-toplevel"],
timeout: 10
)
let root = result.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
guard !result.timedOut, result.status == 0, !root.isEmpty else {
throw CLIError(message: String.localizedStringWithFormat(
CMUXDiffViewerLocalization.string(
"cli.comments.error.notARepository",
defaultValue: "cmux comments requires a git repository: %@"
),
directory
))
}
return root
}
/// Builds the count line.
///
/// Selection stays here rather than in catalog plural variations: the count is
/// resolved before the string is, so a `variations.plural` entry could not see
/// it. The catalog's non-singular values therefore avoid numeral-governed
/// nouns, keeping one form grammatical for every count above one in Slavic and
/// Arabic locales.
private func commentsListHeaderText(count: Int, repoRoot: String) -> String {
if count == 1 {
return String.localizedStringWithFormat(
CMUXDiffViewerLocalization.string(
"cli.comments.list.header.one",
defaultValue: "1 review comment (repo: %@)"
),
repoRoot
)
}
return String.localizedStringWithFormat(
CMUXDiffViewerLocalization.string(
"cli.comments.list.header.other",
defaultValue: "%1$lld review comments (repo: %2$@)"
),
Int64(count),
repoRoot
)
}
/// Renders a `comments.list` reply: raw JSON when `--json` is set, otherwise one
/// line per comment with its anchor text and message.
private func printCommentsListPayload(
_ payload: [String: Any],
jsonOutput: Bool,
idFormat: CLIIDFormat
) {
if jsonOutput {
print(jsonString(formatIDs(payload, mode: idFormat)))
return
}
let comments = payload["comments"] as? [[String: Any]] ?? []
let repoRoot = payload["repo_root"] as? String ?? ""
guard !comments.isEmpty else {
print(String.localizedStringWithFormat(
CMUXDiffViewerLocalization.string(
"cli.comments.list.empty",
defaultValue: "No review comments. (repo: %@)"
),
repoRoot
))
return
}
print(commentsListHeaderText(count: comments.count, repoRoot: repoRoot))
for comment in comments {
let filePath = comment["filePath"] as? String ?? "?"
let startLine = intFromAny(comment["startLine"]) ?? 0
let endLine = intFromAny(comment["endLine"]) ?? startLine
let range = endLine > startLine ? "\(startLine)-\(endLine)" : "\(startLine)"
let state = comment["consumedAt"] == nil
? CMUXDiffViewerLocalization.string("cli.comments.list.statePending", defaultValue: "pending")
: CMUXDiffViewerLocalization.string("cli.comments.list.stateConsumed", defaultValue: "consumed")
print("- \(filePath):\(range) [\(state)]")
if let lineText = comment["lineText"] as? String, !lineText.isEmpty {
print(String.localizedStringWithFormat(
CMUXDiffViewerLocalization.string(
"cli.comments.list.anchor",
defaultValue: " anchor: %@"
),
lineText
))
}
if let message = comment["message"] as? String, !message.isEmpty {
print(" \(message)")
}
}
}
}
+6 -4
View File
@@ -369,10 +369,10 @@ extension CMUXCLI {
docs Print the same output as `cmux docs settings`.
Targets:
account, app, terminal, sidebar-appearance, custom-sidebars,
automation, browser, browser-import, global-hotkey,
keyboard-shortcuts, shortcuts, workspace-colors, cmux-json,
json, reset
account, app, terminal, networking, sidebar-appearance,
custom-sidebars, automation, browser, browser-import,
global-hotkey, keyboard-shortcuts, shortcuts, workspace-colors,
cmux-json, json, reset
Config file:
\(Self.primarySettingsDisplayPath)
@@ -411,6 +411,8 @@ extension CMUXCLI {
return "automation"
case "browser":
return "browser"
case "networking", "network", "iroh":
return "networking"
case "browser-import", "browserimport", "import-browser-data":
return "browserImport"
case "global-hotkey", "globalhotkey", "hotkey":
+94 -20
View File
@@ -3,6 +3,7 @@ import Darwin
import Foundation
private struct EventStreamLimitReached: Error {}
private struct EventStreamSnapshotCaptured: Error {}
extension CMUXCLI {
private struct EventsCommandOptions {
@@ -12,6 +13,8 @@ extension CMUXCLI {
var categories: [String] = []
var reconnect = false
var limit: Int?
var timeout: TimeInterval?
var snapshotOnly = false
var printAck = true
var printHeartbeats = true
}
@@ -28,15 +31,56 @@ extension CMUXCLI {
var lastSeq = options.afterSeq
var emittedEvents = 0
// The --timeout budget is measured on a MONOTONIC clock so a
// wall-clock change (NTP step, timezone, manual set) can neither
// expire the whole command instantly nor extend it indefinitely.
// The socket layer takes wall-clock Dates, so each blocking call
// derives a fresh short-lived Date from the monotonic remainder;
// a wall jump can then only skew the single wait in flight, never
// the accumulated budget.
let budgetClock = ContinuousClock()
let budgetDeadline = options.timeout.map { budgetClock.now.advanced(by: .seconds($0)) }
func remainingBudget() -> TimeInterval? {
guard let budgetDeadline else { return nil }
let remaining = budgetClock.now.duration(to: budgetDeadline)
let seconds = Double(remaining.components.seconds)
+ Double(remaining.components.attoseconds) / 1e18
return max(0, seconds)
}
func socketDeadline() -> Date? {
remainingBudget().map { Date(timeIntervalSinceNow: $0) }
}
func timeoutError() -> CLIError {
CLIError(message: String(
localized: "cli.events.error.timeout",
defaultValue: "Timed out waiting for a matching event"
))
}
while true {
if let remaining = remainingBudget(), remaining <= 0 {
throw timeoutError()
}
let client = SocketClient(path: socketPath)
do {
try client.connect()
if let connectDeadline = socketDeadline() {
try client.connect(deadline: connectDeadline)
} else {
try client.connect()
}
// Connection setup may have consumed the rest of the budget;
// re-check before starting authentication so it always gets a
// non-negative timeout.
let authRemaining = remainingBudget()
if let authRemaining, authRemaining <= 0 {
throw timeoutError()
}
try authenticateClientIfNeeded(
client,
explicitPassword: explicitPassword,
socketPath: socketPath
socketPath: socketPath,
responseTimeout: authRemaining,
deadline: socketDeadline()
)
var params: [String: Any] = [
@@ -52,7 +96,11 @@ extension CMUXCLI {
params["categories"] = options.categories
}
try client.streamV2(method: "events.stream", params: params) { line in
try client.streamV2(
method: "events.stream",
params: params,
deadline: socketDeadline()
) { line in
guard !line.isEmpty else { return }
let frame = try parseEventStreamFrame(line)
let type = frame["type"] as? String ?? ""
@@ -67,15 +115,17 @@ extension CMUXCLI {
eventSequence = nil
}
if type == "ack", !options.printAck {
return
}
if type == "heartbeat", !options.printHeartbeats {
return
let shouldPrint =
(type != "ack" || options.printAck)
&& (type != "heartbeat" || options.printHeartbeats)
if shouldPrint {
print(line)
fflush(stdout)
}
print(line)
fflush(stdout)
if type == "ack", options.snapshotOnly {
throw EventStreamSnapshotCaptured()
}
if let eventSequence {
if let cursorFile = options.cursorFile {
@@ -88,15 +138,25 @@ extension CMUXCLI {
}
}
}
} catch is EventStreamSnapshotCaptured {
client.close()
return
} catch is EventStreamLimitReached {
client.close()
return
} catch {
client.close()
if let remaining = remainingBudget(), remaining <= 0 {
throw timeoutError()
}
guard options.reconnect, isTransientEventStreamError(error) else {
throw error
}
waitBeforeReconnectingEventStream()
let remaining = remainingBudget() ?? 1
guard remaining > 0 else {
throw timeoutError()
}
waitBeforeReconnectingEventStream(maximumDelay: remaining)
continue
}
}
@@ -133,15 +193,16 @@ extension CMUXCLI {
|| description.contains("timed out")
}
func waitBeforeReconnectingEventStream() {
let deadline = Date(timeIntervalSinceNow: 1.0)
var didFire = false
let timer = Timer(timeInterval: 1.0, repeats: false) { _ in
didFire = true
}
RunLoop.current.add(timer, forMode: .default)
while !didFire, RunLoop.current.run(mode: .default, before: deadline) {}
timer.invalidate()
func waitBeforeReconnectingEventStream(maximumDelay: TimeInterval = 1) {
let delay = min(1, max(0, maximumDelay))
guard delay > 0 else { return }
// This retry path runs on the CLI's synchronous command thread, which
// pumps no run loop: a Timer + RunLoop.run() wait can spin or park
// with `didFire` as its only exit. A bounded thread sleep is the
// deterministic wait; the caller already clamps the delay to the
// command's remaining --timeout budget, and killing the process (the
// CLI's only cancellation) interrupts it.
Thread.sleep(forTimeInterval: delay)
}
private func parseEventsOptions(_ args: [String]) throws -> EventsCommandOptions {
@@ -178,6 +239,19 @@ extension CMUXCLI {
throw CLIError(message: "--limit must be greater than 0")
}
options.limit = limit
case "--timeout":
let raw = try requireValue()
guard let timeout = TimeInterval(raw),
timeout.isFinite,
timeout > 0 else {
throw CLIError(message: String(
localized: "cli.events.error.invalidTimeout",
defaultValue: "--timeout must be greater than 0"
))
}
options.timeout = timeout
case "--snapshot":
options.snapshotOnly = true
case "--no-ack":
options.printAck = false
case "--no-heartbeat", "--no-heartbeats":
+27 -13
View File
@@ -37,16 +37,14 @@ extension CMUXCLI {
return prefix.contains("cmux claude wrapper - injects hooks and session tracking")
}
func isCmuxClaudeCommandShim(at path: String) -> Bool {
func isCmuxAgentCommandShim(at path: String) -> Bool {
let candidate = URL(fileURLWithPath: path, isDirectory: false)
.standardizedFileURL
.path
let environment = ProcessInfo.processInfo.environment
let shimPaths = [
environment["CMUX_CLAUDE_WRAPPER_SHIM"],
]
for shimPath in shimPaths {
guard let shimPath else { continue }
for (key, rawPath) in environment where key.hasSuffix("_WRAPPER_SHIM") {
let shimPath = rawPath.trimmingCharacters(in: .whitespacesAndNewlines)
guard !shimPath.isEmpty else { continue }
let standardizedShim = URL(fileURLWithPath: shimPath, isDirectory: false)
.standardizedFileURL
.path
@@ -55,16 +53,22 @@ extension CMUXCLI {
}
}
let shimRoots: [String?] = [
environment["CMUX_CLAUDE_WRAPPER_SHIM_ROOT"],
var shimRoots = environment.compactMap { key, rawPath -> String? in
guard key == "CMUX_AGENT_COMMAND_SHIM_ROOT" || key.hasSuffix("_WRAPPER_SHIM_ROOT") else {
return nil
}
return rawPath
}
shimRoots.append(contentsOf: [
URL(fileURLWithPath: environment["TMPDIR"] ?? NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent("cmux-cli-shims", isDirectory: true)
.standardizedFileURL
.path,
"/tmp/cmux-cli-shims",
]
])
for shimRoot in shimRoots {
guard let shimRoot else { continue }
let shimRoot = shimRoot.trimmingCharacters(in: .whitespacesAndNewlines)
guard !shimRoot.isEmpty else { continue }
let standardizedRoot = URL(fileURLWithPath: shimRoot, isDirectory: true)
.standardizedFileURL
.path
@@ -85,8 +89,16 @@ extension CMUXCLI {
let candidate = URL(fileURLWithPath: entry, isDirectory: true)
.appendingPathComponent(name, isDirectory: false)
.path
guard FileManager.default.isExecutableFile(atPath: candidate) else { continue }
// `isExecutableFile(atPath:)` is true for directories, so a directory named
// like the provider binary would otherwise shadow the real executable and
// fail at execv (#8743). Reject directories the way the configured-candidate
// path in `resolveClaudeExecutable` already does.
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: candidate, isDirectory: &isDirectory),
!isDirectory.boolValue,
FileManager.default.isExecutableFile(atPath: candidate) else { continue }
guard !isBundledProviderExecutable(at: candidate) else { continue }
guard !isCmuxAgentCommandShim(at: candidate) else { continue }
if let skip, skip(candidate) { continue }
return candidate
}
@@ -102,7 +114,7 @@ extension CMUXCLI {
!isDirectory.boolValue,
FileManager.default.isExecutableFile(atPath: trimmed),
!isBundledProviderExecutable(at: trimmed),
!isCmuxClaudeCommandShim(at: trimmed),
!isCmuxAgentCommandShim(at: trimmed),
!isCmuxClaudeWrapper(at: trimmed) else { continue }
return URL(fileURLWithPath: trimmed, isDirectory: false).standardizedFileURL.path
}
@@ -114,7 +126,7 @@ extension CMUXCLI {
resolveExecutableInSearchPath(
"claude",
searchPath: searchPath,
skip: { self.isCmuxClaudeCommandShim(at: $0) || self.isCmuxClaudeWrapper(at: $0) }
skip: { self.isCmuxClaudeWrapper(at: $0) }
)
}
@@ -295,6 +307,8 @@ extension CMUXCLI {
for key in ClaudeSessionEnvironmentPolicy().inheritedIndependentLaunchKeys {
unsetenv(key)
}
unsetenv(ClaudeTeamsRespawnEnvironmentTransport.environmentKey)
unsetenv("CMUX_CLAUDE_TEAMS_WRAPPER_LAUNCH")
}
private func providerExecutableSearchDirectories(searchPath: String?) -> [String] {
+218 -12
View File
@@ -1,7 +1,191 @@
import Foundation
import CMUXAgentLaunch
import Darwin
import Foundation
extension CMUXCLI {
private static let hermesTUIActiveSessionFilePrefix = "hermes-tui-active-session-"
private static let hermesTUIActiveSessionMaximumBytes: Int64 = 4_096
private func updateHermesAgentAllowlist(
atPath allowlistPath: String,
transform: (Data?) throws -> Data
) throws -> Bool {
let lockPath = "\(allowlistPath).lock"
let descriptor = Darwin.open(
lockPath,
O_RDWR | O_CREAT | O_CLOEXEC,
S_IRUSR | S_IWUSR
)
guard descriptor >= 0 else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
defer { _ = Darwin.close(descriptor) }
var result: Int32
repeat {
result = flock(descriptor, LOCK_EX)
} while result != 0 && errno == EINTR
guard result == 0 else {
throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno))
}
defer { _ = flock(descriptor, LOCK_UN) }
let oldAllowlist = FileManager.default.contents(atPath: allowlistPath)
let newAllowlist = try transform(oldAllowlist)
guard oldAllowlist != newAllowlist else { return false }
try newAllowlist.write(to: URL(fileURLWithPath: allowlistPath), options: .atomic)
return true
}
private func hermesAgentApprovalPayload(
def: AgentHookDef,
input: ClaudeHookParsedInput
) -> (event: String, extra: [String: Any])? {
guard def.name == "hermes-agent",
let object = input.rawObject ?? input.object,
let event = firstString(
in: object,
keys: ["hook_event_name", "hookEventName", "event", "event_name"]
)?.lowercased(),
event == "pre_approval_request" || event == "post_approval_response" else {
return nil
}
return (event, (object["extra"] as? [String: Any]) ?? [:])
}
func hermesAgentApprovalSessionId(
def: AgentHookDef,
input: ClaudeHookParsedInput
) -> String? {
guard let payload = hermesAgentApprovalPayload(def: def, input: input) else { return nil }
return normalizedHookValue(firstString(in: payload.extra, keys: ["session_key", "sessionKey"]))
}
/// Hermes 0.20's TUI approval gateway omits both the top-level session id
/// and `extra.session_key`. The cmux wrapper gives each TUI invocation a
/// private TMPDIR containing the same active-session file consumed by its
/// lifecycle watcher, so missing-id callbacks can still use Hermes's own
/// conversation identity instead of mistaking the cmux surface UUID for it.
func hermesAgentTUIActiveSessionId(
def: AgentHookDef,
env: [String: String]
) -> String? {
guard def.name == "hermes-agent",
env["CMUX_HERMES_TUI_HOOK_BOOTSTRAP"] == "1",
let temporaryDirectory = normalizedHookValue(env["TMPDIR"]),
temporaryDirectory.hasPrefix("/") else {
return nil
}
let directoryURL = URL(
fileURLWithPath: temporaryDirectory,
isDirectory: true
).standardizedFileURL
guard Self.isHermesTUIInvocationDirectoryName(directoryURL.lastPathComponent),
Self.isRegularFileWithoutFollowingSymbolicLinks(
atPath: directoryURL.path,
expectedType: S_IFDIR
),
let names = try? FileManager.default.contentsOfDirectory(atPath: directoryURL.path) else {
return nil
}
let candidates = names.filter {
$0.hasPrefix(Self.hermesTUIActiveSessionFilePrefix) && $0.hasSuffix(".json")
}
guard candidates.count == 1 else { return nil }
let activeSessionURL = directoryURL.appendingPathComponent(
candidates[0],
isDirectory: false
)
guard let data = Self.readHermesTUIActiveSessionFile(atPath: activeSessionURL.path),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let sessionId = normalizedHookValue(object["session_id"] as? String),
Self.isValidHermesTUISessionId(sessionId) else {
return nil
}
return sessionId
}
private static func isHermesTUIInvocationDirectoryName(_ name: String) -> Bool {
let prefix = "cmux-hermes-tui."
guard name.hasPrefix(prefix) else { return false }
let suffix = name.dropFirst(prefix.count)
return suffix.utf8.count == 6 && suffix.utf8.allSatisfy(Self.isASCIIAlphaNumeric)
}
private static func isValidHermesTUISessionId(_ sessionId: String) -> Bool {
let bytes = Array(sessionId.utf8)
guard (1...128).contains(bytes.count),
let first = bytes.first,
isASCIIAlphaNumeric(first) else {
return false
}
return bytes.dropFirst().allSatisfy {
isASCIIAlphaNumeric($0) || $0 == 46 || $0 == 95 || $0 == 58 || $0 == 45
}
}
private static func isASCIIAlphaNumeric(_ byte: UInt8) -> Bool {
(48...57).contains(byte) || (65...90).contains(byte) || (97...122).contains(byte)
}
private static func isRegularFileWithoutFollowingSymbolicLinks(
atPath path: String,
expectedType: mode_t
) -> Bool {
var metadata = stat()
return lstat(path, &metadata) == 0 && (metadata.st_mode & S_IFMT) == expectedType
}
private static func readHermesTUIActiveSessionFile(atPath path: String) -> Data? {
guard isRegularFileWithoutFollowingSymbolicLinks(atPath: path, expectedType: S_IFREG) else {
return nil
}
let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
guard descriptor >= 0 else { return nil }
defer { _ = Darwin.close(descriptor) }
var metadata = stat()
guard fstat(descriptor, &metadata) == 0,
(metadata.st_mode & S_IFMT) == S_IFREG,
metadata.st_size > 0,
metadata.st_size <= hermesTUIActiveSessionMaximumBytes else {
return nil
}
var bytes = [UInt8](repeating: 0, count: Int(metadata.st_size))
var offset = 0
while offset < bytes.count {
let remaining = bytes.count - offset
let count = bytes.withUnsafeMutableBytes { buffer -> Int in
guard let baseAddress = buffer.baseAddress else { return -1 }
return Darwin.read(
descriptor,
baseAddress.advanced(by: offset),
remaining
)
}
if count < 0, errno == EINTR {
continue
}
guard count > 0 else { return nil }
offset += count
}
return Data(bytes)
}
func isHermesAgentAutomaticApprovalObservation(
def: AgentHookDef,
input: ClaudeHookParsedInput
) -> Bool {
guard let payload = hermesAgentApprovalPayload(def: def, input: input) else { return false }
return firstString(in: payload.extra, keys: ["surface"])?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased() == "smart"
}
func hermesAgentShellCommand(_ script: String) -> String {
"sh -c \(shellQuote(script))"
}
@@ -32,9 +216,31 @@ extension CMUXCLI {
let skipConfirm = ProcessInfo.processInfo.arguments.contains("--yes")
|| ProcessInfo.processInfo.arguments.contains("-y")
guard fm.fileExists(atPath: configDir) else {
print("\(configDir) does not exist. Install \(def.displayName) first.")
return
let configDirectoryFileError = String.localizedStringWithFormat(
String(
localized: "cli.hooks.error.configDirectoryIsFile",
defaultValue: "cmux could not create the hooks directory: a file exists at %@. Remove or rename the conflicting file, then run `cmux hooks setup` again."
),
configDir
)
let configDirectoryCreateError = String.localizedStringWithFormat(
String(
localized: "cli.hooks.error.configDirectoryCreateFailed",
defaultValue: "cmux could not create the hooks directory at %@. Check the parent directory permissions and try again."
),
configDir
)
var isConfigDirectory: ObjCBool = false
if fm.fileExists(atPath: configDir, isDirectory: &isConfigDirectory) {
guard isConfigDirectory.boolValue else {
throw CLIError(message: configDirectoryFileError)
}
} else {
do {
try fm.createDirectory(atPath: configDir, withIntermediateDirectories: true)
} catch {
throw CLIError(message: configDirectoryCreateError)
}
}
let events = hermesAgentEvents(def: def)
@@ -61,10 +267,10 @@ extension CMUXCLI {
print("\(def.displayName) hooks already up to date at \(filePath)")
}
let oldAllowlist = fm.contents(atPath: allowlistPath)
let newAllowlist = try HermesAgentHookAllowlist.installing(events: events, in: oldAllowlist)
if oldAllowlist != newAllowlist {
try newAllowlist.write(to: URL(fileURLWithPath: allowlistPath), options: .atomic)
let updatedAllowlist = try updateHermesAgentAllowlist(atPath: allowlistPath) { oldAllowlist in
try HermesAgentHookAllowlist.installing(events: events, in: oldAllowlist)
}
if updatedAllowlist {
print("Approved \(def.displayName) cmux shell hooks in \(allowlistPath)")
}
}
@@ -90,10 +296,10 @@ extension CMUXCLI {
}
guard fm.fileExists(atPath: allowlistPath) else { return }
let oldAllowlist = fm.contents(atPath: allowlistPath)
let newAllowlist = try HermesAgentHookAllowlist.uninstalling(events: events, from: oldAllowlist)
if oldAllowlist != newAllowlist {
try newAllowlist.write(to: URL(fileURLWithPath: allowlistPath), options: .atomic)
let updatedAllowlist = try updateHermesAgentAllowlist(atPath: allowlistPath) { oldAllowlist in
try HermesAgentHookAllowlist.uninstalling(events: events, from: oldAllowlist)
}
if updatedAllowlist {
print("Removed Hermes Agent cmux shell hook approvals from \(allowlistPath)")
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ extension CMUXCLI {
let configDirectoryFileError = String.localizedStringWithFormat(
String(
localized: "cli.hooks.error.configDirectoryIsFile",
defaultValue: "cmux could not create the hooks directory: a file exists at %@; remove or rename the conflicting file and re-run `cmux hooks setup`"
defaultValue: "cmux could not create the hooks directory: a file exists at %@. Remove or rename the conflicting file, then run `cmux hooks setup` again."
),
configDir
)
+8
View File
@@ -78,6 +78,14 @@ extension CMUXCLI {
remoteMoshProbeFailedMessage: String(
localized: "cli.ssh.mosh.probeFailed",
defaultValue: "[cmux] Could not verify remote Mosh support; continuing over SSH."
),
remoteBootstrapInstallFailedMessage: String(
localized: "cli.ssh.mosh.bootstrapInstallFailed",
defaultValue: "[cmux] Remote bootstrap install failed; continuing over SSH."
),
remoteMoshAddressFallbackMessage: String(
localized: "cli.ssh.mosh.addressFallback",
defaultValue: "[cmux] Remote SSH advertised an unusable address; resolving the Mosh address through the SSH connection."
)
).command()
}
+69 -2
View File
@@ -4,7 +4,7 @@ extension CMUXCLI {
private static let ompExtensionMarker = "cmux-omp-session-extension-marker"
private static let ompExtensionFilename = "cmux-omp-session.ts"
private static let ompExtensionSource = #"""
// cmux-omp-session-extension-marker v1
// cmux-omp-session-extension-marker v2
// Bridges OMP session lifecycle events into cmux's restorable session store.
// Installed by `cmux hooks omp install` or `cmux hooks setup`.
// DO NOT EDIT MANUALLY. cmux upgrades this file in place.
@@ -316,20 +316,87 @@ function sendHook(subcommand: string, ctx: ExtensionContext, extra: Record<strin
return Promise.resolve();
}
// The pane's lifecycle in cmux must be owned by exactly one OMP session: the
// top-level session driving the terminal. Subagents spawned by the task tool
// run in the same process and inherit CMUX_SURFACE_ID, but each has its own
// session id; without an ownership check, every subagent's agent_end reports
// the whole pane idle while the main agent is still mid-turn, and Agent
// Hibernation then SIGHUPs the live pane (issue #9591).
//
// Ownership must live on globalThis, not in module scope: OMP loads a fresh
// copy of this module for every session in the process (each extension import
// uses a unique ?mtime= cache-busting URL), so module state is per-session
// while the pane is per-process.
interface CmuxOmpPaneOwnership {
sessionId: string | null;
}
const cmuxOmpGlobals = globalThis as typeof globalThis & {
__cmuxOmpPaneOwnership?: CmuxOmpPaneOwnership;
};
function paneOwnership(): CmuxOmpPaneOwnership {
cmuxOmpGlobals.__cmuxOmpPaneOwnership ??= { sessionId: null };
return cmuxOmpGlobals.__cmuxOmpPaneOwnership;
}
function contextSessionId(ctx: ExtensionContext): string | null {
return firstString(ctx.sessionManager.getSessionId());
}
function isOwnerContext(ctx: ExtensionContext): boolean {
const sessionId = contextSessionId(ctx);
return sessionId !== null && sessionId === paneOwnership().sessionId;
}
export default function cmuxOmpSessionExtension(api: ExtensionAPI) {
api.on("session_start", async (_event, ctx) => {
// The top-level session bootstraps before any subagent can exist in this
// process, so the first session_start pins ownership. Later session_start
// events with a different session id are subagent bootstraps.
const ownership = paneOwnership();
if (ownership.sessionId === null) ownership.sessionId = contextSessionId(ctx);
if (!isOwnerContext(ctx)) return;
await sendHook("session-start", ctx);
});
// In-process session transitions (/new, fork, resume, handoff) fire only on
// the top-level runtime; subagent sessions never switch or branch. Re-pin
// ownership to the new session id and rebind the surface in cmux.
const adoptSwitchedSession = async (_event: unknown, ctx: ExtensionContext) => {
const sessionId = contextSessionId(ctx);
if (!sessionId) return;
const ownership = paneOwnership();
// OMP's reload() re-emits session_switch for the unchanged session file.
// A same-id "switch" is not an ownership transition: a spurious
// session-start would mark an idle pane running with no agent_end coming.
if (ownership.sessionId === sessionId) return;
ownership.sessionId = sessionId;
await sendHook("session-start", ctx);
};
api.on("session_switch", adoptSwitchedSession);
api.on("session_branch", adoptSwitchedSession);
api.on("before_agent_start", async (event, ctx) => {
if (!isOwnerContext(ctx)) return;
await sendHook("prompt-submit", ctx, { prompt: boundedHookText(event.prompt) });
});
api.on("agent_end", async (event, ctx) => {
if (!isOwnerContext(ctx)) return;
// OMP emits agent_end as the universal terminal settle. willContinue marks
// a scheduled automatic continuation (auto-retry, queued messages,
// session_stop continuations, background jobs), so the turn is still
// logically running and the pane must not report idle yet. Read it
// defensively: AgentEndEvent predates the field on older OMP versions.
if ((event as { willContinue?: unknown }).willContinue === true) return;
await sendHook("stop", ctx, { last_assistant_message: boundedHookText(lastAssistantMessage(event)) });
});
api.on("session_shutdown", async () => {
api.on("session_shutdown", async (_event, ctx) => {
// A subagent session's teardown must not drain (and drop queued
// prompt-submit entries of) the owner session's hook queue.
if (!isOwnerContext(ctx)) return;
await awaitHookQueueDrain();
});
}
+1
View File
@@ -1,6 +1,7 @@
extension CMUXCLI {
static let piExtensionSource = [
piExtensionSourcePart1,
piExtensionSourceDiagnostics,
piExtensionSourceDispatch,
piExtensionSourcePart2,
].joined(separator: "\n")
@@ -0,0 +1,221 @@
extension CMUXCLI {
static let piExtensionSourceDiagnostics = #"""
type CommandFailureReason = "timeout" | "nonzero-exit" | "spawn-error" | "cancelled";
type CommandTerminationReason = "timeout" | "cancelled";
// Loaded repositories have produced successful 9s+ lifecycle hooks. Leave
// headroom above that observed tail without allowing a stuck child to block a
// session's serialized control queue indefinitely.
const defaultPiHookTimeoutMilliseconds = 15_000;
const maximumPiHookTimeoutMilliseconds = 60_000;
// Feed's CLI owns a four-second end-to-end deadline. Give the wrapper enough
// headroom that the child reports that outcome itself instead of being killed
// mid-deadline, while lifecycle tuning still cannot pin the shared Feed pool.
const maximumPiFeedCommandTimeoutMilliseconds = 4_500;
// Diagnostics are best effort and may hold a serialized hook queue only briefly.
const piHookDiagnosticWriteDeadlineMilliseconds = 100;
function piHookTimeoutMilliseconds(
rawValue: string | undefined = process.env.CMUX_PI_HOOK_TIMEOUT_MS,
): number {
const normalized = rawValue?.trim();
if (!normalized || !/^\d+$/.test(normalized)) return defaultPiHookTimeoutMilliseconds;
const parsed = Number(normalized);
if (parsed >= maximumPiHookTimeoutMilliseconds) return maximumPiHookTimeoutMilliseconds;
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : defaultPiHookTimeoutMilliseconds;
}
function piCommandTimeoutMilliseconds(
args: string[],
rawValue: string | undefined = process.env.CMUX_PI_HOOK_TIMEOUT_MS,
): number {
const configured = piHookTimeoutMilliseconds(rawValue);
return args[0] === "hooks" && args[1] === "feed"
? Math.min(configured, maximumPiFeedCommandTimeoutMilliseconds)
: configured;
}
function commandFailureReason(
status: number | null,
error: unknown,
terminationReason?: CommandTerminationReason,
): CommandFailureReason | undefined {
if (terminationReason) return terminationReason;
if (status === 0) return undefined;
if (status !== null && status !== 0) return "nonzero-exit";
return "spawn-error";
}
function boundedPiHookName(value: string): string {
return utf8Prefix(value, 128) || "unknown";
}
function piHookName(args: string[]): string {
if (args[0] === "hooks" && args[1] === "pi") {
return boundedPiHookName(firstString(args[2]) || "unknown");
}
if (args[0] === "hooks" && args[1] === "feed") {
const eventIndex = args.indexOf("--event");
const eventName = eventIndex >= 0 ? firstString(args[eventIndex + 1]) : null;
return boundedPiHookName(eventName ? `feed:${eventName}` : "feed");
}
if (args[0] === "--json" && args[1] === "surface" && args[2] === "resume") {
return boundedPiHookName(`surface-resume-${firstString(args[3]) || "unknown"}`);
}
return "cmux-command";
}
function expandedPiHookLogPath(value: string, home: string | undefined = process.env.HOME): string {
if (value === "~") return home || value;
if (value.startsWith("~/") && home) {
return path.join(home, value.slice(2));
}
return value;
}
function isOwnedRegularPiHookFile(metadata: fs.Stats): boolean {
return metadata.isFile()
&& typeof process.getuid === "function"
&& metadata.uid === process.getuid();
}
let activePiHookDiagnosticWrite: Promise<void> | undefined;
async function runPiHookDiagnosticWrite(operation: () => Promise<void>): Promise<void> {
// Retain at most one file operation. If it stalls after the caller's deadline,
// later diagnostics are dropped instead of accumulating promises or handles.
if (activePiHookDiagnosticWrite) return;
let tracked: Promise<void>;
tracked = Promise.resolve()
.then(operation)
.catch(() => {})
.finally(() => {
if (activePiHookDiagnosticWrite === tracked) activePiHookDiagnosticWrite = undefined;
});
activePiHookDiagnosticWrite = tracked;
let deadline: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
tracked,
new Promise<void>((resolve) => {
deadline = setTimeout(resolve, piHookDiagnosticWriteDeadlineMilliseconds);
}),
]);
} finally {
if (deadline !== undefined) clearTimeout(deadline);
}
}
function piHookDiagnosticPath(
environment: Record<string, string | undefined> = process.env,
lastDebugLogPathFile = "/tmp/cmux-last-debug-log-path",
fallbackLogPath = "/tmp/cmux-debug.log",
): string {
const explicit = firstString(environment.CMUX_DEBUG_LOG);
if (explicit) return expandedPiHookLogPath(explicit, environment.HOME);
const socketPath = firstString(environment.CMUX_SOCKET_PATH, environment.CMUX_SOCKET);
if (socketPath) {
const socketName = path.basename(socketPath);
if (socketName.startsWith("cmux-debug-") && socketName.endsWith(".sock")) {
return path.join("/tmp", `${socketName.slice(0, -".sock".length)}.log`);
}
}
let pointerDescriptor: number | undefined;
try {
// The shared pointer is untrusted: inspect a nonblocking descriptor and
// bound the read so a special or oversized file cannot stall Pi.
pointerDescriptor = fs.openSync(
lastDebugLogPathFile,
fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | fs.constants.O_NOFOLLOW,
);
if (isOwnedRegularPiHookFile(fs.fstatSync(pointerDescriptor))) {
const pointerContents = Buffer.alloc(4096);
const bytesRead = fs.readSync(
pointerDescriptor,
pointerContents,
0,
pointerContents.byteLength,
0,
);
const lastPath = firstString(pointerContents.subarray(0, bytesRead).toString("utf8"));
if (lastPath) return expandedPiHookLogPath(lastPath, environment.HOME);
}
} catch (_) {
} finally {
if (pointerDescriptor !== undefined) {
try { fs.closeSync(pointerDescriptor); } catch (_) {}
}
}
return fallbackLogPath;
}
async function appendPiHookDiagnostic(
payload: Record<string, unknown>,
environment: Record<string, string | undefined> = process.env,
lastDebugLogPathFile = "/tmp/cmux-last-debug-log-path",
fallbackLogPath = "/tmp/cmux-debug.log",
): Promise<void> {
let line: string;
try {
line = JSON.stringify({ timestamp: new Date().toISOString(), ...payload });
} catch (_) {
line = JSON.stringify({
timestamp: new Date().toISOString(),
source: "cmux-pi-extension",
level: "warning",
message: "failed to serialize Pi hook diagnostic",
hook_name: "extension",
reason: "serialization-error",
timeout_ms: piHookTimeoutMilliseconds(),
elapsed_ms: 0,
});
}
try {
// Read/write permits checking the existing JSONL boundary, while O_NONBLOCK
// keeps special files such as a FIFO from stalling Pi's lifecycle queue.
const flags = fs.constants.O_RDWR
| fs.constants.O_APPEND
| fs.constants.O_CREAT
| fs.constants.O_NONBLOCK
| fs.constants.O_NOFOLLOW;
const handle = await fs.promises.open(
piHookDiagnosticPath(environment, lastDebugLogPathFile, fallbackLogPath),
flags,
0o600,
);
try {
const metadata = await handle.stat();
// cmux diagnostics are files; drop device, socket, and pipe destinations.
if (!isOwnedRegularPiHookFile(metadata)) return;
let prefix = "";
if (metadata.size > 0) {
const trailingByte = Buffer.alloc(1);
const { bytesRead } = await handle.read(trailingByte, 0, 1, metadata.size - 1);
if (bytesRead !== 1 || trailingByte[0] !== 0x0a) prefix = "\n";
}
await handle.writeFile(`${prefix}${line}\n`, "utf8");
} finally {
try { await handle.close(); } catch (_) {}
}
} catch (_) {}
}
function commandFailureDetails(
args: string[],
result: CommandResult,
): Record<string, unknown> {
return {
hook_name: piHookName(args),
reason: result.reason || commandFailureReason(result.status, result.error) || "spawn-error",
timeout_ms: result.timeoutMs,
elapsed_ms: result.elapsedMs,
status: result.status,
stderr_available: result.stderr.trim().length > 0,
error_available: result.error !== undefined,
};
}
"""#
}
+58 -21
View File
@@ -335,7 +335,7 @@ class PiCmuxCommandDispatcher {
this.failTerminalFeedForSession(sessionId);
this.discardFeedForSession(sessionId);
}
} else if (result.error instanceof Error && result.error.message.includes("timed out after")) {
} else if (result.reason === "timeout") {
const sessionId = command.context.sessionId;
if (sessionId) {
this.failTerminalFeedForSession(sessionId);
@@ -367,18 +367,20 @@ class PiCmuxCommandDispatcher {
}
const result = await this.spawnCmux(args, cwd, input, cancellation);
if (this.isSurfaceResolutionFailure(result)) {
const shouldWarn = !sessionId || !this.unavailableSessions.has(sessionId);
if (sessionId) this.unavailableSessions.add(sessionId);
if (shouldWarn) {
warn(context, "cmux hook command failed", {
status: result.status,
stderr_available: result.stderr.trim().length > 0,
error_available: result.error !== undefined,
surface_unavailable: true,
dispatch_disabled: true,
});
}
const surfaceUnavailable = this.isSurfaceResolutionFailure(result);
let shouldLogFailure = true;
if (surfaceUnavailable && sessionId) {
// Claim synchronously so overlapping Feed/control failures emit one diagnostic.
shouldLogFailure = !this.unavailableSessions.has(sessionId);
this.unavailableSessions.add(sessionId);
}
if (!result.ok && result.reason !== "cancelled" && shouldLogFailure) {
await warn(context, "cmux hook command failed", {
...commandFailureDetails(args, result),
...(surfaceUnavailable ? { surface_unavailable: true, dispatch_disabled: true } : {}),
});
}
if (surfaceUnavailable) {
return { ...result, surfaceUnavailable: true };
}
return result;
@@ -391,6 +393,8 @@ class PiCmuxCommandDispatcher {
cancellation?: PiCommandCancellation,
): Promise<CommandResult> {
return new Promise<CommandResult>((resolve) => {
const startedAt = performance.now();
const timeoutMs = piCommandTimeoutMilliseconds(args);
let settled = false;
let stdout = "";
let stderr = "";
@@ -399,6 +403,7 @@ class PiCmuxCommandDispatcher {
let terminateGrace: ReturnType<typeof setTimeout> | null = null;
let forceSettleTimeout: ReturnType<typeof setTimeout> | null = null;
let terminationError: Error | undefined;
let terminationReason: CommandTerminationReason | undefined;
const appendOutput = (current: string, chunk: unknown): string => {
const limit = 1024 * 1024;
@@ -414,12 +419,18 @@ class PiCmuxCommandDispatcher {
if (cancellation) cancellation.cancel = undefined;
resolve(result);
};
const elapsedMilliseconds = (): number => (
Math.max(0, Math.round(performance.now() - startedAt))
);
const terminatedResult = (): CommandResult => ({
ok: false,
status: null,
stdout,
stderr,
error: terminationError,
reason: commandFailureReason(null, terminationError, terminationReason),
timeoutMs,
elapsedMs: elapsedMilliseconds(),
});
try {
@@ -438,9 +449,10 @@ class PiCmuxCommandDispatcher {
child.stdin.on("error", (error) => {
inputError = error;
});
const beginTermination = (error: Error) => {
const beginTermination = (reason: CommandTerminationReason, error: Error) => {
if (terminationError) return;
terminationError = error;
terminationReason = reason;
child.stdin.destroy();
try {
child.kill("SIGTERM");
@@ -458,7 +470,16 @@ class PiCmuxCommandDispatcher {
}, 250);
};
child.on("error", (error) => {
settle(terminationError ? terminatedResult() : { ok: false, status: null, stdout, stderr, error });
settle(terminationError ? terminatedResult() : {
ok: false,
status: null,
stdout,
stderr,
error,
reason: commandFailureReason(null, error),
timeoutMs,
elapsedMs: elapsedMilliseconds(),
});
});
child.on("close", (code) => {
if (terminationError) {
@@ -466,24 +487,38 @@ class PiCmuxCommandDispatcher {
return;
}
const status = typeof code === "number" ? code : null;
const error = inputError;
const reason = commandFailureReason(status, error);
settle({
ok: status === 0 && inputError === undefined,
ok: reason === undefined,
status,
stdout,
stderr,
error: inputError,
error,
reason,
timeoutMs,
elapsedMs: elapsedMilliseconds(),
});
});
if (cancellation) {
cancellation.cancel = () => beginTermination(new Error("cmux feed command cancelled"));
cancellation.cancel = () => beginTermination("cancelled", new Error("cmux feed command cancelled"));
if (cancellation.cancelled) cancellation.cancel();
}
timeout = setTimeout(() => {
beginTermination(new Error("cmux command timed out after 5000ms"));
}, 5000);
beginTermination("timeout", new Error(`cmux command timed out after ${timeoutMs}ms`));
}, timeoutMs);
child.stdin.end(input);
} catch (error) {
settle({ ok: false, status: null, stdout, stderr, error });
settle({
ok: false,
status: null,
stdout,
stderr,
error,
reason: commandFailureReason(null, error),
timeoutMs,
elapsedMs: elapsedMilliseconds(),
});
}
});
}
@@ -498,6 +533,8 @@ class PiCmuxCommandDispatcher {
status: null,
stdout: "",
stderr: "",
timeoutMs: piHookTimeoutMilliseconds(),
elapsedMs: 0,
surfaceUnavailable: true,
};
}
+110 -59
View File
@@ -1,6 +1,6 @@
extension CMUXCLI {
static let piExtensionSourcePart1 = #"""
// cmux-pi-session-extension-marker v2
// cmux-pi-session-extension-marker v3
// Bridges Pi session lifecycle, tool telemetry, notifications, and resume bindings into cmux.
// Installed by `cmux hooks pi install` or `cmux hooks setup`.
// DO NOT EDIT MANUALLY. cmux upgrades this file in place.
@@ -17,6 +17,7 @@ interface PendingCompletion {
lastAssistantMessage?: string;
notificationType: string;
turnId: string;
suppressNotification: boolean;
}
interface SessionState {
@@ -33,13 +34,15 @@ interface CommandResult {
stdout: string;
stderr: string;
error?: unknown;
reason?: CommandFailureReason;
timeoutMs: number;
elapsedMs: number;
surfaceUnavailable?: boolean;
}
interface PiExtensionContextSnapshot {
readonly sessionId: string | null;
readonly cwd: string;
readonly notifyWarning?: () => void;
}
function firstString(...values: unknown[]): string | null {
@@ -219,43 +222,84 @@ function looksLikePiScript(value: string): boolean {
);
}
function normalizedLaunchArgv(): string[] {
const raw = Array.isArray(process.argv) ? process.argv.map((value) => String(value)) : [];
if (raw.length === 0) return [resolveExecutable("pi")];
if (looksLikePiExecutable(raw[0])) return raw;
if (raw.length > 1 && looksLikePiScript(raw[1])) {
return [resolveExecutable("pi"), ...raw.slice(2)];
}
return [resolveExecutable("pi"), ...raw.slice(1)];
interface NormalizedLaunchArgvCache {
key: string;
argv: string[];
}
let normalizedLaunchArgvCache: NormalizedLaunchArgvCache | undefined;
function normalizedLaunchArgv(): string[] {
const raw = Array.isArray(process.argv) ? process.argv.map((value) => String(value)) : [];
// Pi's argv and inherited PATH are stable for the lifetime of this extension.
// Memoize executable discovery so every hook subprocess does not synchronously
// stat the full PATH again. Keep the key dynamic for test harnesses and hosts
// that deliberately rewrite process argv at runtime.
const cacheKey = [process.env.PATH || "", ...raw].join("\0");
if (normalizedLaunchArgvCache?.key === cacheKey) {
return normalizedLaunchArgvCache.argv;
}
let argv: string[];
if (raw.length === 0) {
argv = [resolveExecutable("pi")];
} else if (looksLikePiExecutable(raw[0])) {
argv = raw;
} else if (raw.length > 1 && looksLikePiScript(raw[1])) {
argv = [resolveExecutable("pi"), ...raw.slice(2)];
} else {
argv = [resolveExecutable("pi"), ...raw.slice(1)];
}
normalizedLaunchArgvCache = { key: cacheKey, argv };
return argv;
}
interface DetectedPiVersionCache {
key: string;
version: string | null;
}
let detectedPiVersionCache: DetectedPiVersionCache | undefined;
function detectedPiVersion(): string | null {
const cacheKey = [
process.cwd(),
...process.argv.slice(0, 2).map((value) => String(value)),
].join("\0");
if (detectedPiVersionCache?.key === cacheKey) {
return detectedPiVersionCache.version;
}
const script = process.argv.slice(0, 2).find((value) => {
const candidate = String(value);
return looksLikePiScript(candidate) || looksLikePiExecutable(candidate);
});
if (!script) return null;
let scriptPath = path.resolve(String(script));
try {
// npm launches through bin symlinks, so inspect the package containing the resolved script.
scriptPath = fs.realpathSync(scriptPath);
} catch (_) {}
let directory = path.dirname(scriptPath);
for (let depth = 0; depth < 8; depth += 1) {
let version: string | null = null;
if (script) {
let scriptPath = path.resolve(String(script));
try {
const packageJSON = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"));
if (
packageJSON?.name === "@earendil-works/pi-coding-agent" ||
packageJSON?.name === "@mariozechner/pi-coding-agent"
) {
return firstString(packageJSON.version);
}
// npm launches through bin symlinks, so inspect the package containing the resolved script.
scriptPath = fs.realpathSync(scriptPath);
} catch (_) {}
const parent = path.dirname(directory);
if (parent === directory) break;
directory = parent;
let directory = path.dirname(scriptPath);
for (let depth = 0; depth < 8; depth += 1) {
try {
const packageJSON = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"));
if (
packageJSON?.name === "@earendil-works/pi-coding-agent" ||
packageJSON?.name === "@mariozechner/pi-coding-agent"
) {
version = firstString(packageJSON.version);
break;
}
} catch (_) {}
const parent = path.dirname(directory);
if (parent === directory) break;
directory = parent;
}
}
return null;
detectedPiVersionCache = { key: cacheKey, version };
return version;
}
function supportsAgentSettled(): boolean {
@@ -306,6 +350,7 @@ function safeCmuxEnvKey(key: string): boolean {
if (key.startsWith("CMUX_AGENT_LAUNCH_")) return !secretLikeEnvKey(key);
if (key === "CMUX_AGENT_HOOK_STATE_DIR") return true;
if (key === "CMUX_PI_CMUX_BIN" || key === "CMUX_PI_HOOKS_DISABLED") return true;
if (key === "CMUX_PI_HOOK_TIMEOUT_MS") return true;
if (key === "CMUX_SURFACE_ID" || key === "CMUX_WORKSPACE_ID" || key === "CMUX_WINDOW_ID") return true;
if (key === "CMUX_PANE_ID" || key === "CMUX_TAB_ID" || key === "CMUX_PANEL_ID") return true;
if (key === "CMUX_SOCKET" || key === "CMUX_SOCKET_PATH") return true;
@@ -375,18 +420,38 @@ function textFromContent(content: unknown): string | null {
return parts.join("\n") || null;
}
function lastAssistantMessage(event: unknown): string | undefined {
interface AssistantCompletion {
lastAssistantMessage?: string;
suppressNotification: boolean;
}
function assistantCompletionFrom(event: unknown): AssistantCompletion {
const messagesValue = objectValue(event, ["messages"]);
const messages = Array.isArray(messagesValue) ? messagesValue : [];
let suppressNotification = false;
let inspectedLatestAssistant = false;
// Resolve text and interruption metadata in one reverse pass. agent_end may
// carry a large message array, so notification support must not rescan it.
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (!message || typeof message !== "object") continue;
const typed = message as { role?: unknown; content?: unknown };
const typed = message as {
role?: unknown;
content?: unknown;
stopReason?: unknown;
cmuxSuppressNotification?: unknown;
};
if (typed.role !== "assistant") continue;
if (!inspectedLatestAssistant) {
// Input extensions may normalize an abort to `stop` to keep Pi's UI quiet;
// the marker preserves the interruption intent across that normalization.
suppressNotification = typed.stopReason === "aborted" || typed.cmuxSuppressNotification === true;
inspectedLatestAssistant = true;
}
const text = firstString(textFromContent(typed.content));
if (text) return text;
if (text) return { lastAssistantMessage: text, suppressNotification };
}
return undefined;
return { suppressNotification };
}
function sessionIdFrom(ctx: ExtensionContext): string | null {
@@ -398,17 +463,9 @@ function cwdFrom(ctx: ExtensionContext): string {
}
function snapshotContext(ctx: ExtensionContext): PiExtensionContextSnapshot {
let notifyWarning: (() => void) | undefined;
try {
const ui = (ctx as unknown as { ui?: { notify?: (message: string, type?: string) => void } }).ui;
if (typeof ui?.notify === "function") {
notifyWarning = () => ui.notify?.("cmux Pi integration warning - check the terminal for details", "warning");
}
} catch (_) {}
return {
sessionId: sessionIdFrom(ctx),
cwd: cwdFrom(ctx),
notifyWarning,
};
}
@@ -466,26 +523,20 @@ function settleTurn(sessionStates: Map<string, SessionState>, sessionId: string)
return completion;
}
function warn(
ctx: PiExtensionContextSnapshot | null,
async function warn(
_ctx: PiExtensionContextSnapshot | null,
message: string,
details: Record<string, unknown> = {},
notifyUser = false,
): void {
const payload = { source: "cmux-pi-extension", level: "warning", message, ...details };
try {
console.warn(JSON.stringify(payload));
} catch (_) {
console.warn(`[cmux-pi-extension] ${message}`);
}
// Hook transport is best-effort telemetry. Keep routine command failures in
// the terminal instead of interrupting Pi with a generic toast; reserve the
// UI warning for an unexpected extension-task exception.
if (notifyUser) {
try {
ctx?.notifyWarning?.();
} catch (_) {}
}
): Promise<void> {
const payload = {
source: "cmux-pi-extension",
level: "warning",
message,
hook_name: "extension",
reason: "extension-error",
...details,
};
await runPiHookDiagnosticWrite(() => appendPiHookDiagnostic(payload));
}
function cmuxExecutable(): string {
+113 -66
View File
@@ -27,14 +27,6 @@ async function sendHook(
context,
);
if (result.ok) rememberSurfaceTarget(dispatcher, sessionId, result);
if (!result.ok && !result.surfaceUnavailable) {
warn(context, "cmux hook command failed", {
subcommand,
status: result.status,
stderr_available: result.stderr.trim().length > 0,
error_available: result.error !== undefined,
});
}
return result.ok;
}
@@ -206,14 +198,7 @@ async function ensureResumeBinding(
"--",
...resumeArgv,
], cwd, undefined, context);
if (!set.ok && !set.surfaceUnavailable) {
warn(context, "failed to set Pi resume binding", {
status: set.status,
stderr_available: set.stderr.trim().length > 0,
error_available: set.error !== undefined,
});
return;
}
if (!set.ok && !set.surfaceUnavailable) return;
if (set.surfaceUnavailable) return;
const verification = await dispatcher.run(
@@ -225,7 +210,11 @@ async function ensureResumeBinding(
if (verification.surfaceUnavailable) return;
const verified = parseJSONOutput(verification);
if (!resumeBindingMatches(verified, sessionId)) {
warn(context, "Pi resume binding did not verify after write", { session_id: sessionId });
await warn(context, "Pi resume binding did not verify after write", {
session_id: sessionId,
hook_name: "surface-resume-get",
reason: "verification-failure",
});
}
}
@@ -238,7 +227,7 @@ async function clearResumeBinding(
const target = surfaceTargetArgs(dispatcher, sessionId);
if (!target) return;
const cwd = context.cwd;
const result = await dispatcher.run([
await dispatcher.run([
"--json",
"surface",
"resume",
@@ -249,14 +238,6 @@ async function clearResumeBinding(
"--source",
"agent-hook",
], cwd, undefined, context);
if (result.surfaceUnavailable) return;
if (!result.ok) {
warn(context, "failed to clear Pi resume binding", {
status: result.status,
stderr_available: result.stderr.trim().length > 0,
error_available: result.error !== undefined,
});
}
}
type PiFeedEventName =
@@ -299,42 +280,59 @@ function prepareFeedDispatch(
const cwd = context.cwd;
const toolCallId = firstString(objectValue(event, ["toolCallId", "tool_call_id", "id"]));
const toolName = firstString(objectValue(event, ["toolName", "tool_name", "name"]));
const projectionState: PiFeedProjectionState = { remainingNodes: 48, seen: new WeakSet() };
const payload: HookExtra = {
session_id: utf8Prefix(sessionId, 256),
cwd: utf8Prefix(cwd, 2048),
hook_event_name: eventName,
event: eventName,
turn_id: utf8Prefix(currentTurnId(sessionStates, sessionId, event), 256),
};
const boundedToolCallId = utf8Prefix(toolCallId, 256);
if (boundedToolCallId !== undefined) payload.tool_call_id = boundedToolCallId;
const boundedToolName = utf8Prefix(toolName, 256);
if (boundedToolName !== undefined) payload.tool_name = boundedToolName;
const turnId = currentTurnId(sessionStates, sessionId, event);
const toolInput = objectValue(event, ["args", "input"]);
if (toolInput !== undefined) payload.tool_input = projectPiFeedValue(toolInput, projectionState);
if (isTerminalFeedEvent(eventName)) {
const toolResult = objectValue(event, ["result", "details", "content"]);
if (toolResult !== undefined) {
payload.tool_result = projectPiFeedValue(toolResult, projectionState, 0, false);
}
const isError = objectValue(event, ["isError", "is_error"]);
if (isError !== undefined) payload.is_error = projectPiFeedValue(isError, projectionState);
}
const terminal = isTerminalFeedEvent(eventName);
const toolResult = terminal
? objectValue(event, ["result", "details", "content"])
: undefined;
const isError = terminal ? objectValue(event, ["isError", "is_error"]) : undefined;
return () => {
const target = surfaceTargetArgs(dispatcher, sessionId);
if (!target) return;
// Pi invokes tool lifecycle handlers on its UI event loop. Keep those
// callbacks lightweight by traversing and bounding tool payloads only in
// the already-detached lifecycle task.
const projectionState: PiFeedProjectionState = { remainingNodes: 48, seen: new WeakSet() };
const payload: HookExtra = {
session_id: utf8Prefix(sessionId, 256),
cwd: utf8Prefix(cwd, 2048),
hook_event_name: eventName,
event: eventName,
turn_id: utf8Prefix(turnId, 256),
};
const boundedToolCallId = utf8Prefix(toolCallId, 256);
if (boundedToolCallId !== undefined) payload.tool_call_id = boundedToolCallId;
const boundedToolName = utf8Prefix(toolName, 256);
if (boundedToolName !== undefined) payload.tool_name = boundedToolName;
if (toolInput !== undefined) payload.tool_input = projectPiFeedValue(toolInput, projectionState);
if (toolResult !== undefined) {
payload.tool_result = projectPiFeedValue(toolResult, projectionState, 0, false);
}
if (isError !== undefined) payload.is_error = projectPiFeedValue(isError, projectionState);
dispatcher.enqueueFeed(`${sessionId}:${toolCallId || toolName || "unknown"}`, {
args: ["hooks", "feed", "--source", "pi", "--event", eventName, ...target],
cwd,
payload,
context,
terminal: isTerminalFeedEvent(eventName),
terminal,
onFailure: () => { state.feedDeliveryFailed = true; },
});
};
}
async function warnFeedDeliveryDropped(
context: PiExtensionContextSnapshot,
sessionId: string,
): Promise<void> {
await warn(context, "cmux feed delivery dropped", {
session_id: sessionId,
hook_name: "feed",
reason: "dispatch-dropped",
});
}
async function publishPendingCompletion(
dispatcher: PiCmuxCommandDispatcher,
sessionStates: Map<string, SessionState>,
@@ -346,14 +344,16 @@ async function publishPendingCompletion(
const state = stateFor(sessionStates, sessionId);
const feedDelivered = !state.feedDeliveryFailed;
state.feedDeliveryFailed = false;
if (!feedDelivered) {
warn(context, "cmux hook command failed", { session_id: sessionId });
}
if (!feedDelivered) await warnFeedDeliveryDropped(context, sessionId);
const stopPayload: HookExtra = {
last_assistant_message: completion.lastAssistantMessage,
turn_id: completion.turnId,
};
if (feedDelivered) {
if (completion.suppressNotification) {
// Stop normally creates cmux's native fallback notification when no explicit
// notification was routed. Mark intentional interruption as already handled.
stopPayload.cmux_notification_routed = true;
} else if (feedDelivered) {
const notificationRouted = await sendHook(dispatcher, "notification", context, {
message: completion.lastAssistantMessage || "Task completed",
turn_id: completion.turnId,
@@ -364,34 +364,77 @@ async function publishPendingCompletion(
await sendHook(dispatcher, "stop", context, stopPayload);
}
export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
const dispatcher = new PiCmuxCommandDispatcher();
const sessionStates = new Map<string, SessionState>();
const lifecycleTails = new Map<string, Promise<void>>();
// A stalled lifecycle hook may run for its full configured timeout while Pi
// keeps emitting tool events. Bound the pending tasks a session can stack
// behind it so bursts cannot pin unbounded event payloads: droppable Feed
// preparation is shed first and surfaces as a dropped delivery at completion.
const maximumPiLifecycleBacklogTasks = 32;
const enqueueLifecycleTask = (
interface PiLifecycleQueue {
enqueue(
sessionId: string,
context: PiExtensionContextSnapshot,
operation: () => Promise<unknown> | unknown,
): Promise<void>;
tryEnqueue(
sessionId: string,
context: PiExtensionContextSnapshot,
operation: () => Promise<unknown> | unknown,
): boolean;
}
function createPiLifecycleQueue(): PiLifecycleQueue {
const tails = new Map<string, Promise<void>>();
const pendingCounts = new Map<string, number>();
const enqueue = (
sessionId: string,
context: PiExtensionContextSnapshot,
operation: () => Promise<unknown> | unknown,
): Promise<void> => {
const previous = lifecycleTails.get(sessionId) || Promise.resolve();
pendingCounts.set(sessionId, (pendingCounts.get(sessionId) || 0) + 1);
const previous = tails.get(sessionId) || Promise.resolve();
let tracked: Promise<void>;
tracked = previous
.then(operation)
.then(() => undefined)
.catch((error) => {
const errorMessage = error instanceof Error ? error.message : undefined;
warn(context, "cmux lifecycle task failed", {
return warn(context, "cmux lifecycle task failed", {
hook_name: "lifecycle-task",
reason: "extension-error",
error_available: error !== undefined,
error_message: utf8Prefix(errorMessage, 512),
}, true);
});
})
.finally(() => {
if (lifecycleTails.get(sessionId) === tracked) lifecycleTails.delete(sessionId);
const remaining = (pendingCounts.get(sessionId) || 1) - 1;
if (remaining > 0) pendingCounts.set(sessionId, remaining);
else pendingCounts.delete(sessionId);
if (tails.get(sessionId) === tracked) tails.delete(sessionId);
});
lifecycleTails.set(sessionId, tracked);
tails.set(sessionId, tracked);
return tracked;
};
return {
enqueue,
tryEnqueue(sessionId, context, operation) {
if ((pendingCounts.get(sessionId) || 0) >= maximumPiLifecycleBacklogTasks) return false;
void enqueue(sessionId, context, operation);
return true;
},
};
}
export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
const dispatcher = new PiCmuxCommandDispatcher();
const sessionStates = new Map<string, SessionState>();
const lifecycleTasks = createPiLifecycleQueue();
const enqueueLifecycleTask = (
sessionId: string,
context: PiExtensionContextSnapshot,
operation: () => Promise<unknown> | unknown,
): Promise<void> => lifecycleTasks.enqueue(sessionId, context, operation);
pi.on("session_start", (_event, ctx) => {
const context = snapshotContext(ctx);
@@ -429,7 +472,10 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
if (!sessionId) return;
const dispatch = prepareFeedDispatch(dispatcher, sessionStates, eventName, context, event);
if (!dispatch) return;
enqueueLifecycleTask(sessionId, context, dispatch);
if (!lifecycleTasks.tryEnqueue(sessionId, context, dispatch)) {
// A shed completion must fail visibly instead of reporting delivery.
if (isTerminalFeedEvent(eventName)) stateFor(sessionStates, sessionId).feedDeliveryFailed = true;
}
};
pi.on("tool_execution_start", (event, ctx) => {
@@ -453,12 +499,13 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
const sessionId = context.sessionId;
if (!sessionId) return;
const state = stateFor(sessionStates, sessionId);
const message = lastAssistantMessage(event);
const assistantCompletion = assistantCompletionFrom(event);
// Preserve the latest low-level result until Pi confirms no automatic work remains.
state.pendingCompletion = {
lastAssistantMessage: message || state.pendingCompletion?.lastAssistantMessage,
lastAssistantMessage: assistantCompletion.lastAssistantMessage || state.pendingCompletion?.lastAssistantMessage,
notificationType: firstString(objectValue(event, ["stopReason", "reason", "terminationReason"])) || "completed",
turnId: currentTurnId(sessionStates, sessionId, event),
suppressNotification: assistantCompletion.suppressNotification,
};
// Older Pi versions do not emit agent_settled, so retain their established completion behavior.
if (!supportsAgentSettled()) {
@@ -502,7 +549,7 @@ export default function cmuxPiSessionExtension(pi: ExtensionAPI) {
await dispatcher.finishFeedForSession(sessionId);
const feedDelivered = !state.feedDeliveryFailed;
state.feedDeliveryFailed = false;
if (!feedDelivered) warn(context, "cmux hook command failed", { session_id: sessionId });
if (!feedDelivered) await warnFeedDeliveryDropped(context, sessionId);
if (stopPayload) await sendHook(dispatcher, "stop", context, stopPayload);
try {
await clearResumeBinding(dispatcher, context, sessionId);
+256 -39
View File
@@ -1,7 +1,15 @@
import CMUXAgentLaunch
import Darwin
import Foundation
extension CMUXCLI {
var restoreCommandUsageLine: String {
String(
localized: "cli.help.restore",
defaultValue: "restore [--surface <id|ref>] <kind> <checkpoint-id> | restore --surface [id|ref]"
)
}
func controlAgentLaunchCommandPayload(
_ command: AgentLaunchCommand
) -> [String: Any] {
@@ -18,6 +26,9 @@ extension CMUXCLI {
if let environment = command.environment {
payload["environment"] = environment
}
if let verificationHome = command.verificationHome {
payload["verification_home"] = verificationHome
}
if let capturedAt = command.capturedAt {
payload["captured_at"] = capturedAt
}
@@ -53,23 +64,13 @@ extension CMUXCLI {
}
params["surface_id"] = surfaceID
} else if selector.usesCurrentSurface,
let surfaceID = processEnvironment["CMUX_SURFACE_ID"],
!surfaceID.isEmpty {
params["surface_id"] = surfaceID
} else if selector.usesCurrentSurface,
let ttyName = resolveCallerTTYName(),
let caller = resolveTerminalBinding(
ttyName: ttyName,
client: client
let surfaceID = try currentRestoreSurfaceID(
client: client,
processEnvironment: processEnvironment
) {
params["surface_id"] = caller.surfaceId
params["surface_id"] = surfaceID
} else {
throw CLIError(
message: String(
localized: "cli.restore.error.currentSurfaceUnknown",
defaultValue: "restore: the current cmux surface could not be identified. Retry from this terminal or pass --surface <id|ref>."
)
)
throw currentRestoreSurfaceUnknownError()
}
let payload = try client.sendV2(method: "surface.resume.get", params: params)
@@ -82,7 +83,7 @@ extension CMUXCLI {
)
)
}
let record = try restoreRecord(from: rawRecord)
var record = try restoreRecord(from: rawRecord)
if let expectedKind = selector.kind, expectedKind != record.kind {
throw loggedRestoreError(
stage: "record.kind-mismatch",
@@ -105,6 +106,14 @@ extension CMUXCLI {
)
}
if let surfaceID = params["surface_id"] as? String {
record = try recoveredHermesRestoreRecord(
record,
surfaceID: surfaceID,
processEnvironment: processEnvironment
)
}
let environment = processEnvironment.merging(record.environment) { _, restored in
restored
}
@@ -190,42 +199,249 @@ extension CMUXCLI {
)
}
private func restoreSelector(_ arguments: [String]) throws -> RestoreSelector {
if arguments.first == "--surface" {
if arguments.count == 1 {
return RestoreSelector(
surface: nil,
usesCurrentSurface: true,
kind: nil,
checkpointID: nil
/// Repairs transient Hermes TUI identities using hook process-generation
/// evidence and the durable Hermes state database.
private func recoveredHermesRestoreRecord(
_ record: RestoreRecord,
surfaceID: String,
processEnvironment: [String: String]
) throws -> RestoreRecord {
guard record.kind == "hermes-agent",
let checkpointID = record.checkpointID,
let surfaceUUID = UUID(uuidString: surfaceID) else {
return record
}
var recoveryEnvironment = processEnvironment
recoveryEnvironment.merge(record.environment) { _, restored in restored }
if let captured = record.launchCommand?.environment {
recoveryEnvironment.merge(captured) { _, restored in restored }
}
let hookStatePath = agentHookStatePath(
sessionStoreSuffix: "hermes-agent",
env: processEnvironment
)
switch HermesLegacySessionIdentityRecovery().resolve(
surfaceID: surfaceUUID,
corruptSessionID: checkpointID,
expectedWorkingDirectory: record.workingDirectory
?? record.launchCommand?.workingDirectory,
hookStateFileURL: URL(fileURLWithPath: hookStatePath),
environment: recoveryEnvironment
) {
case .valid, .legacyRestore, .unavailable:
return record
case .missing:
throw loggedRestoreError(
stage: "hermes.checkpoint.missing",
detail: checkpointID,
message: String(
localized: "cli.restore.error.noRecord",
defaultValue: "restore: this session has nothing to restore. Start the agent again in this terminal."
)
)
case .recovered(let candidate):
return record.repairingHermesCheckpoint(
candidate.sessionID,
fallbackLaunchCommand: candidate.launchCommand
)
}
}
private func currentRestoreSurfaceID(
client: SocketClient,
processEnvironment: [String: String]
) throws -> String? {
// The remote relay and the local CLI do not share a PID namespace.
if client.isRelayBacked {
return try relayRestoreSurfaceID(
client: client,
processEnvironment: processEnvironment
)
}
do {
let payload = try implicitCallerIdentifyResponse(
client: client,
processEnvironment: processEnvironment
)
guard let surfaceID = identifiedCallerSurfaceID(in: payload) else {
throw currentRestoreSurfaceUnknownError()
}
return surfaceID
} catch let error as CLIError {
switch error.v2Code {
case "not_found":
client.close()
throw currentRestoreSurfaceUnknownError()
case "method_not_found", "unrecognized_method":
// These protocol replies were consumed in full, so the socket
// remains synchronized for the legacy discovery request.
return legacyRestoreSurfaceID(
client: client,
workspaceID: nil
)
default:
client.close()
throw error
}
} catch {
client.close()
throw error
}
}
private func relayRestoreSurfaceID(
client: SocketClient,
processEnvironment: [String: String]
) throws -> String? {
let ttyName = resolveCallerDescriptorTTYName()
?? resolveCallerTTYName(includeAmbientTTY: false)
guard let ttyName else { return nil }
let resolution = AgentTTYBindingResolution.reportedTTY.rawValue
let workspaceID = normalizedHandleValue(processEnvironment["CMUX_WORKSPACE_ID"])
var params: [String: Any] = [
"tty_name": ttyName,
"tty_resolution": resolution,
]
if let workspaceID {
// Lets an older app identify this probe as an unsupported
// workspace-only resolution. The authenticated relay rewrites
// aliases and separately stamps its authoritative owner id.
params["workspace_id"] = workspaceID
}
do {
let payload = try client.sendV2(
method: "agent.resolve_delivery_target",
params: params
)
if payload["source"] as? String == "workspace",
payload["surface_id"] == nil || payload["surface_id"] is NSNull,
let resolvedWorkspaceID = normalizedHandleValue(payload["workspace_id"] as? String),
isUUID(resolvedWorkspaceID) {
// Previous app versions ignore the TTY probe and resolve
// only workspace_id. Use their alias-rewritten result to
// scope the legacy terminal list, not the stale remote
// shell environment value that produced the request.
return legacyRestoreSurfaceID(
client: client,
workspaceID: resolvedWorkspaceID
)
}
guard arguments.count == 2, !arguments[1].isEmpty else {
guard payload["source"] as? String == "tty",
payload["tty_resolution"] as? String == resolution,
let resolvedWorkspaceID = normalizedHandleValue(payload["workspace_id"] as? String),
isUUID(resolvedWorkspaceID),
let surfaceID = normalizedHandleValue(payload["surface_id"] as? String),
isUUID(surfaceID) else {
throw currentRestoreSurfaceUnknownError()
}
return surfaceID
} catch let error as CLIError {
switch error.v2Code {
case "not_found":
client.close()
throw currentRestoreSurfaceUnknownError()
case "method_not_found", "unrecognized_method":
guard let workspaceID, isUUID(workspaceID) else { return nil }
return legacyRestoreSurfaceID(
client: client,
workspaceID: workspaceID
)
default:
client.close()
throw error
}
} catch {
client.close()
throw error
}
}
private func legacyRestoreSurfaceID(
client: SocketClient,
workspaceID: String?
) -> String? {
// Prefer the live descriptors. Generic TTY variables can be inherited
// across nested shells, so only dedicated cmux hints are a fallback.
let ttyName = resolveCallerDescriptorTTYName()
?? resolveCallerTTYName(includeAmbientTTY: false)
guard let ttyName,
let binding = uniqueCallerTerminalBindingByTTY(
ttyName: ttyName,
client: client,
workspaceId: workspaceID
) else {
return nil
}
return binding.surfaceId
}
private func currentRestoreSurfaceUnknownError() -> CLIError {
CLIError(
message: String(
localized: "cli.restore.error.currentSurfaceUnknown",
defaultValue: "restore: the current cmux surface could not be identified. Retry from this terminal or pass --surface <id|ref>."
)
)
}
private func restoreSelector(_ arguments: [String]) throws -> RestoreSelector {
if arguments == ["--surface"] {
return RestoreSelector(
surface: nil,
usesCurrentSurface: true,
kind: nil,
checkpointID: nil
)
}
let surfaceOptionCount = arguments.filter { argument in
argument == "--surface" || argument.hasPrefix("--surface=")
}.count
guard surfaceOptionCount <= 1 else {
throw CLIError(message: String(
localized: "cli.restore.usage.surface",
defaultValue: "Usage: cmux restore --surface [id|ref]"
))
}
let (surface, positionalArguments) = parseOption(arguments, name: "--surface")
if surfaceOptionCount == 1 {
guard let surface,
!surface.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw CLIError(message: String(
localized: "cli.restore.usage.surface",
defaultValue: "Usage: cmux restore --surface [id|ref]"
))
}
return RestoreSelector(
surface: arguments[1],
usesCurrentSurface: false,
kind: nil,
checkpointID: nil
)
if positionalArguments.isEmpty {
return RestoreSelector(
surface: surface,
usesCurrentSurface: false,
kind: nil,
checkpointID: nil
)
}
}
guard arguments.count == 2,
!arguments[0].trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
!arguments[1].trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
guard positionalArguments.count == 2,
!positionalArguments[0].trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
!positionalArguments[1].trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw CLIError(message: String(
localized: "cli.restore.usage.positional",
defaultValue: "Usage: cmux restore <kind> <checkpoint-id>"
defaultValue: """
Usage: cmux restore [--surface <id|ref>] <kind> <checkpoint-id>
cmux restore <kind> <checkpoint-id> --surface <id|ref>
cmux restore --surface=<id|ref> <kind> <checkpoint-id>
"""
))
}
return RestoreSelector(
surface: nil,
usesCurrentSurface: true,
kind: arguments[0],
checkpointID: arguments[1]
surface: surface,
usesCurrentSurface: surface == nil,
kind: positionalArguments[0],
checkpointID: positionalArguments[1]
)
}
@@ -288,6 +504,7 @@ extension CMUXCLI {
arguments: arguments,
workingDirectory: object["working_directory"] as? String,
environment: object["environment"] as? [String: String],
verificationHome: object["verification_home"] as? String,
capturedAt: (object["captured_at"] as? NSNumber)?.doubleValue,
source: object["source"] as? String
)
+19
View File
@@ -14,5 +14,24 @@ extension CMUXCLI {
let preparedArgumentsWorkingDirectory: String?
let permissionMode: String?
let legacyCommand: String?
func repairingHermesCheckpoint(
_ checkpointID: String,
fallbackLaunchCommand: AgentLaunchCommand?
) -> RestoreRecord {
RestoreRecord(
mode: mode,
kind: kind,
checkpointID: checkpointID,
source: source,
workingDirectory: workingDirectory,
environment: environment,
launchCommand: launchCommand ?? fallbackLaunchCommand,
preparedArguments: nil,
preparedArgumentsWorkingDirectory: nil,
permissionMode: permissionMode,
legacyCommand: legacyCommand
)
}
}
}
+151 -48
View File
@@ -130,72 +130,70 @@ extension CMUXCLI {
surfaceID: String?,
sessionID: String,
lifecycleID: String,
reconciliationConfirmedSessionEnded: inout Bool,
intentionalOnly: Bool,
sessionRunningExitCode: SSHPTYAttachExitCode = .bridgeClosedSessionRunning
sessionRunningExitCode: SSHPTYAttachExitCode = .bridgeClosedSessionRunning,
reconciliationUnavailableExitCode: SSHPTYAttachExitCode = .retryableTransient
) throws -> Bool {
let reconciliationFailure = "ssh-pty-attach: bridge closed before remote PTY exit could be confirmed"
let response: [String: Any]
do {
var params: [String: Any] = [
"workspace_id": workspaceId,
"session_id": sessionID,
"lifecycle_id": lifecycleID,
"acknowledge_lifecycle_if_session_absent": !intentionalOnly,
]
if let surfaceID {
params["surface_id"] = surfaceID
params["allow_moved_surface"] = true
}
response = try client.sendV2(method: "workspace.remote.pty_sessions", params: params)
} catch {
throw CLIError(
message: "\(reconciliationFailure): \(userFacingRemotePTYErrorMessage(error))",
exitCode: SSHPTYAttachExitCode.retryableTransient
)
var params: [String: Any] = [
"workspace_id": workspaceId,
"session_id": sessionID,
"lifecycle_id": lifecycleID,
"acknowledge_lifecycle_if_session_absent": false,
]
if let surfaceID {
params["surface_id"] = surfaceID
params["allow_moved_surface"] = true
}
let requestedLifecycle = (response["requested_session_lifecycle"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let intentionalCleanup = requestedLifecycle == "intentional_cleanup_requested" ||
requestedLifecycle == "intentionally_closed"
guard let sessions = response["sessions"] as? [[String: Any]] else {
throw CLIError(message: reconciliationFailure, exitCode: SSHPTYAttachExitCode.retryableTransient)
}
let errors: [[String: Any]]
if let rawErrors = response["errors"] {
guard let parsedErrors = rawErrors as? [[String: Any]] else {
throw CLIError(message: reconciliationFailure, exitCode: SSHPTYAttachExitCode.retryableTransient)
}
errors = parsedErrors
} else {
errors = []
}
if !intentionalCleanup, !errors.isEmpty {
throw CLIError(
message: "\(reconciliationFailure)\n\(sshSessionListFailureMessage(errors))",
exitCode: SSHPTYAttachExitCode.retryableTransient
var reconciliation = try requestValidatedSSHPTYReconciliation(
client: client,
params: params,
unavailableExitCode: reconciliationUnavailableExitCode
)
if !intentionalOnly,
!reconciliation.intentionalCleanup,
!reconciliation.sessionIDs.contains(sessionID) {
// Keep the first liveness read side-effect free. Only after its
// response is validated may the server atomically recheck absence
// and acknowledge this exact lifecycle generation.
params["acknowledge_lifecycle_if_session_absent"] = true
reconciliation = try requestValidatedSSHPTYReconciliation(
client: client,
params: params,
unavailableExitCode: reconciliationUnavailableExitCode
)
}
if intentionalOnly, !intentionalCleanup { return false }
if intentionalOnly, !reconciliation.intentionalCleanup { return false }
let sessionStillRunning = sessions.contains {
(($0["session_id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "") == sessionID
}
if !intentionalCleanup, sessionStillRunning {
if !reconciliation.intentionalCleanup,
reconciliation.sessionIDs.contains(sessionID) {
let message: String
if sessionRunningExitCode == .bridgeClosedWithoutProgress {
message = String(
localized: "cli.sshPtyAttach.bridgeClosedWithoutProgress",
defaultValue: "ssh-pty-attach: bridge closed without receiving new output while the remote PTY session is still running"
defaultValue: "ssh-pty-attach: bridge closed without receiving new output while the remote PTY session is still running",
bundle: CLIExecutableLocator.enclosingAppBundle() ?? .main
)
} else if sshPTYAttachWrapperWillRetry(sessionRunningExitCode) {
message = String(
localized: "cli.sshPtyAttach.bridgeClosedSessionRunningReconnecting",
defaultValue: "The SSH terminal connection ended while the remote session is still running; cmux is reconnecting.",
bundle: CLIExecutableLocator.enclosingAppBundle() ?? .main
)
} else {
message = "ssh-pty-attach: bridge closed while remote PTY session is still running"
message = String(
localized: "cli.sshPtyAttach.bridgeClosedSessionRunning",
defaultValue: "The SSH terminal connection ended; the remote session may still be running.",
bundle: CLIExecutableLocator.enclosingAppBundle() ?? .main
)
}
throw CLIError(
message: message,
exitCode: sessionRunningExitCode
)
}
reconciliationConfirmedSessionEnded = true
guard let surfaceID else { return true }
do {
_ = try client.sendV2(method: "workspace.remote.pty_attach_end", params: [
@@ -206,12 +204,117 @@ extension CMUXCLI {
} catch {
throw CLIError(
message: "ssh-pty-attach: remote PTY exited but local session cleanup failed: \(userFacingRemotePTYErrorMessage(error))",
exitCode: SSHPTYAttachExitCode.retryableTransient
exitCode: SSHPTYAttachExitCode.fatal
)
}
return true
}
private func requestValidatedSSHPTYReconciliation(
client: SocketClient,
params: [String: Any],
unavailableExitCode: SSHPTYAttachExitCode
) throws -> (intentionalCleanup: Bool, sessionIDs: [String]) {
let response: [String: Any]
do {
response = try client.sendV2(method: "workspace.remote.pty_sessions", params: params)
} catch {
throw CLIError(
message: sshPTYReconciliationUnavailableMessage(
detail: userFacingRemotePTYErrorMessage(error)
),
exitCode: unavailableExitCode
)
}
return try validatedSSHPTYReconciliation(
response,
unavailableExitCode: unavailableExitCode
)
}
private func validatedSSHPTYReconciliation(
_ response: [String: Any],
unavailableExitCode: SSHPTYAttachExitCode
) throws -> (intentionalCleanup: Bool, sessionIDs: [String]) {
let requestedLifecycle: String?
if let rawRequestedLifecycle = response["requested_session_lifecycle"] {
guard let rawRequestedLifecycle = rawRequestedLifecycle as? String else {
throw CLIError(
message: sshPTYReconciliationUnavailableMessage(detail: nil),
exitCode: unavailableExitCode
)
}
let normalizedLifecycle = rawRequestedLifecycle
.trimmingCharacters(in: .whitespacesAndNewlines)
guard [
"active",
"intentional_cleanup_requested",
"intentionally_closed",
].contains(normalizedLifecycle) else {
throw CLIError(
message: sshPTYReconciliationUnavailableMessage(detail: nil),
exitCode: unavailableExitCode
)
}
requestedLifecycle = normalizedLifecycle
} else {
requestedLifecycle = nil
}
let intentionalCleanup = requestedLifecycle == "intentional_cleanup_requested" ||
requestedLifecycle == "intentionally_closed"
guard let sessions = response["sessions"] as? [[String: Any]] else {
throw CLIError(
message: sshPTYReconciliationUnavailableMessage(detail: nil),
exitCode: unavailableExitCode
)
}
let sessionIDs = sessions.compactMap { session -> String? in
guard let rawSessionID = session["session_id"] as? String else { return nil }
let normalizedSessionID = rawSessionID.trimmingCharacters(in: .whitespacesAndNewlines)
return normalizedSessionID.isEmpty ? nil : normalizedSessionID
}
guard sessionIDs.count == sessions.count else {
throw CLIError(
message: sshPTYReconciliationUnavailableMessage(detail: nil),
exitCode: unavailableExitCode
)
}
let errors: [[String: Any]]
if let rawErrors = response["errors"] {
guard let parsedErrors = rawErrors as? [[String: Any]] else {
throw CLIError(
message: sshPTYReconciliationUnavailableMessage(detail: nil),
exitCode: unavailableExitCode
)
}
errors = parsedErrors
} else {
errors = []
}
if !intentionalCleanup, !errors.isEmpty {
throw CLIError(
message: sshPTYReconciliationUnavailableMessage(
detail: sshSessionListFailureMessage()
),
exitCode: unavailableExitCode
)
}
return (intentionalCleanup, sessionIDs)
}
private func sshPTYReconciliationUnavailableMessage(detail: String?) -> String {
let message = String(
localized: "cli.sshPtyAttach.reconciliationUnavailableReattach",
defaultValue: "The SSH terminal connection ended before the remote session state could be confirmed; preserving the remote session for reconnection.",
bundle: CLIExecutableLocator.enclosingAppBundle() ?? .main
)
guard let detail = detail?.trimmingCharacters(in: .whitespacesAndNewlines),
!detail.isEmpty else {
return message
}
return "\(message): \(detail)"
}
func readSSHPTYBridgeReady(fd: Int32) throws -> (attachmentToken: String, replayBytes: Int) {
let maxStatusBytes = 4096
// Bound only the pre-ready status wait: a bridge that accepts the TCP
+57 -5
View File
@@ -1,19 +1,71 @@
import CmuxFoundation
import Darwin
import Foundation
extension CMUXCLI {
func sshAutoReconnectNoteFormat() -> String {
let status = String(localized: "cli.ssh.autoReconnect.status", defaultValue: "[cmux] ssh exited with status %s; reconnecting (attempt %s/%s).")
let stopHint = String(localized: "cli.ssh.autoReconnect.stopHint", defaultValue: "[cmux] close this pane or press Ctrl-C to stop reconnecting.")
let bundle = CLIExecutableLocator.enclosingAppBundle() ?? .main
let status = String(localized: "cli.ssh.autoReconnect.status", defaultValue: "[cmux] ssh exited with status %s; reconnecting (attempt %s/%s).", bundle: bundle)
let stopHint = String(localized: "cli.ssh.autoReconnect.stopHint", defaultValue: "[cmux] close this pane or press Ctrl-C to stop reconnecting.", bundle: bundle)
return "\\n\\033[33m\(status)\\033[0m\\n\\033[2m\(stopHint)\\033[0m\\n"
}
func sshManualReconnectExitPromptFormat() -> String {
let status = String(localized: "cli.ssh.manualReconnectPrompt.status", defaultValue: "[cmux] ssh exited with status %s.")
let detail = String(localized: "cli.ssh.manualReconnectPrompt.detail", defaultValue: "[cmux] the remote VM may have been paused, destroyed, or lost network.")
let prompt = String(localized: "cli.ssh.manualReconnectPrompt.prompt", defaultValue: "[cmux] press Enter to close this pane. Press r then Enter to reconnect.")
let bundle = CLIExecutableLocator.enclosingAppBundle() ?? .main
let status = String(localized: "cli.ssh.manualReconnectPrompt.status", defaultValue: "[cmux] ssh exited with status %s.", bundle: bundle)
let detail = String(localized: "cli.ssh.manualReconnectPrompt.detail", defaultValue: "[cmux] the SSH connection ended; the remote session may still be running.", bundle: bundle)
let prompt = String(localized: "cli.ssh.manualReconnectPrompt.prompt", defaultValue: "[cmux] press Enter to close this pane. Press r then Enter to reconnect.", bundle: bundle)
return "\\n\\033[31m\(status)\\033[0m\\n\\033[2m\(detail)\\033[0m\\n\\033[2m\(prompt)\\033[0m\\n"
}
func sshTerminalExitPromptFormat() -> String {
let bundle = CLIExecutableLocator.enclosingAppBundle() ?? .main
let status = String(localized: "cli.ssh.manualReconnectPrompt.status", defaultValue: "[cmux] ssh exited with status %s.", bundle: bundle)
let detail = String(localized: "cli.ssh.manualReconnectPrompt.detail", defaultValue: "[cmux] the SSH connection ended; the remote session may still be running.", bundle: bundle)
let prompt = String(localized: "cli.ssh.terminalExitPrompt.prompt", defaultValue: "[cmux] press Enter to close this pane.", bundle: bundle)
return "\\n\\033[31m\(status)\\033[0m\\n\\033[2m\(detail)\\033[0m\\n\\033[2m\(prompt)\\033[0m\\n"
}
/// Waits for a post-failure Enter without accepting queued terminal reports.
func runSSHTerminalExitPrompt(commandArgs _: [String]) {
var original = termios()
guard tcgetattr(STDIN_FILENO, &original) == 0 else {
parkSSHTerminalExitPromptAfterEOF()
}
var promptMode = original
cfmakeraw(&promptMode)
promptMode.c_lflag |= tcflag_t(ISIG)
// Changing mode and flushing are one terminal operation: no byte queued
// before this prompt boundary can later be mistaken for a fresh Enter.
guard tcsetattr(STDIN_FILENO, TCSAFLUSH, &promptMode) == 0 else {
parkSSHTerminalExitPromptAfterEOF()
}
defer { _ = tcsetattr(STDIN_FILENO, TCSANOW, &original) }
var inputFilter = SSHTerminalExitPromptInputFilter()
var buffer = [UInt8](repeating: 0, count: 256)
while true {
let count = Darwin.read(STDIN_FILENO, &buffer, buffer.count)
if count > 0 {
if inputFilter.consume(Data(buffer.prefix(count))) {
return
}
} else if count == 0 {
parkSSHTerminalExitPromptAfterEOF()
} else if errno != EINTR {
parkSSHTerminalExitPromptAfterEOF()
}
}
}
/// Keeps a dead input bridge from dismissing the pane while remaining signal-interruptible.
private func parkSSHTerminalExitPromptAfterEOF() -> Never {
while true {
_ = Darwin.pause()
}
}
func sshRemoteReconnectShellFunction() -> String {
[
"cmux_ssh_remote_reconnect() {",
+27 -3
View File
@@ -301,7 +301,17 @@ extension CMUXCLI {
let trimmedOneTimeCommand = oneTimeCommand?.trimmingCharacters(in: .whitespacesAndNewlines)
let hasOneTimeCommand = trimmedOneTimeCommand?.isEmpty == false
let authRetryPolicy = SSHForegroundAuthenticationRetryPolicy()
let authenticationResult = authRetryPolicy.persistentAuthenticationResultShellLine(
variablePrefix: "cmux_ssh",
terminalFailureCommand: "break"
)
let backoffBuilder = SSHRetryBackoffScriptBuilder(context: .startup)
let terminalModeReset = shellQuote(SSHTerminalModeResetSequence().shellPrintfFormat)
let terminalExitPrompt = shellQuote(sshTerminalExitPromptFormat())
let terminalExitPromptCommand = [
shellQuote(resolvedExecutableURL()?.path ?? (args.first ?? "cmux")),
"__ssh-terminal-exit-prompt",
].joined(separator: " ")
var scriptLines: [String] = []
if !shellFeaturesBootstrap.isEmpty {
scriptLines.append(shellFeaturesBootstrap)
@@ -363,11 +373,13 @@ extension CMUXCLI {
] + reconnectConfiguration + [
"cmux_ssh_retry=0",
"cmux_ssh_auth_retry_limit=\(authRetryPolicy.maximumConsecutiveTransientFailures); cmux_ssh_auth_retry=0",
"cmux_ssh_auth_succeeded=0",
// Initial transient foreground-auth failures are a reconnect phase, so boot-time outages share this loop.
"cmux_ssh_reauth_required=\(hasOneTimeCommand ? 1 : 0)",
"CMUX_SSH_CHILD_PID=; CMUX_SSH_AUTH_PID=; CMUX_SSH_PENDING_SIGNAL=; CMUX_SSH_PENDING_SIGNAL_NAME=",
] + backoffBuilder.stateInitializationLines + [
"cmux_ssh_note() { if [ -t 2 ]; then printf \"$@\" >&2 || true; fi; }",
"cmux_ssh_reset_terminal_modes() { if [ -t 2 ]; then printf \(terminalModeReset) >&2 || true; fi; }",
"cmux_ssh_register_attempt() { \(lifecycleLaunching); }",
"cmux_ssh_begin_attempt() { CMUX_SSH_ATTEMPT_ID=$(/usr/bin/uuidgen | /usr/bin/tr '[:upper:]' '[:lower:]') || return 1; export CMUX_SSH_ATTEMPT_ID; cmux_ssh_attempt_registration_retry=0; while ! cmux_ssh_register_attempt; do cmux_ssh_attempt_registration_retry=$((cmux_ssh_attempt_registration_retry + 1)); if [ \"$cmux_ssh_attempt_registration_retry\" -ge 3 ]; then return 1; fi; /bin/sleep 0.1; done; }",
"cmux_ssh_session_end() { if [ \"${CMUX_SSH_SESSION_ENDED:-0}\" = 1 ]; then return; fi; CMUX_SSH_SESSION_ENDED=1; cmux_ssh_cleanup_password; \(lifecycleCleanup); }",
@@ -382,7 +394,7 @@ extension CMUXCLI {
]
if hasOneTimeCommand {
scriptLines.append(" if [ \"$cmux_ssh_reauth_required\" -eq 1 ]; then")
scriptLines += [" ( cmux_ssh_foreground_auth ) <&0 &", " CMUX_SSH_AUTH_PID=$!; if [ -n \"${CMUX_SSH_PENDING_SIGNAL:-}\" ]; then cmux_ssh_signal_exit \"$CMUX_SSH_PENDING_SIGNAL\" \"${CMUX_SSH_PENDING_SIGNAL_NAME:-TERM}\"; fi; wait \"$CMUX_SSH_AUTH_PID\"; cmux_ssh_status=$?; CMUX_SSH_AUTH_PID=; case \"$cmux_ssh_status\" in 129|130|143) cmux_ssh_retire_for_signal \"$cmux_ssh_status\" ;; esac; if [ -n \"${CMUX_SSH_PENDING_SIGNAL:-}\" ]; then cmux_ssh_session_end; trap - EXIT HUP INT TERM; exit \"$CMUX_SSH_PENDING_SIGNAL\"; fi", " if [ \"$cmux_ssh_status\" -eq 0 ]; then cmux_ssh_reauth_required=0; cmux_ssh_auth_retry=0; else case \"$cmux_ssh_status\" in 254) cmux_ssh_auth_retry=$((cmux_ssh_auth_retry + 1)); if [ \"$cmux_ssh_auth_retry\" -ge \"$cmux_ssh_auth_retry_limit\" ]; then cmux_ssh_status=255; break; fi ;; \(authRetryPolicy.unclassifiedFailureExitStatus)) cmux_ssh_status=255; break ;; *) break ;; esac; fi", " fi", " if [ \"$cmux_ssh_reauth_required\" -eq 0 ]; then"]
scriptLines += [" ( cmux_ssh_foreground_auth ) <&0 &", " CMUX_SSH_AUTH_PID=$!; if [ -n \"${CMUX_SSH_PENDING_SIGNAL:-}\" ]; then cmux_ssh_signal_exit \"$CMUX_SSH_PENDING_SIGNAL\" \"${CMUX_SSH_PENDING_SIGNAL_NAME:-TERM}\"; fi; wait \"$CMUX_SSH_AUTH_PID\"; cmux_ssh_status=$?; CMUX_SSH_AUTH_PID=; case \"$cmux_ssh_status\" in 129|130|143) cmux_ssh_retire_for_signal \"$cmux_ssh_status\" ;; esac; if [ -n \"${CMUX_SSH_PENDING_SIGNAL:-}\" ]; then cmux_ssh_session_end; trap - EXIT HUP INT TERM; exit \"$CMUX_SSH_PENDING_SIGNAL\"; fi", " \(authenticationResult)", " fi", " if [ \"$cmux_ssh_reauth_required\" -eq 0 ]; then"]
}
if let trimmedControlPathPreflight, !trimmedControlPathPreflight.isEmpty,
!hasOneTimeCommand {
@@ -419,6 +431,7 @@ extension CMUXCLI {
" cmux_ssh_status=$?",
" CMUX_SSH_CHILD_PID=",
" if [ \"$cmux_ssh_status\" -eq 0 ]; then break; fi",
" cmux_ssh_reset_terminal_modes",
" case \"$cmux_ssh_status\" in \(retryableStatusPattern)) ;; *) break ;; esac",
]
if retryPTYAttachStatus {
@@ -436,6 +449,7 @@ extension CMUXCLI {
scriptLines.append(retryLimitCondition)
scriptLines += [
" cmux_ssh_retry=$((cmux_ssh_retry + 1))",
" \(backoffBuilder.terminalInputModeResetLine)",
" cmux_ssh_note '\\n\\033[33m[cmux] ssh exited with status %s; reconnecting (attempt %s/%s).\\033[0m\\n\\033[2m[cmux] close this pane or press Ctrl-C to stop reconnecting.\\033[0m\\n' \"$cmux_ssh_status\" \"$cmux_ssh_retry\" \"$cmux_ssh_reconnect_limit\"",
]
scriptLines += backoffBuilder.waitLines
@@ -448,8 +462,18 @@ extension CMUXCLI {
"trap - EXIT HUP INT TERM",
"cmux_ssh_session_end",
"if [ \"$cmux_ssh_status\" -ne 0 ]; then",
" printf '\\n\\033[31m[cmux] ssh exited with status %s.\\033[0m\\n\\033[2m[cmux] the remote VM may have been paused, destroyed, or lost network.\\033[0m\\n\\033[2m[cmux] press Enter to close this pane.\\033[0m\\n' \"$cmux_ssh_status\" >&2 || true",
" IFS= read -r _cmux_dismiss_key 2>/dev/null || true",
" \(backoffBuilder.terminalInputModeResetLine)",
" cmux_ssh_prompt_tty_state=$(/bin/stty -g <&0 2>/dev/null || true)",
" cmux_ssh_prompt_restore_tty() { if [ -n \"${cmux_ssh_prompt_tty_state:-}\" ]; then /bin/stty \"$cmux_ssh_prompt_tty_state\" <&0 2>/dev/null || true; cmux_ssh_prompt_tty_state=; fi; }",
" cmux_ssh_prompt_signal_exit() { cmux_ssh_prompt_signal_status=\"$1\"; cmux_ssh_prompt_restore_tty; trap - EXIT HUP INT TERM; exit \"$cmux_ssh_prompt_signal_status\"; }",
" trap 'cmux_ssh_prompt_restore_tty' EXIT",
" trap 'cmux_ssh_prompt_signal_exit 129' HUP",
" trap 'cmux_ssh_prompt_signal_exit 130' INT",
" trap 'cmux_ssh_prompt_signal_exit 143' TERM",
" printf \(terminalExitPrompt) \"$cmux_ssh_status\" >&2 || true",
" if [ -t 0 ]; then \(terminalExitPromptCommand) <&0; else exec \(terminalExitPromptCommand) <&0; fi",
" cmux_ssh_prompt_restore_tty",
" trap - EXIT HUP INT TERM",
"fi",
"exit $cmux_ssh_status",
]
+35 -15
View File
@@ -11,6 +11,11 @@ extension CMUXCLI {
let surfaceId: String?
}
struct ClaudeTeamsShimPlan {
let directory: URL
let managedClaudeWrapperURL: URL?
}
func tmuxCompatResolvedSocketPath(processEnvironment: [String: String]) throws -> String {
let envSocketPath = try CLISocketEnvironment.socketPath(in: processEnvironment)
let bundleIdentifier = CLISocketPathResolver.currentAppBundleIdentifier()
@@ -19,21 +24,29 @@ extension CMUXCLI {
environment: processEnvironment
)
let source: CLISocketPathSource
if let envSocketPath {
source = CLISocketPathResolver.isImplicitDefaultPath(
envSocketPath,
bundleIdentifier: bundleIdentifier,
environment: processEnvironment
) ? .implicitDefault : .environment
if envSocketPath != nil {
// Environment overrides are explicit pins. Never reinterpret a
// stable-looking value as permission to select another instance.
source = .environment
} else {
source = .implicitDefault
}
return CLISocketPathResolver.resolve(
requestedPath: requestedSocketPath,
source: source,
let resolver = CLISocketPathResolver(
environment: processEnvironment,
bundleIdentifier: bundleIdentifier
)
let resolution = resolver.resolve(
requestedPath: requestedSocketPath,
source: source
)
guard resolution.hasLiveSocket else {
throw CLIError(message: resolution.failureMessage)
}
if source == .implicitDefault,
let rerouteNotice = resolution.rerouteNotice {
cliWriteStderr(rerouteNotice + "\n")
}
return resolution.selectedPath ?? requestedSocketPath
}
func tmuxCompatLaunchContext(
@@ -199,11 +212,11 @@ extension CMUXCLI {
return environment
}
func createClaudeTeamsShimDirectory(
func createClaudeTeamsShimPlan(
processEnvironment: [String: String],
commandArgs: [String],
launchContext: TmuxCompatLaunchContext?
) throws -> URL {
) throws -> ClaudeTeamsShimPlan {
let downstreamTmuxMissing = String(
localized: "cli.tmux-compat.error.downstreamTmuxMissing",
defaultValue: "cmux tmux shim: no downstream tmux executable found"
@@ -259,7 +272,11 @@ extension CMUXCLI {
script,
to: managedRoot.appendingPathComponent("tmux", isDirectory: false)
)
return managedRoot
return ClaudeTeamsShimPlan(
directory: managedRoot,
managedClaudeWrapperURL: managedRoot
.appendingPathComponent("claude", isDirectory: false)
)
} catch {
// Informational launches do not create teammates, so they may use the
// launcher-only compatibility directory below. Real Teams sessions must
@@ -270,9 +287,12 @@ extension CMUXCLI {
guard claudeTeamsIsNonLaunchInvocation(commandArgs: commandArgs) else {
throw CLIError(message: managedTerminalRequiredMessage(displayName: "Claude Teams"))
}
return try createTmuxCompatShimDirectory(
directoryName: "claude-teams-bin",
tmuxShimScript: script
return ClaudeTeamsShimPlan(
directory: try createTmuxCompatShimDirectory(
directoryName: "claude-teams-bin",
tmuxShimScript: script
),
managedClaudeWrapperURL: nil
)
}
+37 -21
View File
@@ -1,3 +1,4 @@
import CMUXAgentLaunch
import Foundation
extension CMUXCLI {
@@ -76,26 +77,31 @@ extension CMUXCLI {
/// the pane exits before the real command runs; that is why Claude Code
/// 2.1.183 teammates never opened a split pane (issue #6447).
///
/// Every command is run through `/bin/sh -c '<command>'`, so Ghostty execs a
/// shell rather than a builtin/expression/assignment-prefix. The whole command
/// is single-quoted, so it round-trips verbatim regardless of operators or
/// quoting there is no attempt to classify which commands "need" a shell,
/// which was unreliable (tmux shell-commands can hide operators with no
/// surrounding whitespace). Commands that are already a shell invocation (e.g.
/// OMO's `/bin/sh -c ""`) are simply run through one more shell, which execs
/// straight into them.
/// Every command is run through `/bin/sh -lc '<command>'`, so Ghostty execs a
/// login shell rather than a builtin/expression/assignment-prefix. The `-l`
/// is important: Ghostty's `exec -l` only changes argv[0], and does not make
/// macOS `/bin/sh` read `/etc/profile` when it is given a non-interactive `-c`
/// command. The login shell therefore runs `path_helper` and restores the
/// user's full login PATH before the command starts (issue #10189). The whole
/// command is single-quoted, so it round-trips verbatim regardless of
/// operators or quoting there is no attempt to classify which commands
/// "need" a shell, which was unreliable (tmux shell-commands can hide
/// operators with no surrounding whitespace). Commands that are already a
/// shell invocation (e.g. OMO's `/bin/sh -c ""`) are simply run through one
/// more shell, which execs straight into them.
///
/// A POSIX shell (`/bin/sh`) is used deliberately rather than the user's
/// `$SHELL`: the commands being wrapped are POSIX `sh` syntax (Claude Code's
/// `cd && env `, and the no-command fallback `exec ${SHELL:-/bin/sh} -l`),
/// and `csh`/`tcsh` login shells cannot parse `${VAR:-default}` parameter
/// expansion or `NAME=value` command prefixes. `/bin/sh` is always present and
/// runs the bodies correctly for every user. `-l` is not passed (`/bin/sh`
/// does not take it); on macOS Ghostty already supplies a login-style argv0.
/// and `csh`/`tcsh` cannot parse `${VAR:-default}` parameter expansion or
/// `NAME=value` command prefixes. `/bin/sh` is always present and runs the
/// bodies correctly for every user; its login mode is a shell-independent way to
/// invoke macOS `path_helper` without asking the user's shell to parse a
/// POSIX command body.
func tmuxShellInvokedStartCommand(_ command: String) -> String {
let trimmed = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return command }
return "/bin/sh -c \(tmuxShellQuote(trimmed))"
return "/bin/sh -lc \(tmuxShellQuote(trimmed))"
}
/// Like `tmuxShellInvokedStartCommand`, but first exports `prependEnv` inside
@@ -122,11 +128,14 @@ extension CMUXCLI {
///
/// Teammate panes are respawned by cmux's surface layer, not by `cmux
/// claude-teams`, so they do NOT inherit the launcher environment the lead
/// got from `configureClaudeTeamsEnvironment`. The one variable that matters
/// for startup is `CLAUDE_CODE_SANDBOXED`: Claude Code short-circuits its
/// interactive "Do you trust this folder?" gate on it, and a teammate that
/// hits that gate hangs forever (its pane opens but it never checks in
/// issue #6447). Re-supply it so teammates start the same way the lead does.
/// got from `configureClaudeTeamsEnvironment`. The launcher records a
/// replay-safe snapshot in
/// ``ClaudeTeamsRespawnEnvironmentTransport/environmentKey``;
/// re-supply that snapshot so PATH-based tools and allowlisted Claude
/// configuration match the lead without copying secrets or surface identity.
/// `CLAUDE_CODE_SANDBOXED` is handled alongside it: Claude Code short-circuits
/// its interactive "Do you trust this folder?" gate on that variable, and a
/// teammate that hits the gate hangs forever (issue #6447).
///
/// That trust gate is a real safety boundary, so it is only waived when the
/// user already opted into skipping safety prompts. The opt-in is NOT inferred
@@ -146,10 +155,17 @@ extension CMUXCLI {
/// it correctly falls back to Claude's trust prompt rather than silently bypassing
/// the trust boundary outside an explicit opt-in.
func tmuxClaudeTeamsRespawnEnvironment() -> [(key: String, value: String)] {
guard ProcessInfo.processInfo.environment["CMUX_CLAUDE_TEAMS_SANDBOXED"] == "1" else {
return []
let processEnvironment = ProcessInfo.processInfo.environment
let transport = ClaudeTeamsRespawnEnvironmentTransport()
var environment = transport.decodedEnvironment(
from: processEnvironment[ClaudeTeamsRespawnEnvironmentTransport.environmentKey]
)
if processEnvironment["CMUX_CLAUDE_TEAMS_SANDBOXED"] == "1" {
environment["CLAUDE_CODE_SANDBOXED"] = "1"
}
return environment.keys.sorted().compactMap { key in
environment[key].map { (key: key, value: $0) }
}
return [(key: "CLAUDE_CODE_SANDBOXED", value: "1")]
}
func tmuxShellWords(_ commandText: String) -> [String] {
+193 -29
View File
@@ -17,11 +17,49 @@ import Foundation
/// request and unknown / future event names default to non-actionable
/// telemetry that never notifies. Conflating a tool-start with an approval
/// is the bug behind https://github.com/manaflow-ai/cmux/issues/4985.
/// Resolved user-attention outcome for one raw agent hook event on the
/// Feed bridge path.
struct FeedEventClassification: Equatable {
/// The wire `hook_event_name` forwarded to the app via `feed.push`.
let hookEventName: String
/// Whether the Feed bridge blocks waiting for a user decision (and the
/// app may post an actionable approval card + banner).
let isActionable: Bool
/// The agent is blocked waiting for the user in its OWN approval UI
/// (e.g. Codex's approval reviewer). The feed event stays non-actionable
/// telemetry cmux must not double-prompt but the hook bridge must
/// raise the "Agent Needs Permission"-gated notification, or the blocked
/// agent is silent indefinitely.
/// https://github.com/manaflow-ai/cmux/issues/9592
let notifiesNativeApprovalPrompt: Bool
/// A tool COMPLETED for an agent whose approval prompts notify via
/// ``notifiesNativeApprovalPrompt`` execution strictly follows any
/// approval, so the prompt resolved (approved by the user or by the
/// agent's own auto-reviewer) and the bridge clears the pane's stale
/// notifications. Only tool COMPLETION qualifies: pre-tool events fire
/// when the agent intends to run a tool, with no ordering guarantee
/// against the approval-prompt hook, so clearing there could erase a
/// just-raised prompt while the agent is still blocked.
///
/// The clear is deliberately pane-wide and uncorrelated with any single
/// request: notifications carry no request identity anywhere in cmux,
/// and every agent integration clears the same way on progress signals
/// Claude's `session-start`/`prompt-submit`/`pre-tool-use` hooks, the
/// generic `.approvalResponse` action (Hermes' resolved native
/// approvals), and codex's own `prompt-submit` hook (which also clears
/// deny-without-further-tools residue at the next turn). Pane
/// notifications are attention signals; agent progress in the pane makes
/// them stale as a set.
let clearsNativeApprovalPrompt: Bool
}
struct FeedEventClassifier {
/// Classifies a raw agent hook event into our wire `hook_event_name`
/// plus an `isActionable` flag that drives whether the Feed bridge
/// blocks waiting for a user decision (and whether `FeedCoordinator`
/// posts a "needs approval" notification).
/// posts a "needs approval" notification), plus whether the bridge
/// itself must raise the permission-prompt notification for an agent
/// that blocks in its own native approval UI.
///
/// - Parameters:
/// - source: The agent id that emitted the event (`claude`, `codex`,
@@ -29,17 +67,21 @@ struct FeedEventClassifier {
/// - event: The agent's raw hook event name.
/// - toolName: The tool the event refers to, used only for the two
/// tool-dependent semantics.
/// - Returns: The wire `hook_event_name` and whether the event is
/// Feed-actionable (blocks + may notify).
static func classify(
source: String,
event: String,
toolName: String
) -> (String, Bool) {
) -> FeedEventClassification {
let semantic = feedEventSemantic(source: source, event: event)
return wireMapping(for: semantic, source: source, toolName: toolName)
}
/// Whether any of `source`'s registered events carry the
/// ``FeedEventSemantic/nativeApprovalPrompt`` semantic.
private static func sourceRaisesNativeApprovalPrompts(_ source: String) -> Bool {
feedEventSemanticRegistry[source]?.values.contains(.nativeApprovalPrompt) == true
}
/// User-attention semantic of a hook/feed event, independent of the
/// agent-specific raw event name. Notifications and blocking waits are
/// keyed off this never off raw event-name string matching so the
@@ -54,6 +96,14 @@ struct FeedEventClassifier {
/// only. Used by agents that expose a *separate* approval event
/// (Claude, Codex, Hermes) so their pre-tool hook never escalates.
case toolStart
/// The agent is blocked waiting for the user in its OWN approval
/// UI (e.g. Codex's approval reviewer). The feed event stays
/// non-actionable telemetry an actionable cmux Feed card would
/// compete with the agent's native prompt and bypass features like
/// Codex's "Approve for me" but the bridge raises the
/// "Agent Needs Permission"-gated notification so the blocked
/// agent is not silent (#9592).
case nativeApprovalPrompt
/// A tool is about to run and the agent has *no* dedicated approval
/// event, so a side-effecting tool is escalated to an approval and
/// read-only tools stay telemetry. Resolved against the tool name.
@@ -96,11 +146,11 @@ struct FeedEventClassifier {
/// Tool names that carry their own dedicated approval wire event rather
/// than the generic `PermissionRequest`. Returns the actionable wire
/// mapping for such a tool, or `nil` for ordinary tools.
private static func dedicatedApprovalEvent(for toolName: String) -> (String, Bool)? {
/// event name for such a tool, or `nil` for ordinary tools.
private static func dedicatedApprovalEvent(for toolName: String) -> String? {
switch toolName {
case "ExitPlanMode": return ("ExitPlanMode", true)
case "AskUserQuestion": return ("AskUserQuestion", true)
case "ExitPlanMode": return "ExitPlanMode"
case "AskUserQuestion": return "AskUserQuestion"
default: return nil
}
}
@@ -112,50 +162,93 @@ struct FeedEventClassifier {
for semantic: FeedEventSemantic,
source: String,
toolName: String
) -> (String, Bool) {
) -> FeedEventClassification {
switch semantic {
case .approvalRequest:
return dedicatedApprovalEvent(for: toolName) ?? ("PermissionRequest", true)
return actionable(dedicatedApprovalEvent(for: toolName) ?? "PermissionRequest")
case .toolStartMaybeApproval:
if let dedicated = dedicatedApprovalEvent(for: toolName) {
return dedicated
return actionable(dedicated)
}
// Any tool that can mutate the environment surfaces as a
// permission request so the user can approve/deny from the
// Feed sidebar. Read-only tools stay non-actionable
// telemetry so we don't flood the Actionable view.
if Self.isSideEffectingTool(toolName, source: source) {
return ("PermissionRequest", true)
return actionable("PermissionRequest")
}
return ("PreToolUse", false)
return telemetry("PreToolUse")
case .toolStart:
return ("PreToolUse", false)
// Never clears the native approval prompt: agents fire their
// pre-tool hooks when they INTEND to run a tool, with no ordering
// guarantee against the approval-prompt hook, so a start-time
// clear could erase a just-raised prompt while the agent is still
// blocked reintroducing the silence behind #9592.
return telemetry("PreToolUse")
case .nativeApprovalPrompt:
// Same telemetry wire mapping as .toolStart (no blocking, no
// actionable card), plus the permission-prompt notification.
return FeedEventClassification(
hookEventName: "PreToolUse",
isActionable: false,
notifiesNativeApprovalPrompt: true,
clearsNativeApprovalPrompt: false
)
case .toolEnd:
return ("PostToolUse", false)
// A completed tool ran, and execution strictly follows any
// approval so this is the earliest progress signal that can
// safely clear a resolved native approval prompt (approved by
// the user or by the agent's own auto-reviewer). Scoped to
// sources that raise those prompts so other agents' tool
// telemetry never touches the notification queue.
return FeedEventClassification(
hookEventName: "PostToolUse",
isActionable: false,
notifiesNativeApprovalPrompt: false,
clearsNativeApprovalPrompt: sourceRaisesNativeApprovalPrompts(source)
)
case .preCompact:
return ("PreCompact", false)
return telemetry("PreCompact")
case .postCompact:
return ("PostCompact", false)
return telemetry("PostCompact")
case .promptSubmit:
return ("UserPromptSubmit", false)
return telemetry("UserPromptSubmit")
case .subagentStart:
return ("SubagentStart", false)
return telemetry("SubagentStart")
case .response:
return ("Stop", false)
return telemetry("Stop")
case .subagentResponse:
return ("SubagentStop", false)
return telemetry("SubagentStop")
case .sessionStart:
return ("SessionStart", false)
return telemetry("SessionStart")
case .sessionEnd:
return ("SessionEnd", false)
return telemetry("SessionEnd")
case .statusNotification:
return ("Notification", false)
return telemetry("Notification")
case .unknown:
// Safe default: telemetry, no approval, no notification.
return ("PreToolUse", false)
return telemetry("PreToolUse")
}
}
private static func actionable(_ hookEventName: String) -> FeedEventClassification {
FeedEventClassification(
hookEventName: hookEventName,
isActionable: true,
notifiesNativeApprovalPrompt: false,
clearsNativeApprovalPrompt: false
)
}
private static func telemetry(_ hookEventName: String) -> FeedEventClassification {
FeedEventClassification(
hookEventName: hookEventName,
isActionable: false,
notifiesNativeApprovalPrompt: false,
clearsNativeApprovalPrompt: false
)
}
/// Per-agent event-semantic tables. Each entry is the source of truth
/// for that agent's `(event) -> semantic` mapping; events absent here
/// resolve to ``FeedEventSemantic/unknown``.
@@ -181,10 +274,12 @@ struct FeedEventClassifier {
],
"codex": [
// Codex runs PermissionRequest hooks before its own approval
// reviewer. Treat this as telemetry so "Approve for me" can still
// use Codex's auto-review path instead of blocking on cmux Feed.
"PermissionRequest": .toolStart,
"permission_request": .toolStart,
// reviewer. Keep the feed side telemetry so "Approve for me" can
// still use Codex's auto-review path instead of blocking on cmux
// Feed, but raise the permission-prompt notification: this is the
// only hook Codex fires while blocked on the user (#9592).
"PermissionRequest": .nativeApprovalPrompt,
"permission_request": .nativeApprovalPrompt,
"PreToolUse": .toolStart,
"pre_tool_use": .toolStart,
"beforeShellExecution": .toolStart,
@@ -338,6 +433,75 @@ struct FeedEventClassifier {
"generate_image",
]
/// Builds the pane-attention V1 socket command a classified feed event
/// carries the `needs-permission`-gated `notify_target_async` for a
/// native approval prompt, or the pane-scoped `clear_notifications` for
/// a resolved one. Pure so the exact wire command (UUID gating, payload
/// shape, gate meta) is unit-testable; the CLI feed hook sends the
/// returned line request/response and awaits the app's acknowledgement.
///
/// Returns `nil` when the classification carries no attention side
/// effect or when either identity is missing/not a UUID: the command is
/// advisory and must never fail the hook.
///
/// The notification body deliberately names only the TOOL mirroring
/// the in-app Feed approval banner (`feed.notification.permission.body`)
/// and never the tool input: commands can embed credentials, and
/// notification banners reach lock screens, paired phones, and the
/// recorded notification history.
static func nativeApprovalPromptAttentionCommand(
classification: FeedEventClassification,
displayName: String,
toolName: String,
workspaceId: String?,
surfaceId: String?
) -> String? {
guard classification.notifiesNativeApprovalPrompt
|| classification.clearsNativeApprovalPrompt else { return nil }
guard let workspaceRaw = workspaceId?.trimmingCharacters(in: .whitespacesAndNewlines),
let workspaceUUID = UUID(uuidString: workspaceRaw),
let surfaceRaw = surfaceId?.trimmingCharacters(in: .whitespacesAndNewlines),
let surfaceUUID = UUID(uuidString: surfaceRaw)
else { return nil }
if classification.clearsNativeApprovalPrompt {
return "clear_notifications --tab=\(workspaceUUID.uuidString) --panel=\(surfaceUUID.uuidString)"
}
let subtitle = String(
localized: "agent.generic.notification.subtitle.permission",
defaultValue: "Permission"
)
let sanitizedToolName = attentionNotificationField(toolName)
let body: String
if sanitizedToolName.isEmpty {
body = String(
localized: "agent.generic.notification.body.approvalNeeded",
defaultValue: "Approval needed"
)
} else {
body = String(
localized: "feed.notification.permission.body",
defaultValue: "\(sanitizedToolName) needs approval"
)
}
guard let meta = AgentHookNotifyCategory.needsPermission.metaSegment(pending: false) else {
return nil
}
let payload = [attentionNotificationField(displayName), attentionNotificationField(subtitle), attentionNotificationField(body)]
.joined(separator: "|") + "|" + meta
return "notify_target_async \(workspaceUUID.uuidString) \(surfaceUUID.uuidString) \(payload)"
}
/// Notification payload fields are pipe-delimited single lines; agent
/// tool names are payload-controlled input, so normalize them the same
/// way `notificationPayload` sanitizes its fields.
private static func attentionNotificationField(_ value: String) -> String {
value
.components(separatedBy: .newlines)
.joined(separator: " ")
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "|", with: "¦")
}
/// Whether a tool mutates state and deserves an approval prompt. Exact
/// match against ``sideEffectingTools`` for every source; the `kiro`
/// source additionally matches its case-insensitive internal aliases.
+22 -209
View File
@@ -1,22 +1,13 @@
import CmuxFoundation
import Darwin
import Foundation
final class SSHPTYAttachReconnectInputFilter {
private static let escape: UInt8 = 0x1B
private static let bell: UInt8 = 0x07
private static let leftBracket: UInt8 = 0x5B
private static let rightBracket: UInt8 = 0x5D
private static let backslash: UInt8 = 0x5C
private static let semicolon: UInt8 = 0x3B
private static let questionMark: UInt8 = 0x3F
private static let dollar: UInt8 = 0x24
private static let maxPendingProbeBytes = 512
// Terminal ESC disambiguation: bounded so a literal Escape key is not held indefinitely.
private static let pendingProbeContinuationTimeoutMilliseconds: Int32 = 25
private static let reconnectProbeDeadlineMilliseconds: Int64 = 2_000
private var isFiltering: Bool
private var pending = [UInt8]()
private var byteFilter: SSHPTYReconnectInputByteFilter
private let deadlineReached: (@Sendable () -> Bool)?
private let remainingDeadline: (@Sendable () -> Int64?)?
@@ -25,14 +16,13 @@ final class SSHPTYAttachReconnectInputFilter {
deadlineReached: (@Sendable () -> Bool)? = nil,
remainingDeadlineMilliseconds: (@Sendable () -> Int64?)? = nil
) {
isFiltering = enabled
byteFilter = SSHPTYReconnectInputByteFilter(enabled: enabled)
self.deadlineReached = deadlineReached
remainingDeadline = remainingDeadlineMilliseconds
}
private init(state: SSHPTYAttachReconnectInputFilterState) {
isFiltering = state.isFiltering
pending = state.pending
byteFilter = SSHPTYReconnectInputByteFilter(enabled: state.isFiltering)
deadlineReached = state.deadlineReached
remainingDeadline = state.remainingDeadlineMilliseconds
}
@@ -52,7 +42,6 @@ final class SSHPTYAttachReconnectInputFilter {
let filterState = filterEnabled
? SSHPTYAttachReconnectInputFilterState(
isFiltering: true,
pending: [],
deadlineReached: { deadline.map { now() >= $0 } ?? false },
remainingDeadlineMilliseconds: {
guard let deadline else { return nil }
@@ -180,10 +169,10 @@ final class SSHPTYAttachReconnectInputFilter {
return true
}
func flushPendingThenShutdown() async {
if let filter = reconnectInputFilter, filter.hasPendingInput {
_ = await writeOrShutdown(filter.flushPendingInput())
}
func finishStdin() {
// Reconnect input can disappear with the old bridge during wake.
// It is not an intentional EOF for the newly attached remote PTY.
guard reconnectInputFilter == nil else { return }
_ = shutdown(fd, SHUT_WR)
}
@@ -210,7 +199,7 @@ final class SSHPTYAttachReconnectInputFilter {
stopSignalFD: stopSignalFD,
timeoutMilliseconds: timeoutMilliseconds
) else {
await flushPendingThenShutdown()
finishStdin()
return
}
@@ -222,13 +211,13 @@ final class SSHPTYAttachReconnectInputFilter {
stopSignalFD: nil,
timeoutMilliseconds: pendingProbeContinuationTimeoutMilliseconds
) else {
await flushPendingThenShutdown()
finishStdin()
return
}
if pendingReadiness.inputReady {
readiness = (inputReady: true, stopRequested: true)
} else if let filter = reconnectInputFilter {
guard await writeOrShutdown(filter.flushPendingInput()) else { return }
guard await writeOrShutdown(filter.stopFiltering()) else { return }
guard stopReconnectFiltering() else { return }
continue
}
@@ -244,7 +233,7 @@ final class SSHPTYAttachReconnectInputFilter {
if !readiness.inputReady {
if let filter = reconnectInputFilter,
filter.hasPendingInput {
guard await writeOrShutdown(filter.flushPendingInput()) else { return }
guard await writeOrShutdown(filter.stopFiltering()) else { return }
}
continue
}
@@ -273,227 +262,51 @@ final class SSHPTYAttachReconnectInputFilter {
}
}
} else if count == 0 {
await flushPendingThenShutdown()
finishStdin()
return
} else if errno != EINTR {
await flushPendingThenShutdown()
finishStdin()
return
}
}
}
func filter(_ data: Data) -> Data {
guard isFiltering, !data.isEmpty else {
return data
}
if isDeadlineReached {
var output = stopFiltering()
output.append(data)
return output
}
var bytes = pending
pending.removeAll(keepingCapacity: true)
bytes.append(contentsOf: data)
var output = Data()
var index = 0
while index < bytes.count {
guard bytes[index] == Self.escape else {
isFiltering = false
output.append(contentsOf: bytes[index...])
return output
}
switch Self.reconnectProbeReplySequence(in: bytes, at: index) {
case .strip(let length):
index += length
case .incomplete:
let suffix = bytes[index...]
guard suffix.count <= Self.maxPendingProbeBytes else {
isFiltering = false
output.append(contentsOf: suffix)
return output
}
pending.append(contentsOf: suffix)
return output
case .passThrough:
isFiltering = false
output.append(contentsOf: bytes[index...])
return output
}
}
return output
return byteFilter.filter(data)
}
func finish() -> Data {
guard !pending.isEmpty else {
return Data()
}
let data = Data(pending)
pending.removeAll(keepingCapacity: false)
return data
byteFilter.finish()
}
func stopFiltering() -> Data {
let input = finish()
isFiltering = false
return input
byteFilter.stopFiltering()
}
var hasPendingInput: Bool {
isFiltering && !pending.isEmpty
byteFilter.hasPendingInput
}
var isFilteringAtProbeBoundary: Bool {
isFiltering && pending.isEmpty
byteFilter.isFilteringAtProbeBoundary
}
var isFilteringActive: Bool {
isFiltering
byteFilter.isFilteringActive
}
var isDeadlineReached: Bool {
isFiltering && (deadlineReached?() == true)
byteFilter.isFilteringActive && (deadlineReached?() == true)
}
var remainingDeadlineMilliseconds: Int64? {
guard isFiltering else { return nil }
guard byteFilter.isFilteringActive else { return nil }
return remainingDeadline?()
}
func flushPendingInput() -> Data {
guard hasPendingInput else {
return Data()
}
let data = Data(pending)
pending.removeAll(keepingCapacity: true)
isFiltering = false
return data
}
private static func reconnectProbeReplySequence(
in bytes: [UInt8],
at start: Int
) -> SSHPTYAttachReconnectInputFilterSequenceMatch {
guard start < bytes.count, bytes[start] == escape else {
return .passThrough
}
guard start + 1 < bytes.count else {
// read() can split immediately after ESC; wait for one more byte before deciding.
return .incomplete
}
switch bytes[start + 1] {
case rightBracket:
return oscColorReplySequence(in: bytes, at: start)
case leftBracket:
return csiProbeReplySequence(in: bytes, at: start)
default:
return .passThrough
}
}
private static func oscColorReplySequence(
in bytes: [UInt8],
at start: Int
) -> SSHPTYAttachReconnectInputFilterSequenceMatch {
var cursor = start + 2
var command = [UInt8]()
while cursor < bytes.count {
let byte = bytes[cursor]
if byte == semicolon {
break
}
if byte < 0x30 || byte > 0x39 || command.count >= 2 {
return .passThrough
}
command.append(byte)
cursor += 1
}
guard cursor < bytes.count else {
return isOSCColorReplyCommandPrefix(command) ? .incomplete : .passThrough
}
guard bytes[cursor] == semicolon else {
return .passThrough
}
guard command == [0x31, 0x30] || command == [0x31, 0x31] || command == [0x31, 0x32] else {
return .passThrough
}
cursor += 1
while cursor < bytes.count {
let byte = bytes[cursor]
if byte == bell {
return .strip(length: cursor - start + 1)
}
if byte == escape {
guard cursor + 1 < bytes.count else {
return .incomplete
}
if bytes[cursor + 1] == backslash {
return .strip(length: cursor - start + 2)
}
}
cursor += 1
}
return .incomplete
}
private static func csiProbeReplySequence(
in bytes: [UInt8],
at start: Int
) -> SSHPTYAttachReconnectInputFilterSequenceMatch {
var cursor = start + 2
while cursor < bytes.count {
let byte = bytes[cursor]
if byte >= 0x40, byte <= 0x7E {
return shouldStripCSIReply(bytes: bytes, bodyStart: start + 2, finalIndex: cursor)
? .strip(length: cursor - start + 1)
: .passThrough
}
guard byte >= 0x20, byte <= 0x3F else {
return .passThrough
}
cursor += 1
}
return .incomplete
}
private static func isOSCColorReplyCommandPrefix(_ command: [UInt8]) -> Bool {
command.isEmpty ||
command == [0x31] ||
command == [0x31, 0x30] ||
command == [0x31, 0x31] ||
command == [0x31, 0x32]
}
private static func shouldStripCSIReply(bytes: [UInt8], bodyStart: Int, finalIndex: Int) -> Bool {
var parameterEnd = bodyStart
while parameterEnd < finalIndex, bytes[parameterEnd] >= 0x30, bytes[parameterEnd] <= 0x3F {
parameterEnd += 1
}
guard bytes[parameterEnd..<finalIndex].allSatisfy({ $0 >= 0x20 && $0 <= 0x2F }) else {
return false
}
let parameters = bytes[bodyStart..<parameterEnd]
let intermediates = bytes[parameterEnd..<finalIndex]
let final = bytes[finalIndex]
switch final {
case 0x52, 0x63, 0x6E:
return intermediates.isEmpty
case 0x75:
return intermediates.isEmpty && parameters.first == questionMark
case 0x79:
return intermediates.elementsEqual([dollar])
default:
return false
}
}
}
@@ -1,6 +1,5 @@
struct SSHPTYAttachReconnectInputFilterState: Sendable {
let isFiltering: Bool
let pending: [UInt8]
let deadlineReached: @Sendable () -> Bool
let remainingDeadlineMilliseconds: @Sendable () -> Int64?
}
+65
View File
@@ -0,0 +1,65 @@
import CmuxControlSocket
import Foundation
extension SocketClient {
static func waitForConnectableSocket(path: String, timeout: TimeInterval) throws -> SocketClient {
try waitForConnectableSocket(resolvePath: { path }, timeout: timeout)
}
/// Waits for a socket selected by `resolvePath` to become connectable.
///
/// ``SocketStartupWaiter`` owns the shared deadline, path re-resolution,
/// vnode wakeups, and backoff. This adapter owns CLI connection creation and
/// classifies transport failures so permanent path conflicts surface
/// immediately while startup races remain retryable.
static func waitForConnectableSocket(
resolvePath: () -> String,
timeout: TimeInterval
) throws -> SocketClient {
do {
return try SocketStartupWaiter().wait(
timeout: timeout,
resolvePath: resolvePath
) { path, remainingTime in
let client = SocketClient(path: path)
do {
// Use the remaining total budget as the connect deadline so
// a full listen backlog cannot extend the bounded startup wait.
try client.connectWithoutRetry(
responseTimeout: max(remainingTime, 0.001)
)
if client.isRelayBacked {
client.close()
}
return client
} catch {
client.close()
guard shouldRetrySocketStartup(error) else {
throw error
}
return nil
}
}
} catch let startupTimeout as SocketStartupWaitTimeout {
throw startupSocketTimeout(path: startupTimeout.path)
}
}
static func isSocketStartupTimeout(_ error: Error) -> Bool {
(error as? CLIError)?.socketFailureKind == .startupTimeout
}
private static func shouldRetrySocketStartup(_ error: Error) -> Bool {
if shouldRetryConnect(error) {
return true
}
return (error as? CLIError)?.socketFailureKind == .pathMissing
}
private static func startupSocketTimeout(path: String) -> CLIError {
CLIError(
message: "cmux app did not start in time (socket not found at \(path))",
socketFailureKind: .startupTimeout
)
}
}
+1439 -436
View File
File diff suppressed because it is too large Load Diff
+29 -5
View File
@@ -9,10 +9,14 @@ This is a standalone macOS app that embeds a CMUX sidebar ExtensionKit app exten
3. Replace the Manaflow signing team with your own team.
4. Replace the app and extension bundle identifiers with your own reverse-DNS identifiers.
5. Keep the extension point identifier as `com.cmuxterm.app.cmux.sidebar`.
6. Build and launch the containing app once.
7. In CMUX, click the puzzle button next to the sidebar help button, open Sidebar Extensions, and enable the sample.
8. In the same puzzle menu, choose the extension sidebar provider.
9. In the extension sidebar header, choose `CMUX ExtKit Sample Sidebar` if more than one sidebar extension is enabled.
6. Keep App Sandbox enabled for the extension target. An unsandboxed appex can
build, embed, and sign without registering with ExtensionKit.
7. Build and launch the containing app once.
8. In CMUX, click the puzzle button next to the sidebar help button, open Sidebar
Extensions, and enable the sample.
9. In the same puzzle menu, choose the extension sidebar provider.
10. In the extension sidebar header, choose `CMUX ExtKit Sample Sidebar` if more
than one sidebar extension is enabled.
The sample targets macOS 14+, matching CMUX.
@@ -90,9 +94,29 @@ authors do not define `configuration`, bind an extension point in Swift, or touc
The manifest is the permission request CMUX shows to users. Request only the scopes
your sidebar actually needs.
## Running External Tools
CMUX read scopes can supply a workspace path, but they do not grant filesystem
access to that path. A compiled extension that launches `git` or another
external process must also satisfy the macOS App Sandbox, executable resolution,
working-directory, and file-privacy rules. Follow the SDK's
[Running external tools](../../Packages/macOS/CmuxExtensionKit/README.md#running-external-tools)
guide before adding `Process` code.
## Troubleshooting
If the extension does not appear in CMUX, launch the containing app once, then reopen CMUX's Sidebar Extensions browser.
If the extension does not appear in CMUX, confirm that App Sandbox is enabled for
the extension target, launch the containing app once, then reopen CMUX's Sidebar
Extensions browser. You can also check system discovery directly:
```sh
pluginkit -mAvvv
```
The normal CMUX release uses `com.cmuxterm.app.cmux.sidebar`. Tagged development
builds use the point stored in the host's `CMUXSidebarExtensionPointIdentifier`
Info.plist key (currently `<host-bundle-id>.cmux.sidebar`), so do not filter on
the production point when diagnosing a tagged or custom host.
If it appears but cannot be enabled, check signing on both the containing app and the embedded appex.
+17
View File
@@ -0,0 +1,17 @@
import UIKit
@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
let configuration = UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role
)
configuration.delegateClass = SceneDelegate.self
return configuration
}
}
@@ -0,0 +1,19 @@
import UIKit
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = WorkspaceDetailViewController()
window.overrideUserInterfaceStyle = .dark
window.makeKeyAndVisible()
self.window = window
}
}
@@ -0,0 +1,354 @@
import OSLog
import UIKit
@MainActor
final class WorkspaceDetailViewController: UIViewController {
private let logger = Logger(subsystem: "ai.manaflow.KeyboardPinningLab", category: "Keyboard")
private let terminalView = TerminalCanvasView()
private let dockView = ComposerDockView()
private let headerView = WorkspaceHeaderView()
private var stressTask: Task<Void, Never>?
override func viewDidLoad() {
super.viewDidLoad()
configureHierarchy()
configureKeyboardPinning()
configureActions()
observeKeyboardFrames()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
dockView.focusComposer()
if UserDefaults.standard.bool(forKey: "stressKeyboard") {
runStressSequence()
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stressTask?.cancel()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let guideTop = view.keyboardLayoutGuide.layoutFrame.minY
let dockBottom = dockView.frame.maxY
let gap = guideTop - dockBottom
headerView.updatePinGap(gap)
}
private func configureHierarchy() {
view.backgroundColor = UIColor(red: 0.075, green: 0.078, blue: 0.082, alpha: 1)
[headerView, terminalView, dockView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
headerView.heightAnchor.constraint(equalToConstant: 60),
terminalView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
terminalView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
terminalView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
terminalView.bottomAnchor.constraint(equalTo: dockView.topAnchor),
dockView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
dockView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
private func configureKeyboardPinning() {
let keyboardGuide = view.keyboardLayoutGuide
keyboardGuide.followsUndockedKeyboard = true
// The dock and keyboard share one UIKit constraint graph. UIKit owns the
// keyboard's presentation frame and interruptible animation, so no copied
// keyboard height or separately-timed animation can diverge.
dockView.bottomAnchor.constraint(equalTo: keyboardGuide.topAnchor).isActive = true
}
private func configureActions() {
terminalView.onTap = { [weak self] in
self?.dockView.focusComposer()
}
dockView.onKeyboardToggle = { [weak self] in
self?.toggleKeyboard()
}
headerView.onStress = { [weak self] in
self?.runStressSequence()
}
}
private func observeKeyboardFrames() {
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillChangeFrameNotification,
object: nil,
queue: .main
) { [weak self] notification in
guard let self,
let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
else { return }
self.logger.debug("Keyboard target minY: \(frame.minY, format: .fixed(precision: 1))")
}
}
private func toggleKeyboard() {
if dockView.isComposerFocused {
dockView.dismissComposer()
} else {
dockView.focusComposer()
}
}
private func runStressSequence() {
stressTask?.cancel()
stressTask = Task { [weak self] in
guard let self else { return }
for _ in 0..<20 {
guard !Task.isCancelled else { return }
self.toggleKeyboard()
try? await Task.sleep(for: .milliseconds(135))
}
guard !Task.isCancelled else { return }
self.dockView.focusComposer()
}
}
}
private final class WorkspaceHeaderView: UIView {
var onStress: (() -> Void)?
private let statusLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(red: 0.075, green: 0.078, blue: 0.082, alpha: 0.98)
let backButton = Self.symbolButton("chevron.left")
let titleLabel = UILabel()
titleLabel.text = String(localized: "workspace.title", defaultValue: "cmux DEV lab")
titleLabel.font = .preferredFont(forTextStyle: .headline)
titleLabel.textColor = .white
let terminalButton = Self.symbolButton("rectangle.on.rectangle")
let stressButton = Self.symbolButton("arrow.trianglehead.2.clockwise.rotate.90")
stressButton.accessibilityIdentifier = "stressKeyboard"
stressButton.accessibilityLabel = String(localized: "stress.accessibility", defaultValue: "Rapidly toggle keyboard 20 times")
stressButton.addAction(UIAction { [weak self] _ in self?.onStress?() }, for: .touchUpInside)
statusLabel.font = .monospacedDigitSystemFont(ofSize: 11, weight: .semibold)
statusLabel.textColor = UIColor(red: 0.31, green: 0.84, blue: 0.53, alpha: 1)
statusLabel.textAlignment = .center
statusLabel.accessibilityIdentifier = "pinGapStatus"
let row = UIStackView(arrangedSubviews: [backButton, titleLabel, UIView(), statusLabel, stressButton, terminalButton])
row.axis = .horizontal
row.alignment = .center
row.spacing = 10
row.translatesAutoresizingMaskIntoConstraints = false
addSubview(row)
NSLayoutConstraint.activate([
row.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
row.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
row.centerYAnchor.constraint(equalTo: centerYAnchor),
statusLabel.widthAnchor.constraint(equalToConstant: 105),
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { nil }
func updatePinGap(_ gap: CGFloat) {
let clamped = abs(gap) < 0.05 ? 0 : gap
let formattedGap = Double(clamped).formatted(.number.precision(.fractionLength(1)))
statusLabel.text = String(localized: "PIN GAP \(formattedGap) pt")
statusLabel.textColor = abs(clamped) < 0.1 ? UIColor(red: 0.31, green: 0.84, blue: 0.53, alpha: 1) : .systemRed
}
private static func symbolButton(_ symbol: String) -> UIButton {
var configuration = UIButton.Configuration.plain()
configuration.image = UIImage(systemName: symbol)
configuration.baseForegroundColor = .white
return UIButton(configuration: configuration)
}
}
private final class TerminalCanvasView: UIView {
var onTap: (() -> Void)?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(red: 0.105, green: 0.108, blue: 0.112, alpha: 1)
isAccessibilityElement = true
accessibilityIdentifier = "terminalCanvas"
accessibilityLabel = String(localized: "terminal.accessibility", defaultValue: "Terminal. Tap to show keyboard.")
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapGesture)
let terminalText = UILabel()
terminalText.translatesAutoresizingMaskIntoConstraints = false
terminalText.numberOfLines = 0
terminalText.font = .monospacedSystemFont(ofSize: 11, weight: .regular)
terminalText.textColor = UIColor(white: 0.79, alpha: 1)
terminalText.text = String(localized: "terminal.sample", defaultValue: "Last login: Fri Aug 7 19:45:12 on ttys006\n\n~/cmux git:(feat/keyboard-pinning-lab)\n")
addSubview(terminalText)
NSLayoutConstraint.activate([
terminalText.topAnchor.constraint(equalTo: topAnchor, constant: 14),
terminalText.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
terminalText.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -12),
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { nil }
@objc private func handleTap() {
onTap?()
}
}
private final class ComposerDockView: UIView, UITextFieldDelegate {
var onKeyboardToggle: (() -> Void)?
private let textField = UITextField()
var isComposerFocused: Bool { textField.isFirstResponder }
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(red: 0.075, green: 0.078, blue: 0.082, alpha: 0.99)
let shortcuts = makeShortcutBar()
let composer = makeComposerBar()
let stack = UIStackView(arrangedSubviews: [shortcuts, composer])
stack.axis = .vertical
stack.spacing = 4
stack.translatesAutoresizingMaskIntoConstraints = false
addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: topAnchor, constant: 6),
stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -6),
shortcuts.heightAnchor.constraint(equalToConstant: 36),
composer.heightAnchor.constraint(equalToConstant: 42),
])
}
@available(*, unavailable)
required init?(coder: NSCoder) { nil }
func focusComposer() {
textField.becomeFirstResponder()
}
func dismissComposer() {
textField.resignFirstResponder()
}
private func makeShortcutBar() -> UIView {
let specs: [(String, String)] = [
("keyboard", "keyboard.toggle"),
("circle.fill", "shortcut.control"),
("square.and.pencil", "shortcut.command"),
("chevron.up", "shortcut.up"),
("option", "shortcut.option"),
("command", "shortcut.command"),
("doc.on.clipboard", "shortcut.paste"),
]
let buttons = specs.map { symbol, identifier in
var configuration = UIButton.Configuration.plain()
configuration.image = UIImage(systemName: symbol)
configuration.baseForegroundColor = .white
configuration.contentInsets = .zero
let button = UIButton(configuration: configuration)
button.accessibilityIdentifier = identifier
if identifier == "keyboard.toggle" {
button.addAction(UIAction { [weak self] _ in self?.onKeyboardToggle?() }, for: .touchUpInside)
}
return button
}
var tabConfiguration = UIButton.Configuration.filled()
tabConfiguration.title = String(localized: "shortcut.tab", defaultValue: "Tab")
tabConfiguration.baseBackgroundColor = UIColor(white: 0.18, alpha: 1)
tabConfiguration.baseForegroundColor = .white
tabConfiguration.cornerStyle = .capsule
tabConfiguration.contentInsets = .init(top: 0, leading: 2, bottom: 0, trailing: 2)
tabConfiguration.titleLineBreakMode = .byClipping
tabConfiguration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
var attributes = attributes
attributes.font = .systemFont(ofSize: 11, weight: .medium)
return attributes
}
let tabButton = UIButton(configuration: tabConfiguration)
var escapeConfiguration = tabConfiguration
escapeConfiguration.title = String(localized: "shortcut.escape", defaultValue: "Esc")
let escapeButton = UIButton(configuration: escapeConfiguration)
let stack = UIStackView(arrangedSubviews: buttons + [tabButton, escapeButton])
stack.axis = .horizontal
stack.alignment = .fill
stack.distribution = .fillEqually
stack.spacing = 4
return stack
}
private func makeComposerBar() -> UIView {
let attachment = Self.circleButton(symbol: "paperclip")
let microphone = Self.circleButton(symbol: "mic")
let send = Self.circleButton(symbol: "arrow.up", filled: true)
textField.delegate = self
textField.placeholder = String(localized: "composer.placeholder", defaultValue: "Message")
textField.textColor = .white
textField.tintColor = .white
textField.font = .preferredFont(forTextStyle: .body)
textField.returnKeyType = .send
textField.autocorrectionType = .no
textField.accessibilityIdentifier = "composerTextField"
let fieldContainer = UIView()
fieldContainer.backgroundColor = UIColor(white: 0.12, alpha: 1)
fieldContainer.layer.cornerRadius = 17
textField.translatesAutoresizingMaskIntoConstraints = false
fieldContainer.addSubview(textField)
NSLayoutConstraint.activate([
textField.leadingAnchor.constraint(equalTo: fieldContainer.leadingAnchor, constant: 12),
textField.trailingAnchor.constraint(equalTo: fieldContainer.trailingAnchor, constant: -8),
textField.topAnchor.constraint(equalTo: fieldContainer.topAnchor),
textField.bottomAnchor.constraint(equalTo: fieldContainer.bottomAnchor),
])
let row = UIStackView(arrangedSubviews: [attachment, microphone, fieldContainer, send])
row.axis = .horizontal
row.alignment = .fill
row.spacing = 6
return row
}
private static func circleButton(symbol: String, filled: Bool = false) -> UIButton {
var configuration = filled ? UIButton.Configuration.filled() : UIButton.Configuration.plain()
configuration.image = UIImage(systemName: symbol)
configuration.baseForegroundColor = filled ? .black : .white
configuration.baseBackgroundColor = filled ? UIColor(white: 0.85, alpha: 1) : .clear
configuration.cornerStyle = .capsule
configuration.contentInsets = .zero
return UIButton(configuration: configuration)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.text = nil
return false
}
}
+11
View File
@@ -0,0 +1,11 @@
# Verification
Source reference: `ScreenRecording_08-07-2026 19-50-56_1.MP4`, 1320×2868, 60 fps, 51.62 seconds.
Invariant: during interrupted rapid keyboard show and hide cycles, the Shortcut bar and Composer bar remain one rigid dock. The dock bottom stays coincident with the keyboard top, with no transient gap, overlap, lag, or snap-back.
Scenario: iPhone 17 Pro Max simulator on iOS 26.5, dark appearance, keyboard initially shown, 20 first-responder reversals at 135 ms intervals, then a final focused state. This interval is shorter than a normal keyboard transition and forces animation interruption.
Result: the live layout diagnostic remained `PIN GAP 0.0 pt`. The final run was sampled at 15 fps, including steady, partial-hide, hidden, partial-show, reversed, and final frames. The invariant held in every sampled frame.
Durable evidence is stored at `cmux-assets/feat-keyboard-pinning-lab/rapid-toggle-final/` in the cmuxterm-hq checkout. The directory contains the raw recording, annotated frames, contact sheet, and manifest with the success criterion.
@@ -0,0 +1,351 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
1E81F0EEF337DE057CCEC892 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CE0E17A1008C3577E9CD4995 /* InfoPlist.strings */; };
7BCC8A0872603104E124D03E /* WorkspaceDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12AF82959FE7DAAA5C3ADBEE /* WorkspaceDetailViewController.swift */; };
8F1F331AA4DD22756ED437F4 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 949EB7D848CCC51B60D11989 /* Localizable.xcstrings */; };
DE8D30BAEC473E45B80219A5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2728F872D5FB8D33A41A6E91 /* SceneDelegate.swift */; };
EFCF59C7ED8878A5FAA7DDF5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4444F6FE9025CD71B41223BE /* AppDelegate.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
12AF82959FE7DAAA5C3ADBEE /* WorkspaceDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceDetailViewController.swift; sourceTree = "<group>"; };
2728F872D5FB8D33A41A6E91 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
2C03363774959A68994988EB /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
4444F6FE9025CD71B41223BE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
5405DD1047FED983298B8CD3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
949EB7D848CCC51B60D11989 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
D4959730D56E1DB02C931108 /* KeyboardPinningLab.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KeyboardPinningLab.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
0CA61C38419BE31AC5D5455E /* App */ = {
isa = PBXGroup;
children = (
4444F6FE9025CD71B41223BE /* AppDelegate.swift */,
2728F872D5FB8D33A41A6E91 /* SceneDelegate.swift */,
12AF82959FE7DAAA5C3ADBEE /* WorkspaceDetailViewController.swift */,
);
path = App;
sourceTree = "<group>";
};
CD6A0D068D8B8C2D583490C8 = {
isa = PBXGroup;
children = (
0CA61C38419BE31AC5D5455E /* App */,
E0F87851FC78526801069DE5 /* Resources */,
E6422832B55FB3758A111DC2 /* Products */,
);
sourceTree = "<group>";
};
E0F87851FC78526801069DE5 /* Resources */ = {
isa = PBXGroup;
children = (
949EB7D848CCC51B60D11989 /* Localizable.xcstrings */,
CE0E17A1008C3577E9CD4995 /* InfoPlist.strings */,
);
path = Resources;
sourceTree = "<group>";
};
E6422832B55FB3758A111DC2 /* Products */ = {
isa = PBXGroup;
children = (
D4959730D56E1DB02C931108 /* KeyboardPinningLab.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
27AE499E18794152BA4672C0 /* KeyboardPinningLab */ = {
isa = PBXNativeTarget;
buildConfigurationList = 76AA4EB0FA9193B6C03BEA00 /* Build configuration list for PBXNativeTarget "KeyboardPinningLab" */;
buildPhases = (
668F9A8ACF9F66A71F3435F3 /* Sources */,
AF47256CFA451165B9D69F33 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = KeyboardPinningLab;
packageProductDependencies = (
);
productName = KeyboardPinningLab;
productReference = D4959730D56E1DB02C931108 /* KeyboardPinningLab.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
58E3B7797EDEB766152AADC7 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
27AE499E18794152BA4672C0 = {
DevelopmentTeam = 7WLXT3NR37;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 6AE1CD5DF1B492DCA42ECF57 /* Build configuration list for PBXProject "KeyboardPinningLab" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
ja,
);
mainGroup = CD6A0D068D8B8C2D583490C8;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = E6422832B55FB3758A111DC2 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
27AE499E18794152BA4672C0 /* KeyboardPinningLab */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
AF47256CFA451165B9D69F33 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1E81F0EEF337DE057CCEC892 /* InfoPlist.strings in Resources */,
8F1F331AA4DD22756ED437F4 /* Localizable.xcstrings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
668F9A8ACF9F66A71F3435F3 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
EFCF59C7ED8878A5FAA7DDF5 /* AppDelegate.swift in Sources */,
DE8D30BAEC473E45B80219A5 /* SceneDelegate.swift in Sources */,
7BCC8A0872603104E124D03E /* WorkspaceDetailViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
CE0E17A1008C3577E9CD4995 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
5405DD1047FED983298B8CD3 /* en */,
2C03363774959A68994988EB /* ja */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
0E30ABD0849A82BD34D9BF72 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 7WLXT3NR37;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
};
name = Release;
};
C0E137783EBF25A0C576D57F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Pinning Lab";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = ai.manaflow.KeyboardPinningLab;
PRODUCT_NAME = "Keyboard Pinning Lab";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
DF22FCDD84F72C666DC4079C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Pinning Lab";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = ai.manaflow.KeyboardPinningLab;
PRODUCT_NAME = "Keyboard Pinning Lab";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
EE990F81439C9DDADF595351 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 7WLXT3NR37;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
6AE1CD5DF1B492DCA42ECF57 /* Build configuration list for PBXProject "KeyboardPinningLab" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EE990F81439C9DDADF595351 /* Debug */,
0E30ABD0849A82BD34D9BF72 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
76AA4EB0FA9193B6C03BEA00 /* Build configuration list for PBXNativeTarget "KeyboardPinningLab" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DF22FCDD84F72C666DC4079C /* Debug */,
C0E137783EBF25A0C576D57F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = 58E3B7797EDEB766152AADC7 /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27AE499E18794152BA4672C0"
BuildableName = "KeyboardPinningLab.app"
BlueprintName = "KeyboardPinningLab"
ReferencedContainer = "container:KeyboardPinningLab.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27AE499E18794152BA4672C0"
BuildableName = "KeyboardPinningLab.app"
BlueprintName = "KeyboardPinningLab"
ReferencedContainer = "container:KeyboardPinningLab.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27AE499E18794152BA4672C0"
BuildableName = "KeyboardPinningLab.app"
BlueprintName = "KeyboardPinningLab"
ReferencedContainer = "container:KeyboardPinningLab.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "27AE499E18794152BA4672C0"
BuildableName = "KeyboardPinningLab.app"
BlueprintName = "KeyboardPinningLab"
ReferencedContainer = "container:KeyboardPinningLab.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+11
View File
@@ -0,0 +1,11 @@
# Keyboard Pinning Lab
This standalone iOS app isolates the Workspace Detail keyboard geometry from cmux state and networking.
The Composer and Shortcut bars live in one `ComposerDockView`. Its bottom edge is constrained directly to `UIKeyboardLayoutGuide.topAnchor`. UIKit therefore owns the keyboard frame, the dock frame, and interrupted animation timing in one constraint graph.
The circular-arrow header button reverses first-responder state every 135 ms to stress interrupted keyboard transitions. The header reports the live constraint gap. Green `PIN GAP 0.0 pt` means the dock and keyboard guide are coincident in the current layout pass.
Tapping the terminal canvas focuses the same Composer text field, so terminal tap, direct Composer tap, the keyboard button, and the stress control all exercise one keyboard ownership path.
Generate the Xcode project with `xcodegen generate`, then build the `KeyboardPinningLab` scheme.
@@ -0,0 +1,54 @@
{
"sourceLanguage" : "en",
"strings" : {
"composer.placeholder" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Message" } },
"ja" : { "stringUnit" : { "state" : "translated", "value" : "メッセージ" } }
}
},
"PIN GAP %@ pt" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "PIN GAP %1$@ pt" } },
"ja" : { "stringUnit" : { "state" : "translated", "value" : "固定間隔 %1$@ pt" } }
}
},
"shortcut.escape" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Esc" } },
"ja" : { "stringUnit" : { "state" : "translated", "value" : "Esc" } }
}
},
"stress.accessibility" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Rapidly toggle keyboard 20 times" } },
"ja" : { "stringUnit" : { "state" : "translated", "value" : "キーボードを20回すばやく切り替える" } }
}
},
"shortcut.tab" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Tab" } },
"ja" : { "stringUnit" : { "state" : "translated", "value" : "Tab" } }
}
},
"terminal.sample" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Last login: Fri Aug 7 19:45:12 on ttys006\n\n~/cmux git:(feat/keyboard-pinning-lab)\n" } },
"ja" : { "stringUnit" : { "state" : "translated", "value" : "最終ログイン: 8月7日 金 19:45:12 ttys006\n\n~/cmux git:(feat/keyboard-pinning-lab)\n" } }
}
},
"terminal.accessibility" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Terminal. Tap to show keyboard." } },
"ja" : { "stringUnit" : { "state" : "translated", "value" : "ターミナル。タップしてキーボードを表示します。" } }
}
},
"workspace.title" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "cmux DEV lab" } },
"ja" : { "stringUnit" : { "state" : "translated", "value" : "cmux DEV ラボ" } }
}
}
},
"version" : "1.0"
}
@@ -0,0 +1 @@
"CFBundleDisplayName" = "Pinning Lab";
@@ -0,0 +1 @@
"CFBundleDisplayName" = "ピン留めラボ";
+32
View File
@@ -0,0 +1,32 @@
name: KeyboardPinningLab
options:
bundleIdPrefix: ai.manaflow
deploymentTarget:
iOS: "17.0"
generateEmptyDirectories: true
settings:
base:
DEVELOPMENT_TEAM: 7WLXT3NR37
SWIFT_VERSION: 6.0
SWIFT_STRICT_CONCURRENCY: complete
targets:
KeyboardPinningLab:
type: application
platform: iOS
sources:
- path: App
- path: Resources
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: ai.manaflow.KeyboardPinningLab
PRODUCT_NAME: Keyboard Pinning Lab
INFOPLIST_KEY_CFBundleDisplayName: Pinning Lab
INFOPLIST_KEY_UIApplicationSceneManifest_Generation: true
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: true
INFOPLIST_KEY_UILaunchScreen_Generation: true
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: UIInterfaceOrientationPortrait
TARGETED_DEVICE_FAMILY: 1
GENERATE_INFOPLIST_FILE: true
CODE_SIGN_STYLE: Automatic
scheme:
testTargets: []
@@ -4,6 +4,7 @@ import PackageDescription
let package = Package(
name: "CMUXMobileCore",
defaultLocalization: "en",
platforms: [
.iOS(.v18),
.macOS(.v14),
@@ -17,6 +18,7 @@ let package = Package(
targets: [
.target(
name: "CMUXMobileCore",
resources: [.process("Resources")],
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
+18
View File
@@ -0,0 +1,18 @@
# CMUXMobileCore
Shared protocol seams and value types used by both the iOS and macOS apps.
Higher-level mobile packages depend on this package instead of importing one
another for shared contracts.
## Testing telemetry consent
Inject a suite-scoped defaults store so tests do not read or mutate the user's
preferences:
```swift
let defaults = UserDefaults(suiteName: "example.telemetry-test")!
let consent = UserDefaultsAnalyticsConsentProvider(defaults: defaults)
defaults.set(true, forKey: UserDefaultsAnalyticsConsentProvider.telemetryKey)
#expect(consent.isTelemetryEnabled)
```
@@ -0,0 +1,12 @@
/// The shared opt-out gate consulted before sending telemetry.
///
/// Analytics and crash-reporting infrastructure depend on this lower-level
/// seam so both obey the same live consent source without depending on each
/// other.
public protocol AnalyticsConsentProviding: Sendable {
/// Whether anonymous product telemetry may currently be sent.
///
/// A conformer must return its current value on every read so consent
/// changes take effect without rebuilding the telemetry graph.
var isTelemetryEnabled: Bool { get }
}
@@ -0,0 +1,591 @@
public import Foundation
/// The consolidated on-disk application log: one active file with bounded
/// archives for app-wide events and one equivalent set for network diagnostics.
///
/// ``AppLog`` is the durable half of the diagnostics stack. The in-memory
/// ``DiagnosticLog`` ring stays the single structured spine every subsystem
/// records into; the composition root taps that ring into an ``AppLog``, which
/// renders each event through ``DiagnosticEventPresentation`` and appends it to
/// one of two files:
///
/// - the **app log** (``appLogFileName``): every event that is not
/// network-plane simulator streaming/control, browser streaming, composer,
/// render plus mirrored string debug-log lines, so one file tells the whole
/// in-app story in wall-clock order;
/// - the **network log** (``networkLogFileName``): transport dials, discovery,
/// relay policy, path changes, session lifecycle and close attribution.
///
/// Cross-cutting context (app lifecycle, reachability) is written to both so
/// each file is self-sufficient. Diagnostic events are integer-encoded and
/// privacy-safe by construction, so persistence is always on, including
/// Release; the free-text mirror keeps the string log's own gating (DEBUG
/// always, Release behind the verbose opt-in) because those lines are not
/// structurally scrubbed.
///
/// Ordering: both entry points are non-blocking and feed one buffered stream
/// drained by a single internal task, so lines land on disk in admission
/// order. Consecutive frame-pipeline events for the same panel and stage are
/// coalesced into a `repeated ×N` summary when the run breaks, so a healthy
/// 20 fps stream costs one line plus one summary instead of megabytes.
///
/// The active generation is reopened for appending on the next launch. When
/// the byte budget is reached it is moved to a timestamped archive, then a
/// fresh active generation is opened. Archives are bounded by both count and
/// total bytes, so retention never requires clearing the app container.
///
/// Inject one instance from the app composition root; do not add a `.shared`
/// singleton.
public actor AppLog {
/// Which on-disk file an event belongs to.
public enum Domain: Sendable, Equatable {
case app
case network
case both
}
public static let appLogFileName = "cmux-app.log"
public static let networkLogFileName = "cmux-network.log"
/// The approximate size of one active generation in production.
public static let defaultMaxFileBytes = 5_000_000
/// Number of timestamped generations retained in addition to the active
/// file. A legacy `.1` file is migrated before this limit is applied.
public static let defaultMaxArchiveCount = 3
/// Per-file retention ceiling, including the active generation.
public static let defaultMaxRetainedBytes = 12_000_000
/// Default location of the app-wide log inside Application Support, or
/// `nil` when the directory cannot be resolved. Exists so settings UI can
/// offer the file for sharing without holding the ``AppLog`` instance.
public static var defaultAppLogFileURL: URL? {
defaultFileURL(named: appLogFileName)
}
/// Default location of the network diagnostics log. See
/// ``defaultAppLogFileURL``.
public static var defaultNetworkLogFileURL: URL? {
defaultFileURL(named: networkLogFileName)
}
/// All available generations for the app log, newest first after the
/// active file. Settings passes this collection to the share UI so a
/// diagnostic export includes the bounded history, not only the active
/// generation.
public static var appLogFileURLs: [URL] {
guard let url = defaultAppLogFileURL else { return [] }
return logFileURLs(for: url)
}
/// All available generations for the network log, with the active file
/// first. See ``appLogFileURLs``.
public static var networkLogFileURLs: [URL] {
guard let url = defaultNetworkLogFileURL else { return [] }
return logFileURLs(for: url)
}
/// Returns the active file and any retained archive generations for a
/// caller-supplied location. The legacy `<name>.1` generation is included
/// when a prior build could not migrate it.
public static func logFileURLs(for fileURL: URL) -> [URL] {
let fileManager = FileManager.default
var urls: [URL] = []
if fileManager.fileExists(atPath: fileURL.path) {
urls.append(fileURL)
}
urls.append(contentsOf: archiveURLs(for: fileURL))
let legacyURL = legacyRotationURL(for: fileURL)
if fileManager.fileExists(atPath: legacyURL.path) {
urls.append(legacyURL)
}
return urls
}
private static let archiveMarker = ".archive-"
private static func legacyRotationURL(for fileURL: URL) -> URL {
URL(fileURLWithPath: fileURL.path + ".1")
}
private static func archivePrefix(for fileURL: URL) -> String {
let stem = fileURL.pathExtension.isEmpty
? fileURL.lastPathComponent
: fileURL.deletingPathExtension().lastPathComponent
return "\(stem)\(archiveMarker)"
}
private static func archiveStamp(for url: URL, prefix: String) -> Int64? {
let remainder = url.lastPathComponent.dropFirst(prefix.count)
let stamp = remainder.prefix(13)
guard stamp.count == 13,
stamp.allSatisfy({ $0.isNumber }),
remainder.dropFirst(stamp.count).first == "-"
else {
return nil
}
return Int64(stamp)
}
private static func archiveURLs(for fileURL: URL) -> [URL] {
let fileManager = FileManager.default
let directory = fileURL.deletingLastPathComponent()
let prefix = archivePrefix(for: fileURL)
guard let names = try? fileManager.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: [.contentModificationDateKey, .fileSizeKey],
options: [.skipsHiddenFiles]
) else {
return []
}
return names
.filter { candidate in
candidate.lastPathComponent.hasPrefix(prefix)
&& candidate.pathExtension == fileURL.pathExtension
&& fileManager.fileExists(atPath: candidate.path)
}
.sorted { lhs, rhs in
let leftStamp = archiveStamp(for: lhs, prefix: prefix)
let rightStamp = archiveStamp(for: rhs, prefix: prefix)
switch (leftStamp, rightStamp) {
case let (left?, right?):
if left != right { return left > right }
case (_?, nil):
return true
case (nil, _?):
return false
case (nil, nil):
break
}
let leftDate = (try? lhs.resourceValues(
forKeys: [.contentModificationDateKey]
).contentModificationDate) ?? .distantPast
let rightDate = (try? rhs.resourceValues(
forKeys: [.contentModificationDateKey]
).contentModificationDate) ?? .distantPast
if leftDate != rightDate { return leftDate > rightDate }
return lhs.lastPathComponent > rhs.lastPathComponent
}
}
private static func makeArchiveURL(for fileURL: URL, date: Date) -> URL {
let stem = fileURL.pathExtension.isEmpty
? fileURL.lastPathComponent
: fileURL.deletingPathExtension().lastPathComponent
let extensionSuffix = fileURL.pathExtension.isEmpty
? ""
: ".\(fileURL.pathExtension)"
let milliseconds = Int64(date.timeIntervalSince1970 * 1_000)
let stamp = String(format: "%013lld", milliseconds)
let unique = String(UUID().uuidString.prefix(8))
return fileURL.deletingLastPathComponent()
.appendingPathComponent(
"\(stem)\(archiveMarker)\(stamp)-\(unique)\(extensionSuffix)"
)
}
private static func defaultFileURL(named name: String) -> URL? {
let fileManager = FileManager.default
guard let base = fileManager.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first else { return nil }
do {
try fileManager.createDirectory(at: base, withIntermediateDirectories: true)
} catch {
return nil
}
return base.appendingPathComponent(name)
}
private enum Entry: Sendable {
case event(DiagnosticEvent, wall: Date)
case appLine(String, wall: Date)
}
private struct LogFile {
let url: URL
let maxBytes: Int
let maxArchiveCount: Int
let maxRetainedBytes: Int
let header: String
let now: @Sendable () -> Date
var handle: FileHandle?
var bytesWritten = 0
/// Byte level at which the next rotation is attempted. Normally
/// `maxBytes`; raised after a failed rotate so a sustained failure
/// (busy file, read-only directory) retries once per additional
/// budget of growth instead of once per appended line.
var rotationThreshold: Int
init(
url: URL,
maxBytes: Int,
maxArchiveCount: Int,
maxRetainedBytes: Int,
header: String,
now: @escaping @Sendable () -> Date
) {
self.url = url
self.maxBytes = max(1, maxBytes)
self.maxArchiveCount = max(1, maxArchiveCount)
self.maxRetainedBytes = max(maxRetainedBytes, self.maxBytes)
self.header = header
self.now = now
self.rotationThreshold = max(1, maxBytes)
migrateLegacyRotation()
if FileManager.default.fileExists(atPath: url.path) {
openExistingForAppending()
if handle != nil, bytesWritten >= self.maxBytes {
_ = rotate()
}
} else {
_ = openFreshGeneration()
}
if handle != nil {
pruneArchives()
}
}
/// Moves a legacy `<name>.1` generation into the timestamped archive
/// namespace. If the move cannot be completed, the legacy file stays
/// untouched and remains shareable.
private mutating func migrateLegacyRotation() {
let fileManager = FileManager.default
let legacyURL = AppLog.legacyRotationURL(for: url)
guard fileManager.fileExists(atPath: legacyURL.path) else { return }
let archiveURL = AppLog.makeArchiveURL(for: url, date: now())
try? fileManager.moveItem(at: legacyURL, to: archiveURL)
}
/// Opens a new active generation. This method never removes or
/// overwrites an existing file. The caller must move an old active
/// generation away first.
@discardableResult
private mutating func openFreshGeneration() -> Bool {
let fileManager = FileManager.default
guard !fileManager.fileExists(atPath: url.path),
fileManager.createFile(atPath: url.path, contents: nil),
let opened = try? FileHandle(forWritingTo: url) else {
handle = nil
return false
}
handle = opened
bytesWritten = 0
rotationThreshold = maxBytes
write(header)
return handle != nil
}
/// Rotates the active generation into a unique archive. A failed move
/// reopens the original file for appending and leaves every existing
/// byte in place. If creating the replacement fails after the move,
/// the archive is restored when possible; otherwise it remains on disk
/// and is still returned by ``AppLog.logFileURLs(for:)``.
@discardableResult
private mutating func rotate() -> Bool {
let fileManager = FileManager.default
guard fileManager.fileExists(atPath: url.path) else {
return openFreshGeneration()
}
close()
let archiveURL = AppLog.makeArchiveURL(for: url, date: now())
do {
try fileManager.moveItem(at: url, to: archiveURL)
} catch {
openExistingForAppending()
rotationThreshold = bytesWritten + maxBytes
return false
}
guard openFreshGeneration() else {
if !fileManager.fileExists(atPath: url.path) {
try? fileManager.moveItem(at: archiveURL, to: url)
}
openExistingForAppending()
rotationThreshold = bytesWritten + maxBytes
return false
}
pruneArchives()
return true
}
/// Keeps writing to the current generation. Existing files are opened
/// at their end, never truncated. An empty pre-existing file receives
/// the generation header once.
private mutating func openExistingForAppending() {
guard let opened = try? FileHandle(forWritingTo: url),
let size = try? opened.seekToEnd() else {
// A generation that cannot be opened or positioned at its end
// is not safely appendable: writing from offset 0 would
// overwrite the content this fallback exists to preserve.
handle = nil
return
}
handle = opened
bytesWritten = Int(clamping: size)
rotationThreshold = maxBytes
if bytesWritten == 0 {
write(header)
}
}
mutating func append(_ line: String) {
guard handle != nil else { return }
let data = Data((line + "\n").utf8)
if bytesWritten + data.count > rotationThreshold {
_ = rotate()
}
write(line)
}
/// Removes only timestamped archives, and only after a new active
/// generation has been opened. The newest archive is always kept even
/// if one unusually large line temporarily exceeds the byte ceiling.
private mutating func pruneArchives() {
let fileManager = FileManager.default
var archives = AppLog.archiveURLs(for: url)
guard !archives.isEmpty else { return }
var totalBytes = fileSize(of: url)
var archiveSizes = archives.map { fileSize(of: $0) }
totalBytes += archiveSizes.reduce(0, +)
while archives.count > maxArchiveCount || totalBytes > maxRetainedBytes {
guard archives.count > 1 else { break }
let oldestIndex = archives.count - 1
let oldest = archives.remove(at: oldestIndex)
let oldestSize = archiveSizes.remove(at: oldestIndex)
do {
try fileManager.removeItem(at: oldest)
totalBytes -= oldestSize
} catch {
// A protection or sharing failure should never cause us
// to remove a different, newer generation.
break
}
}
}
private func fileSize(of fileURL: URL) -> Int {
guard let values = try? fileURL.resourceValues(forKeys: [.fileSizeKey]),
let size = values.fileSize else {
return 0
}
return size
}
private mutating func write(_ line: String) {
guard let handle else { return }
let data = Data((line + "\n").utf8)
do {
try handle.write(contentsOf: data)
bytesWritten += data.count
} catch {
try? handle.close()
self.handle = nil
}
}
mutating func close() {
try? handle?.close()
handle = nil
}
}
/// One in-progress run of coalescible frame events.
private struct FrameRun {
let key: FrameRunKey
var lastEvent: DiagnosticEvent
var count: Int
}
private struct FrameRunKey: Equatable {
let code: DiagnosticEventCode
let surface: UInt32?
let stage: Int?
}
private var appFile: LogFile?
private var networkFile: LogFile?
private var pendingFrameRun: FrameRun?
private var processed = 0
private let presentation = DiagnosticEventPresentation()
private let timestampFormatter: ISO8601DateFormatter
private let ingress: AsyncStream<Entry>.Continuation
/// Create a log writing to the given locations. Passing `nil` for a URL
/// disables that file (used by tests exercising one file at a time).
public init(
appFileURL: URL?,
networkFileURL: URL?,
maxFileBytes: Int = AppLog.defaultMaxFileBytes,
buildStamp: String = "",
maxArchiveCount: Int = AppLog.defaultMaxArchiveCount,
maxRetainedBytes: Int = AppLog.defaultMaxRetainedBytes,
now: @escaping @Sendable () -> Date = { Date() }
) {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
timestampFormatter = formatter
let started = formatter.string(from: now())
if let appFileURL {
appFile = LogFile(
url: appFileURL,
maxBytes: maxFileBytes,
maxArchiveCount: maxArchiveCount,
maxRetainedBytes: maxRetainedBytes,
header: "cmux app log · \(buildStamp) · started \(started)",
now: now
)
}
if let networkFileURL {
networkFile = LogFile(
url: networkFileURL,
maxBytes: maxFileBytes,
maxArchiveCount: maxArchiveCount,
maxRetainedBytes: maxRetainedBytes,
header: "cmux network diagnostics log · \(buildStamp) · started \(started)",
now: now
)
}
let (stream, continuation) = AsyncStream<Entry>.makeStream(
bufferingPolicy: .bufferingNewest(2048)
)
ingress = continuation
// The drain holds self only across one write; when the log deallocs,
// `deinit` finishes the stream and the loop ends on its own.
Task { [weak self] in
for await entry in stream {
guard let self else { return }
await self.write(entry)
}
}
}
deinit {
ingress.finish()
}
/// Record one structured diagnostic event. Non-blocking and safe to call
/// from the ``DiagnosticLog`` event tap (which runs on the ring's drain
/// task and must not block).
public nonisolated func ingest(_ event: DiagnosticEvent) {
ingress.yield(.event(event, wall: Date()))
}
/// Mirror one free-text debug-log line into the app file. The caller owns
/// the privacy gating (the string debug log only produces lines in DEBUG
/// or behind the user's verbose opt-in).
public nonisolated func mirrorAppLine(_ line: String) {
ingress.yield(.appLine(line, wall: Date()))
}
/// The total number of entries the drain task has written. Never
/// decreases, so after admitting `n` entries a test can poll this to `n`
/// to know everything reached the files, without sleeping.
public func processedCount() -> Int {
processed
}
/// Flushes a pending coalesced frame run to disk. Test-only
/// synchronization; in production runs flush when they break.
public func flushForTesting() {
flushPendingFrameRun()
}
private func write(_ entry: Entry) {
processed += 1
switch entry {
case .event(let event, let wall):
writeEvent(event, wall: wall)
case .appLine(let line, let wall):
flushPendingFrameRun()
appFile?.append("\(timestampFormatter.string(from: wall)) \(line)")
}
}
private func writeEvent(_ event: DiagnosticEvent, wall: Date) {
if let key = Self.frameRunKey(for: event) {
if var run = pendingFrameRun, run.key == key {
run.lastEvent = event
run.count += 1
pendingFrameRun = run
return
}
flushPendingFrameRun()
appendRendered(event, wall: wall)
pendingFrameRun = FrameRun(key: key, lastEvent: event, count: 1)
return
}
flushPendingFrameRun()
appendRendered(event, wall: wall)
}
/// Frame-pipeline events repeat at frame cadence with only the sequence
/// and byte count varying; they coalesce per (code, panel, stage).
private static func frameRunKey(for event: DiagnosticEvent) -> FrameRunKey? {
guard event.code == .simulatorFrameLifecycle else { return nil }
return FrameRunKey(code: event.code, surface: event.surface, stage: event.a)
}
private func flushPendingFrameRun() {
guard let run = pendingFrameRun else { return }
pendingFrameRun = nil
guard run.count > 1 else { return }
let summary = presentation.summary(run.lastEvent)
append(
line: "\(timestampFormatter.string(from: Date())) \(summary) (repeated ×\(run.count))",
domain: run.key.code.appLogDomain
)
}
private func appendRendered(_ event: DiagnosticEvent, wall: Date) {
append(
line: "\(timestampFormatter.string(from: wall)) \(presentation.summary(event))",
domain: event.code.appLogDomain
)
}
private func append(line: String, domain: Domain) {
switch domain {
case .app:
appFile?.append(line)
case .network:
networkFile?.append(line)
case .both:
appFile?.append(line)
networkFile?.append(line)
}
}
}
public extension DiagnosticEventCode {
/// Which on-disk log this event belongs to: the app-wide log, the network
/// diagnostics log, or both (cross-cutting context that keeps each file
/// self-sufficient). New codes default to the app log.
var appLogDomain: AppLog.Domain {
switch self {
case .connect, .pairOk, .pairFail, .pairUnreachable,
.livenessResubscribe, .streamEnded, .inputSeqBehind, .byteGap,
.transportDialStarted, .transportDialConnected, .transportDialFailed,
.hostAuthenticated, .hostAuthenticationFailed,
.rpcReady, .rpcFailed,
.recoveryStarted, .recoverySucceeded, .recoveryFailed,
.endpointStarting, .endpointActive, .endpointStopped, .endpointFailed,
.relayPolicyRefreshStarted, .relayPolicyRefreshSucceeded,
.relayPolicyRefreshFailed,
.selectedPathChanged, .sessionClosed, .routeUnavailable,
.retryScheduled,
.discoveryStarted, .discoverySucceeded, .discoveryFailed,
.admissionSucceeded, .admissionFailed,
.transportSessionLifecycle,
.transportCloseAttribution, .transportPathEvent,
.transportDialPlanBuilt, .transportPrivateAddressJoin,
.transportLANDiscovery, .transportDialLegSucceeded,
.transportDialLegFailed, .lanPublicationState,
.transportDialSessionLinked, .transportDialCancelled,
.transportCloseReason:
return .network
case .appLifecycleChanged, .reachabilityChanged:
return .both
default:
return .app
}
}
}
@@ -0,0 +1,161 @@
/// A privacy-safe, staged explanation of Iroh connection readiness.
public struct CmxIrohConnectionCheckReport: Equatable, Sendable {
public enum Role: Equatable, Sendable {
case mobileClient
case macHost
}
public enum StageKind: CaseIterable, Hashable, Sendable {
case encryptedTransport
case relayPolicy
case relayReachability
case macDiscovery
case secureSession
}
public enum StageStatus: Equatable, Sendable {
case passed
case warning
case failed
case notApplicable
}
public enum RelayReachability: Equatable, Sendable {
case notConfigured
case reachable
case unreachable
case unavailable
}
public enum MacDiscovery: Equatable, Sendable {
case found
case missing
case unavailable
}
public enum Recommendation: Equatable, Sendable {
case none
case retry
case checkInternet
case openMacApp
case allowRelayTraffic
case refreshAccount
case reviewRelaySettings
case updateOrRepair
}
public struct Stage: Identifiable, Equatable, Sendable {
public var id: StageKind { kind }
public let kind: StageKind
public let status: StageStatus
public init(kind: StageKind, status: StageStatus) {
self.kind = kind
self.status = status
}
}
public let role: Role
public let stages: [Stage]
public let recommendation: Recommendation
public let failureKind: DiagnosticFailureKind?
public let selectedPath: CmxIrohSelectedTransportPath
public var isReady: Bool {
!stages.contains { $0.status == .failed }
}
public init(
role: Role,
snapshot: CmxIrohSettingsSnapshot,
diagnostics: DiagnosticReport,
relayReachability: RelayReachability,
macDiscovery: MacDiscovery = .unavailable
) {
self.role = role
failureKind = diagnostics.lastFailureKind
selectedPath = snapshot.selectedTransportPath
let transportStatus: StageStatus = switch snapshot.runtimeStatus {
case .inactive, .degraded: .failed
case .starting: .warning
case .active, .direct, .relayed, .privateNetwork: .passed
}
let policyStatus: StageStatus = switch snapshot.policySource {
case .server: .passed
case .cached: .warning
case .unavailable: .failed
}
let relayStatus: StageStatus = switch relayReachability {
case .notConfigured: .notApplicable
case .reachable: .passed
case .unreachable: .failed
case .unavailable: .failed
}
let discoveryStatus: StageStatus
let sessionStatus: StageStatus
switch role {
case .macHost:
discoveryStatus = .notApplicable
sessionStatus = .notApplicable
case .mobileClient:
discoveryStatus = switch macDiscovery {
case .found: .passed
case .missing, .unavailable: .failed
}
sessionStatus = snapshot.selectedTransportPath == .unavailable ? .failed : .passed
}
stages = [
Stage(kind: .encryptedTransport, status: transportStatus),
Stage(kind: .relayPolicy, status: policyStatus),
Stage(kind: .relayReachability, status: relayStatus),
Stage(kind: .macDiscovery, status: discoveryStatus),
Stage(kind: .secureSession, status: sessionStatus),
]
recommendation = Self.recommendation(
role: role,
transportStatus: transportStatus,
policyStatus: policyStatus,
relayReachability: relayReachability,
discoveryStatus: discoveryStatus,
sessionStatus: sessionStatus,
failureKind: diagnostics.lastFailureKind,
hasRelayConfigurationProblem: !snapshot.staleRelayIDs.isEmpty
|| snapshot.failureDescription != nil
)
}
private static func recommendation(
role: Role,
transportStatus: StageStatus,
policyStatus: StageStatus,
relayReachability: RelayReachability,
discoveryStatus: StageStatus,
sessionStatus: StageStatus,
failureKind: DiagnosticFailureKind?,
hasRelayConfigurationProblem: Bool
) -> Recommendation {
if transportStatus == .failed, failureKind == .offline { return .checkInternet }
if policyStatus == .failed || hasRelayConfigurationProblem {
return .reviewRelaySettings
}
// Corporate-allowlist advice requires a relay that was actually probed
// and blocked. An unavailable probe is indeterminate (inactive runtime,
// unreadable path hints), so it must never send users to IT.
if relayReachability == .unreachable { return .allowRelayTraffic }
if transportStatus == .failed { return .refreshAccount }
if role == .mobileClient, discoveryStatus == .failed { return .openMacApp }
if role == .mobileClient, sessionStatus == .failed {
switch failureKind {
case .identityMismatch, .accountMismatch, .authorizationFailed,
.admissionDenied, .secureChannelFailed:
return .updateOrRepair
default:
return .retry
}
}
if relayReachability == .unavailable { return .retry }
return .none
}
}
@@ -1,6 +1,6 @@
public import Foundation
/// Release-safe, device-local Iroh path constraint chosen in Settings.
/// Legacy device-local Iroh path preference retained for version compatibility.
public enum CmxIrohPathPreference: String, CaseIterable, Equatable, Sendable {
/// Allows Iroh to select automatic, direct, private-network, or relay paths.
case automatic = "auto"
@@ -8,14 +8,21 @@ public enum CmxIrohPathPreference: String, CaseIterable, Equatable, Sendable {
/// Keeps Iroh connections on relay paths.
case relayOnly
/// Prevents this device from listening or dialing through Iroh relays.
case neverUseRelays
/// Shared defaults key used independently by the macOS and iOS apps.
public static let defaultsKey = "cmux.iroh.pathPreference"
/// The transport constraint this preference imposes on the runtime.
/// The release transport mode after normalizing retired preferences.
///
/// Relay-only is a DEBUG verification mode. A value persisted by an older
/// release must not constrain a current production connection.
public var transportVerificationMode: CmxIrohTransportVerificationMode {
switch self {
case .automatic: .automatic
case .relayOnly: .relayOnly
case .relayOnly: .automatic
case .neverUseRelays: .directOnly
}
}
@@ -0,0 +1,26 @@
import Foundation
public extension Sequence where Element == String {
/// Returns unique, sorted, credential-free HTTPS origins suitable for an IT allowlist.
func cmxIrohCanonicalRelayOrigins() -> [String] {
Array(Set(compactMap { rawValue in
guard let components = URLComponents(string: rawValue),
components.scheme?.lowercased() == "https",
let host = components.host,
!host.isEmpty,
components.user == nil,
components.password == nil,
components.query == nil,
components.fragment == nil,
components.path.isEmpty || components.path == "/" else {
return nil
}
var origin = URLComponents()
origin.scheme = "https"
origin.host = host
origin.port = components.port
return origin.string
})).sorted()
}
}
@@ -27,12 +27,19 @@ public protocol CmxIrohSettingsControlling: AnyObject {
/// Probes one custom relay without changing the active preference.
func testIrohCustomRelay(id: String) async -> CmxIrohRelayTestResult
/// Runs a bounded, privacy-safe check of the active encrypted connection path.
func runIrohConnectionCheck() async -> CmxIrohConnectionCheckReport
/// Persists one device-local custom private-path configuration.
func upsertIrohCustomPrivatePath(_ path: CmxIrohCustomPrivatePathDraft) async throws
/// Removes this device's custom private paths for one Mac.
func removeIrohCustomPrivatePath(macDeviceID: String) async throws
/// Restores the active networking choices to their safe defaults without
/// deleting saved relay definitions or private addresses.
func resetIrohSettingsToDefaults() async throws
/// Fetches the latest signed fleet and account preference.
func refreshIrohSettings() async
@@ -51,6 +58,16 @@ public protocol CmxIrohSettingsControlling: AnyObject {
}
public extension CmxIrohSettingsControlling {
func runIrohConnectionCheck() async -> CmxIrohConnectionCheckReport {
CmxIrohConnectionCheckReport(
role: .mobileClient,
snapshot: await irohSettingsSnapshot(),
diagnostics: await irohDiagnosticReport(),
relayReachability: .unavailable,
macDiscovery: .unavailable
)
}
func setIrohPathPreference(_ preference: CmxIrohPathPreference) async throws {
throw CmxIrohSettingsControlError.unsupported
}
@@ -63,6 +80,35 @@ public extension CmxIrohSettingsControlling {
throw CmxIrohSettingsControlError.unsupported
}
func resetIrohSettingsToDefaults() async throws {
let snapshot = await irohSettingsSnapshot()
var firstError: (any Error)?
do {
try await setIrohRelayPreference(.automatic)
} catch {
firstError = error
}
do {
try await setIrohPathPreference(.automatic)
} catch {
firstError = firstError ?? error
}
for privateNetwork in snapshot.customPrivateNetworks where privateNetwork.isEnabled {
do {
try await upsertIrohCustomPrivatePath(.init(
macDeviceID: privateNetwork.macDeviceID,
macDisplayName: privateNetwork.macDisplayName,
addresses: privateNetwork.addresses,
isEnabled: false
))
} catch {
firstError = firstError ?? error
}
}
if let firstError { throw firstError }
}
func irohDiagnosticReport() async -> DiagnosticReport {
.empty
}
@@ -92,10 +92,16 @@ public struct CmxIrohSettingsSnapshot: Equatable, Sendable {
public struct PrivateNetworkMac: Identifiable, Equatable, Sendable {
public let id: String
public let displayName: String
public let supportsPrivatePaths: Bool
public init(id: String, displayName: String) {
public init(
id: String,
displayName: String,
supportsPrivatePaths: Bool = false
) {
self.id = id
self.displayName = displayName
self.supportsPrivatePaths = supportsPrivatePaths
}
}
@@ -12,9 +12,16 @@ public struct CmxLegacyPrivateNetworkPairingCode: Sendable {
/// Returns a tokenless Tailscale-only v1 pairing URL, or `nil` when the
/// ticket has no Tailscale route to disclose.
public func encode(_ ticket: CmxAttachTicket) throws -> URL? {
public func encode(
_ ticket: CmxAttachTicket,
pairingURLScheme: CmxPairingURLScheme? =
CmxPairingURLSchemeResolver().resolved
) throws -> URL? {
let tailscaleRoutes = ticket.routes.filter { $0.kind == .tailscale }
guard !tailscaleRoutes.isEmpty else { return nil }
guard !tailscaleRoutes.isEmpty,
let scheme = pairingURLScheme?.rawValue else {
return nil
}
let legacyTicket = try CmxAttachTicket(
version: ticket.version,
@@ -35,7 +42,7 @@ public struct CmxLegacyPrivateNetworkPairingCode: Sendable {
encoder.dateEncodingStrategy = .iso8601
let payload = base64URLEncode(try encoder.encode(legacyTicket))
return URL(
string: "\(CmxPairingURLScheme.current)://attach?v=\(legacyTicket.version)&payload=\(payload)"
string: "\(scheme)://attach?v=\(legacyTicket.version)&payload=\(payload)"
)
}
@@ -2,7 +2,7 @@ import Foundation
/// The minimal pairing-QR grammars for Iroh identity and Tailscale routes.
///
/// Current Iroh codes carry only the stable EndpointID:
/// Retained Iroh codes carry only the stable EndpointID:
/// `cmux-ios://attach?v=3&i=<endpoint-id>`.
///
/// The EndpointID is the only value the phone needs before dialing. The
@@ -41,13 +41,12 @@ import Foundation
/// Plain text is also smaller, which lowers the QR version (fewer, larger
/// modules) and makes the code scan faster from a Mac screen.
///
/// Compatibility: these grammars only ever appear in the Mac's pairing QR.
/// v2 remains decodable; an older iPhone presented with a v3 Iroh code gets
/// the existing update-app error and can use the Tailscale compatibility code
/// when one is available. Workspace-scoped tickets, dev loopback tickets, and
/// every RPC consumer
/// keep the compact v1 JSON payload (``CmxAttachTicketCompactCoder``), and the
/// decoder keeps accepting both that and the legacy full-key grammar.
/// Compatibility: the Mac pairing window emits only a Tailscale pairing
/// payload. v3 remains decodable for existing Iroh links and explicit
/// device-attach flows. Workspace-scoped tickets, dev loopback tickets, and
/// every RPC consumer keep the compact v1 JSON payload
/// (``CmxAttachTicketCompactCoder``), and the decoder keeps accepting both that
/// and the legacy full-key grammar.
public struct CmxPairingQRCode: Sendable {
/// The newest grammar version this build can decode.
///
@@ -78,8 +77,13 @@ public struct CmxPairingQRCode: Sendable {
/// route is dropped, never written into a scannable code.
public func encode(
_ ticket: CmxAttachTicket,
routeDisclosureMode: CmxPairingRouteDisclosureMode
routeDisclosureMode: CmxPairingRouteDisclosureMode,
pairingURLScheme: CmxPairingURLScheme? =
CmxPairingURLSchemeResolver().resolved
) -> String? {
guard let scheme = pairingURLScheme?.rawValue else {
return nil
}
let items: [String]
switch routeDisclosureMode {
case .irohIdentityOnly:
@@ -120,7 +124,7 @@ public struct CmxPairingQRCode: Sendable {
// Mac's QR opens the dev iOS build, a release Mac's QR opens the
// release build, and the system camera can no longer hand a beta/prod
// code to a dev build that also claimed the scheme.
return "\(CmxPairingURLScheme.current)://attach?" + items.joined(separator: "&")
return "\(scheme)://attach?" + items.joined(separator: "&")
}
/// Whether `ticket` is expressible in the selected minimal grammar.
@@ -225,7 +229,7 @@ public struct CmxPairingQRCode: Sendable {
/// the minimal grammar).
public func isPairingCodeURLString(_ rawValue: String) -> Bool {
guard let url = URL(string: rawValue),
CmxPairingURLScheme.isPairingScheme(url.scheme),
CmxPairingURLScheme(rawValue: url.scheme) != nil,
url.host == "attach",
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return false
@@ -317,7 +321,10 @@ private extension CmxPairingQRCode {
terminalID: nil,
macDeviceID: "",
macDisplayName: nil,
macPairingCompatibilityVersion: 0,
// v3 is intentionally endpoint-only. `nil` means the QR did not
// make a compatibility claim, unlike v2's explicit unknown value
// (0), which must continue to trigger its legacy warning.
macPairingCompatibilityVersion: nil,
routes: [route],
expiresAt: nil,
authToken: nil
@@ -1,13 +1,14 @@
/// The private-route disclosure policy for a scannable attach payload.
///
/// Callers must choose explicitly so adding a route to a ticket cannot silently
/// add it to a QR code. The legacy mode exists only while released clients still
/// require Tailscale host routes during the Iroh migration.
/// add it to a QR code. The compatibility name is retained because its grammar
/// remains readable by released clients; the Mac pairing window uses it only
/// for the user-selected Tailscale path.
public enum CmxPairingRouteDisclosureMode: Equatable, Sendable {
/// Encode only Iroh EndpointIDs. All Iroh hints and every host/port or URL
/// route are removed.
case irohIdentityOnly
/// Preserve the pre-Iroh compact route grammar for released clients.
/// This may disclose private-network routes and must not become a default.
/// Preserve the pre-Iroh compact route grammar for a Tailscale pairing
/// code. This discloses the selected tailnet destination.
case legacyPrivateNetworkCompatibility
}
@@ -1,75 +1,95 @@
import Foundation
/// The channel-specific URL scheme carried by cmux pairing/attach deep links.
/// One validated URL scheme carried by a cmux pairing or attach deep link.
///
/// All builds used to register and emit one scheme (`cmux-ios`), so scanning a
/// beta/prod pairing QR with the iOS Camera app could open a *dev* build that
/// happened to be installed (the OS picks an arbitrary app when several claim
/// a scheme). The scheme is therefore channel-specific, mirroring how
/// `MobileBuildType` splits channels:
///
/// - **Development (DEBUG)** builds local Xcode and `reload.sh` tagged
/// builds on both Mac and iPhone register and emit ``development``.
/// - **Release** builds (TestFlight beta and App Store prod) register and
/// emit ``release``. Beta and prod share a scheme because they are the same
/// compile configuration and a phone realistically has only one of them.
///
/// Emitters (the Mac building a pairing QR or attach URL) use ``current`` so a
/// dev Mac pairs a dev phone and a release Mac pairs a release phone via the
/// system camera. Parsers (the in-app scanner, manual paste, the root scene's
/// deep-link gate) accept *any* pairing scheme via ``isPairingScheme(_:)`` /
/// ``hasPairingScheme(_:)``, so cross-channel pairing still works when the
/// user scans from inside the app.
///
/// The iOS app's registered scheme comes from `CMUX_IOS_URL_SCHEME` in
/// `ios/Config/Shared.xcconfig` (dev) and `ios/Config/Release.xcconfig`
/// (release); keep those values in sync with these constants.
///
/// lint:allow namespace-type the build channel's URL scheme is a pure
/// compile-time constant set with no per-instance state to inject; these
/// scheme strings and the stateless pairing-scheme predicates are a genuine
/// namespace, like the sanctioned FFI/seam holders.
/// Every installed iOS bundle registers exactly one scheme derived from its
/// complete bundle identifier. Parsers also accept the two historical shared
/// schemes so an old QR remains scannable inside an already-open app, but new
/// apps never register those shared schemes with iOS.
public struct CmxPairingURLScheme {
private init() {}
/// The validated, lowercase URL scheme.
public let rawValue: String
/// The scheme Release (TestFlight beta + App Store) builds register and emit.
/// Creates the exact scheme registered by one installed iOS bundle.
public init?(iOSBundleIdentifier: String?) {
guard let namespace = MobileIOSAppNamespace(
bundleIdentifier: iOSBundleIdentifier
) else {
return nil
}
let scheme = namespace.pairingURLScheme.lowercased()
guard Self.releaseSchemes.contains(scheme)
|| scheme == Self.untaggedDevelopmentScheme
|| scheme.hasPrefix(Self.developmentPrefix) else {
return nil
}
rawValue = scheme
}
/// Parses a classifiable bundle-specific or historical shared pairing
/// scheme. Unknown release-like namespaces fail closed so account preflight
/// cannot be bypassed by a syntactically valid but unclassified scheme.
public init?(rawValue: String?) {
guard let rawValue else { return nil }
let normalized = rawValue.lowercased()
if Self.all.contains(normalized) {
self.rawValue = normalized
return
}
let prefix = "cmux-ios-"
guard normalized.hasPrefix(prefix),
MobileIOSAppNamespace(
bundleIdentifier: String(normalized.dropFirst(prefix.count))
) != nil,
Self.releaseSchemes.contains(normalized)
|| normalized == Self.untaggedDevelopmentScheme
|| normalized.hasPrefix(Self.developmentPrefix) else {
return nil
}
self.rawValue = normalized
}
/// Parses the scheme from a complete pairing URL.
public init?(urlString: String) {
guard urlString.contains("://"),
let components = URLComponents(string: urlString),
let scheme = CmxPairingURLScheme(rawValue: components.scheme) else {
return nil
}
self = scheme
}
/// Whether this scheme identifies a tagged iOS development build.
public var isDevelopment: Bool {
rawValue == Self.development
|| rawValue == Self.untaggedDevelopmentScheme
|| rawValue.hasPrefix(Self.developmentPrefix)
}
/// Whether this scheme identifies an App Store or TestFlight build.
public var isRelease: Bool {
Self.releaseSchemes.contains(rawValue)
}
/// Historical shared Release scheme. Parse-only in new iOS builds.
public static let release = "cmux-ios"
/// The scheme development (DEBUG/tagged) builds register and emit.
/// Historical shared development scheme. Parse-only in new iOS builds.
public static let development = "cmux-ios-dev"
/// Every scheme any cmux build may emit; parsers accept all of them.
/// Historical schemes retained for source compatibility and old QR tests.
public static let all: [String] = [release, development]
/// The scheme this build emits in pairing QRs and attach URLs.
public static var current: String {
scheme(isDevelopmentBuild: isDevelopmentBuild)
}
private static let untaggedDevelopmentScheme = "cmux-ios-dev.cmux.ios"
private static let developmentPrefix = "cmux-ios-dev.cmux.ios."
/// Pure channel-to-scheme mapping, injected with the compile flag so the
/// derivation is testable from a single build configuration.
public static func scheme(isDevelopmentBuild: Bool) -> String {
isDevelopmentBuild ? development : release
}
/// Whether `scheme` is a pairing scheme from any cmux channel.
public static func isPairingScheme(_ scheme: String?) -> Bool {
guard let scheme else { return false }
return all.contains { $0.caseInsensitiveCompare(scheme) == .orderedSame }
}
/// Whether `rawValue` starts with any channel's pairing scheme (the
/// scanner/paste-side prefix check, before URL parsing).
public static func hasPairingScheme(_ rawValue: String) -> Bool {
let lowercased = rawValue.lowercased()
return all.contains { lowercased.hasPrefix($0 + "://") }
}
private static var isDevelopmentBuild: Bool {
#if DEBUG
true
#else
false
#endif
}
private static let releaseSchemes: Set<String> = [
release,
"cmux-ios-com.cmux.app",
"cmux-ios-dev.cmux.app.beta",
"cmux-ios-dev.cmux.app.internal",
"cmux-ios-dev.cmux.app.demo",
]
}
extension CmxPairingURLScheme: Equatable, Sendable {}
@@ -0,0 +1,71 @@
import Foundation
/// Resolves the pairing target for the current process without global state.
public struct CmxPairingURLSchemeResolver: Sendable {
private let currentIOSBundleIdentifier: String?
private let targetIOSBundleIdentifier: String?
private let macInstanceTag: String?
private let isDevelopmentBuild: Bool
/// Captures the current app identity and any explicit Mac pairing target.
///
/// A Mac may set `CMUX_IOS_PAIRING_BUNDLE_IDENTIFIER` to any authoritative
/// release-lane bundle id. Tagged Mac builds otherwise target their exact
/// same-tag iOS bundle.
public init(
bundle: Bundle = .main,
environment: [String: String] = ProcessInfo.processInfo.environment
) {
currentIOSBundleIdentifier = bundle.bundleIdentifier
targetIOSBundleIdentifier =
environment["CMUX_IOS_PAIRING_BUNDLE_IDENTIFIER"]
macInstanceTag = environment["CMUX_TAG"]
#if DEBUG
isDevelopmentBuild = true
#else
isDevelopmentBuild = false
#endif
}
init(
currentIOSBundleIdentifier: String?,
targetIOSBundleIdentifier: String?,
macInstanceTag: String?,
isDevelopmentBuild: Bool
) {
self.currentIOSBundleIdentifier = currentIOSBundleIdentifier
self.targetIOSBundleIdentifier = targetIOSBundleIdentifier
self.macInstanceTag = macInstanceTag
self.isDevelopmentBuild = isDevelopmentBuild
}
/// The exact scheme this process should emit, or `nil` on invalid identity.
public var resolved: CmxPairingURLScheme? {
#if os(iOS)
return CmxPairingURLScheme(
iOSBundleIdentifier: currentIOSBundleIdentifier
)
#else
if let targetIOSBundleIdentifier {
return CmxPairingURLScheme(
iOSBundleIdentifier: targetIOSBundleIdentifier
)
}
if macInstanceTag == nil || macInstanceTag?.isEmpty == true {
return CmxPairingURLScheme(
iOSBundleIdentifier: isDevelopmentBuild
? "dev.cmux.ios"
: "com.cmux.app"
)
}
guard let namespace = MobileIOSAppNamespace(
pairedMacInstanceTag: macInstanceTag
) else {
return nil
}
return CmxPairingURLScheme(
iOSBundleIdentifier: namespace.bundleIdentifier
)
#endif
}
}
@@ -48,7 +48,12 @@ public struct CmxTailscalePeerAddress: Hashable, Sendable {
let bytes = withUnsafeBytes(of: &address) { Array($0) }
var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN))
guard inet_ntop(AF_INET, &address, &buffer, socklen_t(buffer.count)) != nil else { return nil }
return (decode(buffer), bytes)
let canonical = decode(buffer)
// Darwin's inet_pton accepts leading-zero octets as decimal while the
// dialer's inet_aton reads them as octal. Refuse any non-canonical
// spelling so classification never diverges from dialing.
guard canonical == value else { return nil }
return (canonical, bytes)
}
private static func parseIPv6(_ value: String) -> (canonical: String, bytes: [UInt8])? {
@@ -422,6 +422,15 @@ public protocol CmxByteTransportContinuityIdentifying: CmxByteTransport {
func transportContinuityID() async -> UInt64?
}
/// Optional privacy-safe link from a byte dial to the admitted transport
/// session that backs it. The value is process-local and never leaves the
/// diagnostic ring.
public protocol CmxByteTransportDiagnosticSessionIdentifying: CmxByteTransport {
/// Returns the current admitted session ID, or `nil` before connection or
/// after the session has been released.
func transportDiagnosticSessionID() async -> Int?
}
/// A privacy-safe handle that waits for one exact native transport to close.
///
/// The handle captures the transport generation at creation time, so callers

Some files were not shown because too many files have changed in this diff Show More