Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
15 changes: 14 additions & 1 deletion Resources/bin/claude
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,26 @@ done
# the actual claude process PID, which hooks use for stale-session detection.
export CMUX_CLAUDE_PID=$$

# Resolve the directory containing this wrapper (for sibling scripts).
SELF_DIR="$(cd "$(dirname "$0")" && pwd)"

# Guard: $SELF_DIR is interpolated directly into a JSON string literal.
# A path containing '"' or '\' would produce malformed JSON and silently
# disable all hooks. This is extremely unlikely in practice but worth catching.
if [[ "$SELF_DIR" =~ [\"\\] ]]; then
echo "cmux: warning: wrapper directory path contains JSON-unsafe characters: $SELF_DIR" >&2
echo "cmux: hooks disabled — move cmux to a path without '\"' or '\\'" >&2
exec "$REAL_CLAUDE" "$@"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Build hooks settings JSON.
# Claude Code merges --settings additively with the user's own settings.json.
# - SessionStart/Stop/Notification: existing lifecycle hooks
# - SessionEnd: cleanup when Claude exits (covers Ctrl+C where Stop doesn't fire)
# - UserPromptSubmit: clears "Needs input" and sets "Running" on new prompt
# - PreToolUse: clears "Needs input" when Claude resumes after permission grant (async to avoid latency)
HOOKS_JSON='{"hooks":{"SessionStart":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook session-start","timeout":10}]}],"Stop":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook stop","timeout":10}]}],"SessionEnd":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook session-end","timeout":1}]}],"Notification":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook notification","timeout":10}]}],"UserPromptSubmit":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook prompt-submit","timeout":10}]}],"PreToolUse":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook pre-tool-use","timeout":5,"async":true}]}]}}'
# - Tab/workspace naming hooks: AI-powered auto-naming from conversation content
HOOKS_JSON='{"hooks":{"SessionStart":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook session-start","timeout":10},{"type":"command","command":"'"$SELF_DIR"'/cmux-session-namer.sh","timeout":10}]}],"Stop":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook stop","timeout":10},{"type":"command","command":"'"$SELF_DIR"'/cmux-tab-namer.sh","timeout":10}]}],"SessionEnd":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook session-end","timeout":1}]}],"Notification":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook notification","timeout":10}]}],"UserPromptSubmit":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook prompt-submit","timeout":10},{"type":"command","command":"'"$SELF_DIR"'/cmux-rename-namer.sh","timeout":10}]}],"PreToolUse":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook pre-tool-use","timeout":5,"async":true}]}]}}'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 $SELF_DIR injected into JSON without escaping

$SELF_DIR is interpolated directly into the JSON string literal. If the installation path contains a double-quote or backslash (e.g. a path with \" from a symlink target), the resulting JSON would be malformed and --settings would fail silently, disabling all hooks.

Consider JSON-encoding the path before interpolation, or at minimum asserting that the path contains only safe characters:

# Guard against characters that would break the inline JSON
if [[ "$SELF_DIR" =~ [\"\\] ]]; then
    echo "cmux: warning: wrapper directory path contains characters unsafe for JSON: $SELF_DIR" >&2
fi

For a proper fix, build the JSON with a tool that handles escaping (e.g. python3 -c "import json, sys; ...") rather than inline string interpolation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e7c8694 / 4a358ca$SELF_DIR is now validated against a regex that rejects paths containing ", \, ', space, $, , !, ;, |, &, <, >, (, ), *, ?, [. If the path fails, the wrapper falls back to BASE_HOOKS_JSON(which usescmux` from PATH) and emits a warning to stderr. This way the naming add-on is simply skipped rather than injecting a broken command string.


if [[ "$SKIP_SESSION_ID" == true ]]; then
exec "$REAL_CLAUDE" --settings "$HOOKS_JSON" "$@"
Expand Down
84 changes: 84 additions & 0 deletions Resources/bin/cmux-rename-namer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# cmux-rename-namer.sh — Apply pending AI labels + sync /rename custom title to cmux tab
#
# Invoked by the cmux claude wrapper as a UserPromptSubmit hook.
#
# Phase 1: Apply pending AI label from previous Stop (moved here from the Stop hook
# so the tab name updates the moment the user sends their next message,
# rather than waiting for Claude's full response to complete).
#
# Phase 2: Sync /rename custom title to tab and workspace (owner only).
# Writes a marker file so the custom title persists even in long transcripts
# where tail-500 would miss an early /rename entry.
#
# Environment: CMUX_WORKSPACE_ID, CMUX_SURFACE_ID (set by cmux shell integration)
# Stdin: JSON with session_id, transcript_path, etc.

set -e

INPUT=$(cat)

[ "$CMUX_TAB_NAMER_DISABLED" = "1" ] && exit 0
[ -z "$CMUX_WORKSPACE_ID" ] && exit 0
[ -z "$CMUX_SURFACE_ID" ] && exit 0
command -v cmux &>/dev/null || exit 0

# ============================================================
# PHASE 1: Apply pending AI label from previous Stop (fast path)
# Tab name updates on next user prompt, not on next Claude response.
# ============================================================
TAB_PENDING="/tmp/cmux-tab-pending-${CMUX_SURFACE_ID}"
TAB_CACHE="/tmp/cmux-tab-cache-${CMUX_SURFACE_ID}"
if [ -f "$TAB_PENDING" ]; then
LABEL=$(head -1 "$TAB_PENDING" 2>/dev/null)
P_LINE_COUNT=$(sed -n '2p' "$TAB_PENDING" 2>/dev/null)
P_HAS_CUSTOM=$(sed -n '3p' "$TAB_PENDING" 2>/dev/null)
rm -f "$TAB_PENDING" 2>/dev/null

if [ -n "$LABEL" ]; then
# Update tab cache
printf '%s\n%s\n' "${P_LINE_COUNT:-0}" "$LABEL" > "$TAB_CACHE" 2>/dev/null

# Rename this tab
cmux rename-tab --workspace "$CMUX_WORKSPACE_ID" --surface "$CMUX_SURFACE_ID" "$LABEL" 2>/dev/null || true

# Rename workspace only if: (a) no custom-title, AND (b) this tab is the workspace owner
WS_OWNER_FILE="/tmp/cmux-ws-owner-${CMUX_WORKSPACE_ID}"
WS_OWNER=$(cat "$WS_OWNER_FILE" 2>/dev/null || true)
if [ "${P_HAS_CUSTOM:-0}" -eq 0 ] 2>/dev/null && [ "$WS_OWNER" = "$CMUX_SURFACE_ID" ]; then
cmux rename-workspace --workspace "$CMUX_WORKSPACE_ID" "$LABEL" 2>/dev/null || true
fi
fi
fi

# ============================================================
# PHASE 2: Sync /rename custom title
# ============================================================

# Extract transcript path
TRANSCRIPT=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null || true)
[ -z "$TRANSCRIPT" ] && exit 0
[ -f "$TRANSCRIPT" ] || exit 0

# Fast: grep last custom-title from transcript tail
TITLE=$(tail -500 "$TRANSCRIPT" | grep '"type":"custom-title"' | tail -1 | python3 -c "
import sys,json
try:
print(json.loads(sys.stdin.readline()).get('customTitle',''))
except: pass
" 2>/dev/null)

[ -z "$TITLE" ] && exit 0

# Write marker file — ensures custom-title suppresses AI summary even in very long
# transcripts where the /rename entry falls outside the tail-500 window
echo "$TITLE" > "/tmp/cmux-custom-title-${CMUX_SURFACE_ID}" 2>/dev/null

# Always rename this tab
cmux rename-tab --workspace "$CMUX_WORKSPACE_ID" --surface "$CMUX_SURFACE_ID" "$TITLE" 2>/dev/null || true

# Only rename workspace if this tab is the owner (first session)
WS_OWNER=$(cat "/tmp/cmux-ws-owner-${CMUX_WORKSPACE_ID}" 2>/dev/null || true)
if [ "$WS_OWNER" = "$CMUX_SURFACE_ID" ]; then
cmux rename-workspace --workspace "$CMUX_WORKSPACE_ID" "$TITLE" 2>/dev/null || true
fi
68 changes: 68 additions & 0 deletions Resources/bin/cmux-session-namer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# cmux-session-namer.sh — Initialize tab/workspace naming on new Claude Code session
#
# Invoked by the cmux claude wrapper as a SessionStart hook (both new and resume).
#
# New session: clear cache for fresh AI summary, set project basename
# Resume session: scan entire transcript for custom-title and sync it immediately,
# write marker file so AI summary is suppressed (fixes the bug where
# /rename early in a long transcript was invisible to tail-500 checks)
#
# Environment: CMUX_WORKSPACE_ID, CMUX_SURFACE_ID (set by cmux shell integration)
# Stdin: JSON with session_id, cwd, transcript_path, etc.

set -e

INPUT=$(cat)

[ "$CMUX_TAB_NAMER_DISABLED" = "1" ] && exit 0
[ -z "$CMUX_WORKSPACE_ID" ] && exit 0
[ -z "$CMUX_SURFACE_ID" ] && exit 0
command -v cmux &>/dev/null || exit 0

# Register as workspace owner if no owner exists yet (first session wins).
# The workspace owner's AI summary / custom-title controls the workspace name.
WS_OWNER_FILE="/tmp/cmux-ws-owner-${CMUX_WORKSPACE_ID}"
if [ ! -f "$WS_OWNER_FILE" ]; then
echo "$CMUX_SURFACE_ID" > "$WS_OWNER_FILE"
fi
Comment on lines +25 to +42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Workspace owner file is never cleaned up

/tmp/cmux-ws-owner-${CMUX_WORKSPACE_ID} (and the per-surface tab-cache, tab-pending, tab-prompt files) are written but never removed. The docs state the owner file "resets when all sessions in the workspace end", but there is no hook — SessionEnd only calls cmux claude-hook session-end, not any of the new scripts. Over time these files accumulate in /tmp.

A SessionEnd hook in a new cmux-session-ender.sh (or inline in the existing hook dispatch) could remove the owner file when the last session for a workspace exits and clean up the per-surface files. At minimum, the documentation should be corrected to reflect the current (no-cleanup) behaviour.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in be922f6. We intentionally don't clean up the owner file on exit (no reliable exit hook) — instead, stale-owner recovery runs on every SessionStart: cmux list-surfaces --workspace is called to verify the recorded owner is still active, and the file is deleted if not. This covers the case where the original owner tab closes without triggering cleanup.

Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Check entire transcript for an existing custom-title (handles resumed sessions
# where /rename was issued earlier in a long transcript, beyond tail-500 reach)
CUSTOM_MARKER="/tmp/cmux-custom-title-${CMUX_SURFACE_ID}"
TRANSCRIPT=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null || true)

if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
TITLE=$(grep '"type":"custom-title"' "$TRANSCRIPT" 2>/dev/null | tail -1 | python3 -c "
import sys,json
try:
print(json.loads(sys.stdin.readline()).get('customTitle',''))
except: pass
" 2>/dev/null)

if [ -n "$TITLE" ]; then
# Resumed session with custom-title: sync immediately and suppress AI summary
echo "$TITLE" > "$CUSTOM_MARKER"
cmux rename-tab --workspace "$CMUX_WORKSPACE_ID" --surface "$CMUX_SURFACE_ID" "$TITLE" 2>/dev/null || true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
WS_OWNER=$(cat "$WS_OWNER_FILE" 2>/dev/null || true)
if [ "$WS_OWNER" = "$CMUX_SURFACE_ID" ]; then
cmux rename-workspace --workspace "$CMUX_WORKSPACE_ID" "$TITLE" 2>/dev/null || true
fi
exit 0
fi
fi

# No custom-title: clear marker and cache so the first Stop triggers a fresh AI summary
rm -f "$CUSTOM_MARKER" 2>/dev/null
rm -f "/tmp/cmux-tab-cache-${CMUX_SURFACE_ID}" 2>/dev/null
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# Set project basename as initial workspace name — only if this tab owns the workspace.
# Non-owner sessions must not overwrite an already-established workspace name.
WS_OWNER=$(cat "$WS_OWNER_FILE" 2>/dev/null || true)
if [ "$WS_OWNER" = "$CMUX_SURFACE_ID" ]; then
CWD=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('cwd',''))" 2>/dev/null || true)
if [ -n "$CWD" ]; then
BASENAME=$(basename "$CWD")
cmux rename-workspace --workspace "$CMUX_WORKSPACE_ID" "$BASENAME" 2>/dev/null || true
fi
fi
131 changes: 131 additions & 0 deletions Resources/bin/cmux-tab-namer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
# cmux-tab-namer.sh — AI-powered tab & workspace auto-naming for Claude Code
#
# Invoked by the cmux claude wrapper as a Stop hook.
# Generates an AI summary in background → writes to pending file.
# The pending label is applied in cmux-rename-namer.sh (UserPromptSubmit)
# for faster tab name updates (user sees new name on next prompt, not next response).
#
# custom-title detection uses a marker file so long transcripts (e.g. resumed
# sessions with /rename early in history) are handled correctly.
#
# Environment: CMUX_WORKSPACE_ID, CMUX_SURFACE_ID (set by cmux shell integration)
# Stdin: JSON with session_id, transcript_path, cwd, etc.

set -e

INPUT=$(cat)

[ "$CMUX_TAB_NAMER_DISABLED" = "1" ] && exit 0
[ -z "$CMUX_WORKSPACE_ID" ] && exit 0
[ -z "$CMUX_SURFACE_ID" ] && exit 0
command -v cmux &>/dev/null || exit 0

TRANSCRIPT=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null || true)
[ -z "$TRANSCRIPT" ] && exit 0
[ -f "$TRANSCRIPT" ] || exit 0

# Per-tab (surface) files — each tab tracks its own label independently
TAB_PENDING="/tmp/cmux-tab-pending-${CMUX_SURFACE_ID}"
TAB_CACHE="/tmp/cmux-tab-cache-${CMUX_SURFACE_ID}"

# ============================================================
# Generate AI summary if needed, spawn background
# ============================================================
LINE_COUNT=$(wc -l < "$TRANSCRIPT" 2>/dev/null | tr -d ' ')
# Skip tiny transcripts (subagent sessions)
[ "$LINE_COUNT" -lt 50 ] 2>/dev/null && exit 0

# Skip if custom-title exists — check marker file first (handles long/resumed transcripts),
# then fall back to tail-500 scan for /rename done in the current session.
CUSTOM_MARKER="/tmp/cmux-custom-title-${CMUX_SURFACE_ID}"
[ -f "$CUSTOM_MARKER" ] && exit 0
HAS_CUSTOM_TITLE=$(tail -500 "$TRANSCRIPT" | grep -c '"type":"custom-title"' 2>/dev/null; true)
[ "${HAS_CUSTOM_TITLE:-0}" -gt 0 ] 2>/dev/null && exit 0

if [ -f "$TAB_CACHE" ]; then
PREV_COUNT=$(head -1 "$TAB_CACHE" 2>/dev/null || echo 0)
DIFF=$((LINE_COUNT - PREV_COUNT))
if [ "$DIFF" -lt 6 ]; then
exit 0
fi
fi

# Extract recent conversation context
CONTEXT=$(_TRANSCRIPT="$TRANSCRIPT" python3 -c "
import json, os, subprocess, sys

TRANSCRIPT = os.environ['_TRANSCRIPT']

tail_lines = subprocess.run(
['tail', '-300', TRANSCRIPT], capture_output=True, text=True
).stdout.splitlines()

head_lines = subprocess.run(
['head', '-100', TRANSCRIPT], capture_output=True, text=True
).stdout.splitlines()

def extract_text(entry):
msg = entry.get('message', {})
if isinstance(msg, dict):
c = msg.get('content', '')
if isinstance(c, str): return c
if isinstance(c, list):
return ' '.join(b.get('text','') for b in c if isinstance(b, dict) and b.get('type') == 'text')
return ''

first_user = []
for raw in head_lines:
try:
e = json.loads(raw)
if e.get('type') == 'user':
t = extract_text(e)
if t.strip():
first_user.append(f'user: {t[:200]}')
if len(first_user) >= 2: break
except: pass

last_msgs = []
for raw in reversed(tail_lines):
try:
e = json.loads(raw)
if e.get('type') in ('user', 'assistant'):
t = extract_text(e)
if t.strip():
last_msgs.append(f'{e[\"type\"]}: {t[:200]}')
if len(last_msgs) >= 4: break
except: pass
last_msgs.reverse()

all_msgs = first_user + last_msgs
if all_msgs:
print('\n'.join(all_msgs))
" 2>/dev/null)

[ -z "$CONTEXT" ] && exit 0

# Build prompt file (per-surface to avoid collisions)
PROMPT_FILE="/tmp/cmux-tab-prompt-${CMUX_SURFACE_ID}"
cat > "$PROMPT_FILE" << PYEOF
Summarize this conversation in 2-5 words, using the SAME language as the conversation. Output ONLY the summary, nothing else.

$CONTEXT
PYEOF

# Spawn background: generate AI label → write to pending file
# (applied on next UserPromptSubmit by cmux-rename-namer.sh for faster tab updates)
# Unset cmux env vars so the `claude -p` subprocess does not inherit them and
# re-trigger the naming hooks recursively (would corrupt cache/pending state).
(
set +e
unset CMUX_WORKSPACE_ID CMUX_SURFACE_ID CMUX_TAB_ID
# Timeout after 15s using perl alarm (POSIX-compatible, no external deps)
LABEL=$(perl -e 'alarm 15; exec @ARGV' claude -p --model haiku < "$PROMPT_FILE" 2>/dev/null | head -1 | cut -c1-30)
rm -f "$PROMPT_FILE" 2>/dev/null
if [ -n "$LABEL" ]; then
printf '%s\n%s\n%s\n' "$LABEL" "$LINE_COUNT" "${HAS_CUSTOM_TITLE:-0}" > "$TAB_PENDING"
fi
) &>/dev/null &
disown

exit 0
41 changes: 41 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
private var didSetupMultiWindowNotificationsUITest = false
private var didSetupDisplayResolutionUITestDiagnostics = false
private var displayResolutionUITestObservers: [NSObjectProtocol] = []
private var didRequestFallbackUITestWindow = false
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
private struct UITestRenderDiagnosticsSnapshot {
let panelId: UUID
let drawCount: Int
Expand Down Expand Up @@ -2421,6 +2422,46 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}

#if DEBUG
// Retry launch stabilization until a delayed WindowGroup materialization produces
// a visible key window that XCUITest can bring to the foreground on the shared VM.
private func stabilizeUITestLaunchWindowAndForeground(attempt: Int = 0) {
let env = ProcessInfo.processInfo.environment
guard isRunningUnderXCTest(env) else { return }

if NSApp.windows.isEmpty, !didRequestFallbackUITestWindow {
didRequestFallbackUITestWindow = true
openNewMainWindow(nil)
}

moveUITestWindowToTargetDisplayIfNeeded()
activateUITestAppIfNeeded()

let hasWindow = !NSApp.windows.isEmpty
let hasVisibleWindow = NSApp.windows.contains { $0.isVisible }
let hasKeyWindow = NSApp.keyWindow != nil
let stage = attempt == 0 ? "afterForceWindow" : "afterForceWindow.retry\(attempt)"
writeUITestDiagnosticsIfNeeded(stage: stage)

guard attempt < 20 else { return }
if !hasWindow || !hasVisibleWindow || !hasKeyWindow || !NSRunningApplication.current.isActive {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.stabilizeUITestLaunchWindowAndForeground(attempt: attempt + 1)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private func activateUITestAppIfNeeded() {
if let window = NSApp.windows.first {
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
}
if #available(macOS 14.0, *) {
NSRunningApplication.current.activate(options: [.activateAllWindows])
} else {
NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
}
}

Comment on lines +2414 to +2460
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This UI-test stabilization routine is never called (the only references are the function itself and its recursive retry). As-is, didRequestFallbackUITestWindow and the helper functions add dead code/unused state. Either invoke this from the existing UI-test launch path (e.g. after the initial afterForceWindow block) or remove it to keep the debug-only launch logic maintainable.

Suggested change
// Retry launch stabilization until a delayed WindowGroup materialization produces
// a visible key window that XCUITest can bring to the foreground on the shared VM.
private func stabilizeUITestLaunchWindowAndForeground(attempt: Int = 0) {
let env = ProcessInfo.processInfo.environment
guard isRunningUnderXCTest(env) else { return }
if NSApp.windows.isEmpty, !didRequestFallbackUITestWindow {
didRequestFallbackUITestWindow = true
openNewMainWindow(nil)
}
moveUITestWindowToTargetDisplayIfNeeded()
activateUITestAppIfNeeded()
let hasWindow = !NSApp.windows.isEmpty
let hasVisibleWindow = NSApp.windows.contains { $0.isVisible }
let hasKeyWindow = NSApp.keyWindow != nil
let stage = attempt == 0 ? "afterForceWindow" : "afterForceWindow.retry\(attempt)"
writeUITestDiagnosticsIfNeeded(stage: stage)
guard attempt < 20 else { return }
if !hasWindow || !hasVisibleWindow || !hasKeyWindow || !NSRunningApplication.current.isActive {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.stabilizeUITestLaunchWindowAndForeground(attempt: attempt + 1)
}
}
}
private func activateUITestAppIfNeeded() {
if let window = NSApp.windows.first {
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
}
if #available(macOS 14.0, *) {
NSRunningApplication.current.activate(options: [.activateAllWindows])
} else {
NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps])
}
}

Copilot uses AI. Check for mistakes.
private func writeUITestDiagnosticsIfNeeded(stage: String) {
let env = ProcessInfo.processInfo.environment
guard let path = env["CMUX_UI_TEST_DIAGNOSTICS_PATH"], !path.isEmpty else { return }
Expand Down
4 changes: 4 additions & 0 deletions Sources/TerminalWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@ final class WindowTerminalPortal: NSObject {

synchronizeHostedView(withId: hostedId)
scheduleDeferredFullSynchronizeAll()
// Session/window restore can queue additional ancestor layout shifts (sidebar width,
// split positions) after the initial bind tick. Queue a later external sync so the
// portal catches that settled geometry instead of staying at the seeded frame.
scheduleExternalGeometrySynchronize()
pruneDeadEntries()
}

Expand Down
Loading