-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat: AI-powered tab & workspace auto-naming for Claude Code #2043
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
7498a74
bca851e
e9d0968
b1b0f8b
22de633
8fb88cf
e7c8694
be922f6
4a358ca
b73d346
5bb721d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| #!/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. | ||
| # umask 077 → 0600: marker contains conversation-derived text. | ||
| (umask 077; printf '%s\n' "$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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| #!/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}" | ||
|
|
||
| # Stale-owner recovery: if an owner file exists but that surface is no longer | ||
| # active in this workspace, clear the file so a live session can claim ownership. | ||
| # This prevents the workspace name from freezing after the original owner exits. | ||
| if [ -f "$WS_OWNER_FILE" ]; then | ||
| STALE_OWNER=$(cat "$WS_OWNER_FILE" 2>/dev/null || true) | ||
| if [ -n "$STALE_OWNER" ] && [ "$STALE_OWNER" != "$CMUX_SURFACE_ID" ]; then | ||
| # Check if the owner surface is still active in this workspace. | ||
| # Capture output first so a transient cmux error (empty output) is | ||
| # not mistaken for "owner not present" — only delete if the command | ||
| # succeeded AND the owner ID is absent from the listing. | ||
| LIVE_SURFACES=$(cmux list-surfaces --workspace "$CMUX_WORKSPACE_ID" --id-format uuids 2>/dev/null || true) | ||
| if [ -n "$LIVE_SURFACES" ] && ! printf '%s' "$LIVE_SURFACES" | grep -qF "$STALE_OWNER"; then | ||
| rm -f "$WS_OWNER_FILE" 2>/dev/null | ||
| fi | ||
| fi | ||
| fi | ||
|
Comment on lines
+25
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| # Atomic owner claim via noclobber (O_EXCL): only the first concurrent | ||
| # SessionStart wins; others silently skip. | ||
| ( set -o noclobber | ||
| printf '%s\n' "$CMUX_SURFACE_ID" > "$WS_OWNER_FILE" | ||
| ) 2>/dev/null || true | ||
|
|
||
| # 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. | ||
| # Write with umask 077 (0600) — marker contains conversation-derived text. | ||
| (umask 077; printf '%s\n' "$TITLE" > "$CUSTOM_MARKER") | ||
| cmux rename-tab --workspace "$CMUX_WORKSPACE_ID" --surface "$CMUX_SURFACE_ID" "$TITLE" 2>/dev/null || true | ||
|
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, cache, and pending label so the first Stop triggers | ||
| # a fresh AI summary and stale pending labels from a prior session are not applied. | ||
| rm -f "$CUSTOM_MARKER" 2>/dev/null | ||
| rm -f "/tmp/cmux-tab-cache-${CMUX_SURFACE_ID}" 2>/dev/null | ||
| rm -f "/tmp/cmux-tab-pending-${CMUX_SURFACE_ID}" 2>/dev/null | ||
|
|
||
| # 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 | ||
| 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 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private struct UITestRenderDiagnosticsSnapshot { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let panelId: UUID | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let drawCount: Int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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]) | |
| } | |
| } |
Uh oh!
There was an error while loading. Please reload this page.