Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
196d7b9
test(terminal): restore the presented-surface fixture contract so pac…
austinywang Sep 21, 2026
258b283
fix(popover): stop rewriting the presentation binding during view update
austinywang Sep 21, 2026
b30b030
test(cloud): satisfy the plus menu's sign-in gate and route Cmd+Y thr…
austinywang Sep 21, 2026
13fad29
test(chrome): assert the composited Bonsplit chrome contract instead …
austinywang Sep 21, 2026
626112b
test(tmux): wait for the main window to adopt the mirror pane before …
austinywang Sep 21, 2026
b3755b8
test(browser): report the refused connection through the navigation d…
austinywang Sep 21, 2026
dbdcd23
fix(cloud): scope reserved-workspace cleanup to its pending card and …
austinywang Sep 21, 2026
0d6df3a
fix(cli): restore deferred socket connection, explicit SSH control op…
austinywang Sep 21, 2026
b602efb
test(cli): align CLI integration fixtures with the shipped hook, SSH,…
austinywang Sep 21, 2026
18e3c96
test(workspace): restore the app-host repairs for fork, focus recover…
austinywang Sep 21, 2026
70ecc1a
fix(restore): keep a restore identity when the persisted surface id c…
austinywang Sep 21, 2026
9f258dd
test(terminal): align snapshot, projection, terminal, and browser fix…
austinywang Sep 21, 2026
77bfa38
test(browser): align lifecycle, identity, host-view, loopback bridge,…
austinywang Sep 21, 2026
1e62556
test(terminal): wait for asynchronous runtime creation in the remaini…
austinywang Sep 21, 2026
cbc73f7
test(cli): model surface.respawn for respawn-pane and give the Codex …
austinywang Sep 21, 2026
366492b
fix(socket): keep mobile.panel.artifact.fetch off the local socket an…
austinywang Sep 21, 2026
5510126
test(remote): align tmux seed transport, SSH, socket command, and por…
austinywang Sep 21, 2026
a956dae
repair: refresh full-suite fixes on current main
teamleaderleo Sep 21, 2026
2c55871
test: align mobile artifact fetch lane assertion
teamleaderleo Sep 21, 2026
d5cedc9
Merge remote-tracking branch 'origin/main' into fix/main-ci-full-suite
austinywang Sep 22, 2026
e6926fb
repair: close remaining full-suite contracts
austinywang Sep 22, 2026
4c22264
Merge remote-tracking branch 'origin/main' into fix/main-ci-full-suite
austinywang Sep 22, 2026
84e70db
test(restore,sidebar): align two stale contracts with shipped behavior
austinywang Sep 22, 2026
c875d8d
Merge remote-tracking branch 'upstream/main' into fix/app-host-green
teamleaderleo Sep 22, 2026
3dc91b2
test: give Cloud catalog tests live destination workspaces
teamleaderleo Sep 22, 2026
4d1f51e
chore: normalize pbxproj after live workspace fixture
teamleaderleo Sep 22, 2026
999b359
fix: admit agent renames of agent-owned accepted Cloud names
teamleaderleo Sep 22, 2026
17381ac
test: expect adopted machine rows to keep the pending create identity
teamleaderleo Sep 22, 2026
70eaf44
fix: restore per-display noVNC targets and refresh stale Cloud tests
teamleaderleo Sep 22, 2026
42ae036
fix: publish every guest display from daemon graph updates
teamleaderleo Sep 22, 2026
e167c15
test: fence the fake Cloud projection reply with its mutation cursor
teamleaderleo Sep 22, 2026
de81ebd
test: register the restored window before relinking Cloud projections
teamleaderleo Sep 22, 2026
22a62e0
test: wait for the relay ports kick, not the first relay line
teamleaderleo Sep 22, 2026
9d37670
fix(cli): relay output of an SSH session that ends right after auth
teamleaderleo Sep 22, 2026
7b985be
test: restore the no-connection path for rejected vm dev input
teamleaderleo Sep 22, 2026
bafd6c3
test: drain the terminal while the SCP host-key failure runs
teamleaderleo Sep 22, 2026
a91a26c
Merge #13655 into fix/app-host-green
teamleaderleo Sep 22, 2026
f7132b3
Merge #13657 into fix/app-host-green
teamleaderleo Sep 22, 2026
4df563e
test: fail fast when a gated Cloud call ends before its fake is entered
teamleaderleo Sep 22, 2026
49e5dda
test: reveal a retired terminal through the portal rebind, as the app…
teamleaderleo Sep 22, 2026
30590ad
test: pin key-window status in terminal focus suites
teamleaderleo Sep 22, 2026
41d8f87
test: report which layer holds off-plan tmux mirror geometry
teamleaderleo Sep 22, 2026
2f6adcc
Merge #13664 into fix/app-host-green
teamleaderleo Sep 22, 2026
ecc8044
fix: restore Cloud projections before the workspace is published
teamleaderleo Sep 22, 2026
8ca302a
test: repair stale and broken app-host expectations
teamleaderleo Sep 22, 2026
b102403
Merge remote-tracking branch 'upstream/main' into fix/app-host-green
teamleaderleo Sep 22, 2026
5a92d7f
Merge #13678 into fix/app-host-green
teamleaderleo Sep 22, 2026
87a7016
test: activate the app host before waiting for terminal focus
teamleaderleo Sep 22, 2026
a682c55
test: give standalone terminal fixtures a live portal authority
teamleaderleo Sep 22, 2026
e28ef70
Merge remote-tracking branch 'origin/main' into fix/app-host-green
teamleaderleo Sep 22, 2026
e160e82
chore: normalize project.pbxproj after merging main
teamleaderleo Sep 22, 2026
48e712f
Drop a cancelled fork-probe request, and name the inputs behind the l…
teamleaderleo Sep 22, 2026
938dbab
test: repair the remaining app-host failures and the shard-2 host rel…
teamleaderleo Sep 23, 2026
43d7f88
Merge main and repair Grok path preservation and focus-history templa…
teamleaderleo Sep 23, 2026
89bf290
test: measure Cloud title ink beyond the icon and trace measured inva…
teamleaderleo Sep 23, 2026
5c9e83b
test: preserve carrier preparation before fleet discovery
teamleaderleo Sep 23, 2026
3c0e280
fix: retain independent cloud carrier prewarming
teamleaderleo Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 2 additions & 14 deletions CLI/CMUXCLI+AgentHookDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -428,20 +428,8 @@ extension CMUXCLI {
// environments, and for a stale socket node left by an exited app: the
// ambient invocation must succeed, otherwise the pinned chain runs.
// https://github.com/manaflow-ai/cmux/issues/5473
let ambientGuard: String
let ambientInvocation: String
if def.name == "grok" {
// Grok validates interpolated CMUX variables as required hook
// environment. Optional lookups preserve ambient-first routing
// when present and allow the pinned fallback when Grok strips them.
let socket = "\"$(printenv CMUX_SOCKET_PATH || true)\""
let executable = "\"$(printenv CMUX_BUNDLED_CLI_PATH || true)\""
ambientGuard = "[ -n \(socket) ] && [ -S \(socket) ] && [ -f \(executable) ] && [ -x \(executable) ]"
ambientInvocation = "\(pinnedHookEnvironmentPrefix(routedArguments: routedArguments))\(executable) --socket \(socket) \(routedArguments)"
} else {
ambientGuard = pinnedHookAmbientDispatchGuard
ambientInvocation = pinnedHookAmbientInvocation(routedArguments: routedArguments)
}
let ambientGuard = pinnedHookAmbientDispatchGuard
let ambientInvocation = pinnedHookAmbientInvocation(routedArguments: routedArguments)
let dispatch: String
if let cliPath = pinnedAgentHookCLIPath() {
let quotedCLIPath = shellSingleQuote(cliPath)
Expand Down
16 changes: 14 additions & 2 deletions CLI/CMUXCLI+SSHStartupScripts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,13 @@ extension CMUXCLI {
" if {[string length $cmux_buffer] > 0} { send_user -- $cmux_buffer }",
" cmux_relay_session",
" }",
" eof { set status [wait]; exit [lindex $status 3] }",
// A session that authenticates and ends inside this window still
// owes the user its output: flush what expect buffered before exiting.
" eof {",
" catch { send_user -- $expect_out(buffer) }",
" set status [wait]",
" exit [lindex $status 3]",
" }",
" }",
"}",
"expect {",
Expand Down Expand Up @@ -217,7 +223,13 @@ extension CMUXCLI {
" if {[string length $cmux_buffer] > 0} { send_user -- $cmux_buffer }",
" cmux_relay_session",
" }",
" eof { set status [wait]; exit [lindex $status 3] }",
// A session that authenticates and ends inside this window still
// owes the user its output: flush what expect buffered before exiting.
" eof {",
" catch { send_user -- $expect_out(buffer) }",
" set status [wait]",
" exit [lindex $status 3]",
" }",
" }",
"}",
"expect {",
Expand Down
3 changes: 3 additions & 0 deletions CLI/CMUXCLI+VMSCP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ extension CMUXCLI {
"-F", "/dev/null",
"-o", "StrictHostKeyChecking=yes",
"-o", "HostKeyAlgorithms=ssh-ed25519", "-o", "HostKeyAlias=cmux-scp",
// Keep host-key failures actionable while avoiding OpenSSH's
// multi-page warning banner filling a PTY-backed stderr pipe.
"-o", "LogLevel=ERROR",
"-o", "UserKnownHostsFile=" + directory.appendingPathComponent("known_hosts").path.replacingOccurrences(of: "%", with: "%%"),
"-o", "GlobalKnownHostsFile=/dev/null",
"-o", "ConnectTimeout=15", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3",
Expand Down
34 changes: 27 additions & 7 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35979,19 +35979,18 @@ export default CMUXSessionRestore;
// this narrow transcript check for pre-ledger launches so stale
// prompt-depth records cannot strand an older session; it is never
// part of the modern child-work decision.
let activePromptTurnStackForStop = mapped?.activePromptTurnIds?
.compactMap({ normalizedHookValue($0) }) ?? []
let activePromptTurnIdsForStop = activePromptTurnStackForStop.isEmpty
? normalizedHookValue(mapped?.activePromptTurnId).map { [$0] } ?? []
: activePromptTurnStackForStop
let terminalActivePromptTurnIdsForStop: Set<String>
if !relayOrigin,
!staleIdleStopHasNewerRunningSession,
def.name == "codex",
codexLifecycle?.usesLegacyIdentity == true,
let incomingTurnId = normalizedHookValue(input.turnId) {
let activePromptTurnStack = mapped?.activePromptTurnIds?
.compactMap({ normalizedHookValue($0) }) ?? []
let activePromptTurnId = activePromptTurnStack.last ?? normalizedHookValue(mapped?.activePromptTurnId)
let activeTurnIds = activePromptTurnStack.isEmpty
? activePromptTurnId.map { [$0] } ?? []
: activePromptTurnStack
let activeTurnIdsToCheck = activeTurnIds.filter { $0 != incomingTurnId }
let activeTurnIdsToCheck = activePromptTurnIdsForStop.filter { $0 != incomingTurnId }
if !activeTurnIdsToCheck.isEmpty,
let transcriptPath = normalizedHookValue(localTranscriptPath(mapped: mapped))
?? findCodexTranscriptPath(sessionId: sessionId, env: env) {
Expand Down Expand Up @@ -36133,6 +36132,27 @@ export default CMUXSessionRestore;
}
}

if def.name == "codex", codexLifecycle?.usesLegacyIdentity == true,
!suppressCompletionNotification {
for priorTurnId in activePromptTurnIdsForStop
where terminalActivePromptTurnIdsForStop.contains(priorTurnId) {
emitAgentJournalEvent(
client: client,
kind: .idleObserved,
source: def.name,
agentKey: def.statusKey,
sessionId: sessionId,
workspaceId: workspaceId,
surfaceId: surfaceId,
nativeEvent: "transcript-terminal",
attention: AgentAttentionContext(turnIdentity: priorTurnId),
occurredAtMs: Self.semanticOccurredAtMs(input.rawObject),
store: store,
telemetry: telemetry
)
}
}

// The journal records the turn boundary unconditionally: the
// reducer's per-session fold handles stale sessions (a newer
// running session outranks this one) and subagent tagging keeps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,11 @@ public enum ControlCommandExecutionPolicy: Sendable, Equatable {
// surfaces. Keep them on the worker lane so markdown/file-preview panes
// reach TerminalController's mobile.panel.artifact.* dispatcher instead
// of the main-actor switch returning method_not_found.
// `mobile.panel.artifact.fetch` is deliberately absent: it needs the
// authenticated mobile RPC execution context, so the local control
// socket answers method_not_found instead of bypassing
// artifact-transfer authorization (the worker switch has no case for it).
"mobile.panel.artifact.stat",
"mobile.panel.artifact.fetch",
"mobile.panel.artifact.thumbnail",
"system.top",
"system.memory",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ struct ControlCommandExecutionPolicyTests {
"vault.sessions", "vault.search", "vault.checkpoints",
"vault.checkpoint", "vault.fork",
"mobile.compatible_tags.get", "mobile.compatible_tags.set",
"mobile.panel.artifact.stat", "mobile.panel.artifact.fetch",
"mobile.panel.artifact.thumbnail",
"mobile.panel.artifact.stat", "mobile.panel.artifact.thumbnail",
// JavaScript-evaluating browser methods block on page JS and must
// not hold the main actor (see socketWorkerMethods rationale).
"browser.eval", "browser.wait", "browser.snapshot", "browser.click",
Expand Down Expand Up @@ -89,6 +88,9 @@ struct ControlCommandExecutionPolicyTests {
"workspace.create", "browser.url.get",
"browser.open_split", "browser.get.title", "browser.frame.main",
"mobile.terminal.create", "mobile.task.attachment.upload",
// Artifact fetch needs the authenticated mobile execution context;
// the local socket must answer method_not_found, not serve bytes.
"mobile.panel.artifact.fetch",
"vmx.create", "",
// Focus-intent verbs stay on the main lane until the mutations
// tranche decides them deliberately.
Expand Down Expand Up @@ -174,7 +176,7 @@ struct ControlCommandExecutionPolicyTests {
#expect(ControlCommandExecutionPolicy(forMethod: "system.top") == .socketWorker(mainThreadCallable: false))
#expect(ControlCommandExecutionPolicy(forMethod: "mobile.task.models.list") == .socketWorker(mainThreadCallable: false))
#expect(ControlCommandExecutionPolicy(forMethod: "mobile.panel.artifact.stat") == .socketWorker(mainThreadCallable: false))
#expect(ControlCommandExecutionPolicy(forMethod: "mobile.panel.artifact.fetch") == .socketWorker(mainThreadCallable: false))
#expect(ControlCommandExecutionPolicy(forMethod: "mobile.panel.artifact.fetch") == .mainActor)
#expect(ControlCommandExecutionPolicy(forMethod: "mobile.panel.artifact.thumbnail") == .socketWorker(mainThreadCallable: false))
#expect(ControlCommandExecutionPolicy(forMethod: "vm.create") == .socketWorker(mainThreadCallable: false))
}
Expand Down
69 changes: 66 additions & 3 deletions Resources/markdown-viewer/shell.html
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,13 @@
return Math.max(0, Math.min(markdownMaxScrollY(), y));
}

// `setMarkdownScrollY` is the viewer's only programmatic scroll, so the
// position it last wrote is the position the viewer is in unless something
// else moved it: the reader, a find match, an anchor jump. Recording it lets
// the scroll listener below tell those apart from a reflow, which moves
// content under a stationary viewer without scrolling it.
var markdownScrollYWeWrote = null;

function setMarkdownScrollY(y) {
var targetY = clampScrollY(y);
var scroller = markdownScroller();
Expand All @@ -1201,6 +1208,7 @@
}
} finally {
root.style.scrollBehavior = previousBehavior;
markdownScrollYWeWrote = markdownScrollY();
}
}

Expand Down Expand Up @@ -1233,8 +1241,55 @@
};
}

// The restore runs once synchronously, then again on the next two animation
// frames so late layout (images, web fonts, Mermaid) that moves the anchor is
// still corrected. Those follow-up passes must keep making that correction,
// but must never fight a scroll that happened after the content update: a
// queued pass that fires once the reader -- or a find match, or an anchor
// jump -- has already moved the viewer would yank it back to the pre-update
// position.
//
// The two cases are told apart causally, not by comparing positions. A reflow
// moves content under a stationary viewer and fires no scroll event, so the
// correction still runs -- that is the whole reason the frames exist. A real
// scroll fires one, and since `setMarkdownScrollY` is the viewer's only
// programmatic scroll, any scroll that leaves the viewer somewhere other than
// the position we last wrote came from someone else, and this transaction
// stands down. A new content update also cancels the frames the previous one
// queued.
//
// The synchronous pass is the authoritative one. Nothing in the viewer, and
// nothing in its tests, may treat an animation frame as the completion signal
// for a render: a window that is not on an active display never gets one.
var markdownScrollRestoreFrame = 0;
var markdownScrollRestoreAbandoned = true;
var markdownScrollListenerInstalled = false;

function installMarkdownScrollListenerIfNeeded() {
if (markdownScrollListenerInstalled) { return; }
markdownScrollListenerInstalled = true;
window.addEventListener('scroll', function() {
if (markdownScrollRestoreAbandoned) { return; }
if (markdownScrollYWeWrote === null) { return; }
if (Math.abs(markdownScrollY() - markdownScrollYWeWrote) > 1) {
cancelPendingMarkdownScrollRestore();
}
}, { passive: true });
}

function cancelPendingMarkdownScrollRestore() {
if (markdownScrollRestoreFrame) {
window.cancelAnimationFrame(markdownScrollRestoreFrame);
}
markdownScrollRestoreFrame = 0;
markdownScrollRestoreAbandoned = true;
}

function restoreMarkdownScrollState(state) {
cancelPendingMarkdownScrollRestore();
if (!state) { return; }
installMarkdownScrollListenerIfNeeded();
markdownScrollRestoreAbandoned = false;
function apply() {
var targetY;
if (state.nearTop) {
Expand All @@ -1252,10 +1307,18 @@
}
setMarkdownScrollY(targetY);
}
apply();
window.requestAnimationFrame(function() {
function reapplyUnlessAbandoned() {
markdownScrollRestoreFrame = 0;
if (markdownScrollRestoreAbandoned) { return false; }
apply();
window.requestAnimationFrame(apply);
return true;
}
apply();
markdownScrollRestoreFrame = window.requestAnimationFrame(function() {
if (!reapplyUnlessAbandoned()) { return; }
markdownScrollRestoreFrame = window.requestAnimationFrame(function() {
reapplyUnlessAbandoned();
});
});
}

Expand Down
19 changes: 17 additions & 2 deletions Sources/AppDelegate+NewCloudWorkspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,26 @@ extension AppDelegate {
}
}

/// The window a finished Cloud creation is allowed to navigate: the window
/// that started the creation, and only while that window is still key.
/// A creation that lands while the user is working in another window must
/// not pull them away from it.
///
/// This asks the window itself, the same way every other focus-gated path
/// in the app does, rather than comparing against `NSApp.keyWindow`. The
/// two agree in the running app, and the window-scoped question is the one
/// this rule is actually about.
func cloudWorkspaceCreationFocusWindow(windowID: UUID) -> NSWindow? {
guard let context = mainWindowContexts.values.first(where: { $0.windowId == windowID }),
let window = resolvedWindow(for: context),
window.isKeyWindow else { return nil }
return window
}

private func focusCreatedCloudWorkspace(_ workspaceID: UUID, manager: TabManager?, revision: UInt64, windowID: UUID) {
guard let manager, manager.cloudWorkspaceSelection.revision == revision,
tabManagerFor(windowId: windowID) === manager,
let window = mainWindowContexts.values.first(where: { $0.windowId == windowID }).flatMap({ resolvedWindow(for: $0) }),
window === NSApp.keyWindow,
cloudWorkspaceCreationFocusWindow(windowID: windowID) != nil,
let workspace = manager.workspacesById[workspaceID] else { return }
manager.selectWorkspace(workspace)
}
Expand Down
10 changes: 8 additions & 2 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9719,8 +9719,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
// An addressable duplicate window can fail closed during reindexing;
// retain the validated active owner until routing resolves rather than
// pruning it merely because its cached AppKit identity is transiently
// absent. Windowless app shortcuts still prune as before.
if (event == nil || eventContext != nil) && pruneWindowlessActiveMainWindowContext() {
// absent. That race only exists when the event names a window, so gate
// the deferral on the event's addressability: an event that carries no
// window at all (windowNumber 0 responder-chain shortcuts) has no
// reindexing candidate to protect, and must still prune as before.
let eventDefersActiveContextPrune = event.map { event in
eventContext == nil && shortcutEventHasAddressableWindow(event)
} ?? false
if !eventDefersActiveContextPrune && pruneWindowlessActiveMainWindowContext() {
#if DEBUG
logWorkspaceCreationRouting(
phase: "choose",
Expand Down
8 changes: 4 additions & 4 deletions Sources/Cloud/CloudTreeNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,8 @@ enum CloudTreeNodeBuilder {
let projectionIndex = LocalProjectionIndex(snapshot: snapshot, unreadTerminalIDs: unreadTerminalIDs)
let resourceNodeBuilder = CloudTreeMachineResourceNodeBuilder()
var identities = adoptedOperationIDs
for operation in pendingCreates where !operation.request.isBaseSetup {
for operation in pendingCreates where !operation.request.isBaseSetup &&
(operation.isRunning || operation.isReconciling) {
if let id = operation.createdMachineID ?? operation.reconcilingMachineID, identities[id] == nil {
identities[id] = operation.id
}
Expand All @@ -600,9 +601,8 @@ enum CloudTreeNodeBuilder {
nodes.append(CloudTreeNode(id: nodeID(pendingCreate: operation.id), kind: .pendingMachine(operation)))
}
let infoByMachine = Dictionary(snapshot.machines.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first })
let failedIDs = Set(pendingCreates.filter { !$0.request.isBaseSetup && $0.failureOutput != nil }.compactMap(\.createdMachineID))
var seen = failedIDs
for machine in machines where !failedIDs.contains(machine.id) {
var seen = Set<String>()
for machine in machines {
seen.insert(machine.id)
let info = infoByMachine[.cloud(machine.id)]
let stableID = identities[machine.id].map { nodeID(pendingCreate: $0) }
Expand Down
5 changes: 4 additions & 1 deletion Sources/Cloud/MachineCreateCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ final class MachineCreateCoordinator {
cancelCreatedMachine: { CloudVMActionLauncher.shared.destroyMachineBestEffort($0) },
cancelOperation: { operation in
guard let workspaceID = operation.request.presentationWorkspaceID else { return }
NewMachineSheetPresenter.closeReservedWorkspace(workspaceID)
NewMachineSheetPresenter.closeReservedWorkspace(
workspaceID,
machineID: operation.createdMachineID ?? operation.reconcilingMachineID
)
}
)
static let didChangeNotification = Notification.Name("cmux.machineCreate.didChange")
Expand Down
Loading
Loading