Skip to content

Fix focus reporting leak during pane creation (#2446)#2511

Merged
austinywang merged 5 commits intomainfrom
issue-2446-focus-reporting-leak
Apr 6, 2026
Merged

Fix focus reporting leak during pane creation (#2446)#2511
austinywang merged 5 commits intomainfrom
issue-2446-focus-reporting-leak

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented Apr 1, 2026

Summary

  • Start desiredFocusState = false so new surfaces opt into focus only when the workspace/AppKit focus path explicitly requests it, preventing CSI I/O focus sequences from leaking into shells during pane creation.
  • Pass desiredFocusState through surfaceConfig.focused so Ghostty surfaces can be seeded as unfocused at the C level, eliminating the window where a background pane briefly appears focused.
  • Extract GhosttyKit build/cache logic into scripts/ensure-ghosttykit.sh for reuse by both setup.sh and reload.sh.

Note: Ghostty submodule changes (initial focus seeding in Surface.zig/embedded.zig, DECSET 1004 side-effect-free enablement in stream_handler.zig) are in the working tree but not yet committed to the fork. The ghostty.h header and fork docs are updated in this PR to reflect the intended API.

Closes #2446

Test plan

  • Open a split pane — background pane should not emit CSI I on creation
  • Enable DECSET 1004 in a shell, split — no spurious focus-in sequence in the new pane
  • Restore a multi-pane workspace from layout — only the focused pane reports focus
  • Verify P10k prompt doesn't redraw on pane creation

🤖 Generated with Claude Code


Summary by cubic

Start new Ghostty surfaces unfocused and emit focus sequences only on real focus changes. This stops stray CSI I/O with DECSET 1004 enabled and prevents background panes from briefly appearing focused.

  • Bug Fixes

    • Initialize desiredFocusState = false, pass through as surfaceConfig.focused, and re-apply post‑creation so runtime focus matches any changes during init.
    • Update the ghostty submodule and ghostty.h to seed initial focus and keep DECSET 1004 enablement side‑effect free.
  • Refactors

    • Add scripts/ensure-ghosttykit.sh and invoke it from setup.sh and reload.sh.
    • Build-keyed caching (dirty/untracked detection), bridge header validation, symlinked GhosttyKit.xcframework, pinned checksum for the merged ghostty SHA, and updated fork docs.

Written for commit 7a89bec. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Terminals default to unfocused on creation and support explicit initial focus seeding.
    • DECSET 1004 now avoids emitting focus-report sequences during pane startup.
  • Documentation

    • Added notes about initial-focus plumbing and merge considerations across components.
  • Chores

    • Updated runtime submodule reference and added robust tooling to cache/reuse the framework.
    • Setup/reload scripts now delegate to the new caching helper.

austinywang and others added 2 commits April 1, 2026 01:54
… config

Pass desiredFocusState through surfaceConfig.focused so background panes
start unfocused at the Ghostty level. This prevents CSI I/O focus sequences
from leaking into shells during pane creation when DECSET 1004 is enabled.

Also extracts GhosttyKit build/cache logic into ensure-ghosttykit.sh for
reuse by both setup.sh and reload.sh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Apr 5, 2026 11:46pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7bc1c59a-2898-44b2-a003-d7a0631a6612

📥 Commits

Reviewing files that changed from the base of the PR and between 90152a1 and 7a89bec.

📒 Files selected for processing (1)
  • scripts/ghosttykit-checksums.txt
✅ Files skipped from review due to trivial changes (1)
  • scripts/ghosttykit-checksums.txt

📝 Walkthrough

Walkthrough

Initial surface focus is now explicitly seeded and propagated through Ghostty at creation (surfaces start unfocused by default). GhosttyKit xcframework caching/build logic was extracted to a new lock-backed script; setup and reload scripts now invoke it. The ghostty submodule reference was updated.

Changes

Cohort / File(s) Summary
Focus Initialization
Sources/GhosttyTerminalView.swift, docs/ghostty-fork.md, include/ghostty.h
Changed TerminalSurface.desiredFocusState default to false; seed surfaceConfig.focused from desiredFocusState before calling ghostty_surface_new; docs added clarifying initial focused flag and DECSET 1004 behavior (no CSI I/O during init).
GhosttyKit caching & setup
scripts/ensure-ghosttykit.sh (new), scripts/setup.sh, scripts/reload.sh, scripts/ghosttykit-checksums.txt
Added new script that computes a cache key (commit SHA + dirty-state hash), acquires a lock, reuses or builds GhosttyKit.xcframework, and updates a symlink; removed inline caching from setup.sh; reload.sh now invokes the new script. Added checksum entry for updated submodule SHA.
Submodule pointer
ghostty (gitlink update)
Updated ghostty submodule reference from ae3cc5d... to 9fa3ab0....

Sequence Diagram(s)

sequenceDiagram
    participant App as Host App (cmux)
    participant Swift as GhosttyTerminalView / TerminalSurface
    participant C as Ghostty C API
    participant Renderer as Renderer/TermIO / IO Thread

    App->>Swift: create TerminalSurface (desiredFocusState = false)
    Swift->>C: prepare surfaceConfig (focused = desiredFocusState)
    C->>C: ghostty_surface_new(surfaceConfig)
    C-->>Swift: createdSurface
    Swift->>C: ghostty_surface_set_focus(createdSurface, desiredFocusState)
    Swift->>Renderer: seed renderer/termio focus bookkeeping (focused = desiredFocusState)
    Note over Renderer,Swift: Converge/re-apply focus during init before IO thread runs
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I seeded focus low, not high,
so panes won't shout their escape-seq cry.
I cached the xcframework in a burrowed spot,
locked the door, rebuilt what we forgot.
thump-thump, happy hops 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main bug fix: preventing focus reporting escape sequences from leaking during pane creation, which matches the primary change in the changeset.
Description check ✅ Passed The PR description covers the summary of changes and provides a test plan, but lacks explicit details on manual testing performed and does not follow the template format completely.
Linked Issues check ✅ Passed The code changes directly address #2446 by initializing desiredFocusState=false, passing it through surfaceConfig.focused, and preventing CSI I/O leakage during pane creation as required.
Out of Scope Changes check ✅ Passed The GhosttyKit caching refactor in scripts/ is a supporting change for the focus fix; all modifications relate to either the core focus-reporting issue or its necessary build infrastructure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2446-focus-reporting-leak

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
scripts/reload.sh (1)

282-283: Consider using $SCRIPT_DIR for consistency with setup.sh.

The script invocation works correctly since ensure-ghosttykit.sh resolves its own directory at startup. However, setup.sh uses "$SCRIPT_DIR/ensure-ghosttykit.sh" while this uses "$PWD/scripts/ensure-ghosttykit.sh". For consistency and resilience when PWD differs from repo root:

Suggested change

Add SCRIPT_DIR resolution at the top of reload.sh (if not already present) and use:

-"$PWD/scripts/ensure-ghosttykit.sh"
+"$(dirname "${BASH_SOURCE[0]}")/ensure-ghosttykit.sh"

Or, since reload.sh already operates in $PWD, this is acceptable as-is given ensure-ghosttykit.sh's self-resolution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/reload.sh` around lines 282 - 283, The invocation in reload.sh uses
"$PWD/scripts/ensure-ghosttykit.sh" which is inconsistent with setup.sh; update
reload.sh to resolve and use SCRIPT_DIR like setup.sh by adding the same
SCRIPT_DIR resolution block at the top of reload.sh (the code that sets
SCRIPT_DIR based on the script's directory) and replace the "$PWD/..."
invocation with "$SCRIPT_DIR/ensure-ghosttykit.sh" so ensure-ghosttykit.sh is
invoked relative to the script location; reference SCRIPT_DIR, reload.sh,
setup.sh, and ensure-ghosttykit.sh when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/ghostty-fork.md`:
- Around line 118-129: The docs currently state "Status: working tree change
(not yet committed)" for the Ghostty fork; fix this by committing and pushing
the changes in the ghostty submodule (the changes that touch include/ghostty.h,
macos/Sources/Ghostty/Surface View/SurfaceView.swift, src/Surface.zig,
src/apprt/embedded.zig, src/termio/stream_handler.zig) to the
manaflow-ai/ghostty fork, note the resulting commit SHA, then update the
docs/ghostty-fork.md entry to replace the status line with the actual commit
hash (e.g. "Status: commit <SHA>") and optionally add the commit link or SHA(s)
in the summary so the parent repo can pin and reproduce the behavior.

---

Nitpick comments:
In `@scripts/reload.sh`:
- Around line 282-283: The invocation in reload.sh uses
"$PWD/scripts/ensure-ghosttykit.sh" which is inconsistent with setup.sh; update
reload.sh to resolve and use SCRIPT_DIR like setup.sh by adding the same
SCRIPT_DIR resolution block at the top of reload.sh (the code that sets
SCRIPT_DIR based on the script's directory) and replace the "$PWD/..."
invocation with "$SCRIPT_DIR/ensure-ghosttykit.sh" so ensure-ghosttykit.sh is
invoked relative to the script location; reference SCRIPT_DIR, reload.sh,
setup.sh, and ensure-ghosttykit.sh when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60c7689c-34e2-4154-8aaf-10576b811d0d

📥 Commits

Reviewing files that changed from the base of the PR and between e9b2090 and 2aa1325.

📒 Files selected for processing (6)
  • Sources/GhosttyTerminalView.swift
  • docs/ghostty-fork.md
  • ghostty.h
  • scripts/ensure-ghosttykit.sh
  • scripts/reload.sh
  • scripts/setup.sh

Comment thread docs/ghostty-fork.md Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 1, 2026

Greptile Summary

This PR fixes spurious CSI I (focus-in) sequences leaking into background shells during pane creation by defaulting desiredFocusState to false and seeding the Ghostty C surface with that state at creation time via the new surfaceConfig.focused field, so background panes never need a synthetic focus-loss transition. It also extracts GhosttyKit build/cache logic into a reusable scripts/ensure-ghosttykit.sh with improved dirty-tree hashing and a header sync guard.

Key points:

  • Swift change is clean: the desiredFocusState = false default plus surfaceConfig.focused seeding is logically correct; the post-creation ghostty_surface_set_focus call remains as a convergence guard for any race between surface init and arriving focus events.
  • ghostty.h is ahead of the submodule: the new focused, io_mode, io_write_cb, and io_write_userdata fields exist only in the root header — the Zig implementation and ghostty/include/ghostty.h are not yet committed. The new header sync guard in ensure-ghosttykit.sh will hard-fail on any clean checkout until the submodule pointer is updated with a matching commit from the fork.
  • reload.sh uses $PWD: the invocation of ensure-ghosttykit.sh uses $PWD/scripts/… rather than the $SCRIPT_DIR-relative pattern used by setup.sh, which is fragile when reload.sh is called with an absolute path from outside the project root.
  • Minor lock-reset bug: after clearing a stale lock, LOCK_START is not reset, causing the loop to tight-spin on contention rather than falling back to the 1-second polling interval.

Confidence Score: 4/5

  • Not safe to merge as-is — the root ghostty.h is ahead of the submodule, causing the new sync guard to fail on any clean checkout until the Zig fork commit lands and the submodule pointer is updated.
  • The Swift fix is sound and the ensure-ghosttykit.sh refactor is a net improvement, but the PR is explicitly incomplete: the Zig changes required to make surfaceConfig.focused actually work at the C level are not committed, and the sync check added in this same PR will block builds for anyone on a clean checkout. This is a P1 blocker that must be resolved before merging.
  • ghostty.h — new struct fields are ahead of the submodule's include/ghostty.h; the submodule pointer must be advanced to a fork commit that implements these fields before this PR can be safely merged.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Changes desiredFocusState initial value from true to false and seeds the C surface with it via surfaceConfig.focused before creation, eliminating the spurious focus-loss transition on pane creation; post-creation ghostty_surface_set_focus re-sync is preserved as a convergence guard.
ghostty.h Adds focused flag and io_mode/io_write_cb/io_write_userdata to ghostty_surface_config_s, and new ghostty_surface_io_mode_e enum. These additions are ahead of the submodule — ghostty/include/ghostty.h doesn't yet have them, which will cause the new sync guard in ensure-ghosttykit.sh to fail on any clean checkout.
scripts/ensure-ghosttykit.sh New script extracting GhosttyKit build/cache logic; adds dirty-tree hashing for accurate cache keying, a header sync guard, and legacy stamp migration. Has two minor issues: str.index() raises uncaught ValueError, and the stale-lock path doesn't reset LOCK_START.
scripts/setup.sh Replaces ~65 lines of inline GhosttyKit build/cache logic with a single call to ensure-ghosttykit.sh; straightforward refactor with no functional change to setup semantics.
scripts/reload.sh Adds a call to ensure-ghosttykit.sh before the Zig build step; uses $PWD/scripts/ensure-ghosttykit.sh which is fragile if the script is invoked from a non-project-root directory (setup.sh uses the more robust $SCRIPT_DIR/...).
docs/ghostty-fork.md Documents section 9 for initial focus seeding and DECSET 1004 startup behavior, and adds rebase-conflict notes for the new files; documentation-only change.

Sequence Diagram

sequenceDiagram
    participant WS as Workspace/AppKit Focus Path
    participant TS as TerminalSurface (Swift)
    participant GC as Ghostty C Surface

    Note over TS: desiredFocusState = false (new default)

    TS->>GC: createSurface(surfaceConfig.focused = false)
    Note over GC: Surface starts unfocused<br/>(no CSI I emitted)

    TS->>GC: ghostty_surface_set_focus(surface, false)
    Note over TS,GC: Re-sync guard — converges any<br/>focus change during init

    alt Pane is the focused pane
        WS->>TS: setFocus(true)
        TS->>TS: desiredFocusState = true
        TS->>GC: ghostty_surface_set_focus(surface, true)
        Note over GC: CSI I emitted (legitimate focus-in)
    else Pane is a background pane
        Note over TS,GC: desiredFocusState stays false<br/>No CSI I/O sequences emitted
    end
Loading

Reviews (1): Last reviewed commit: "Fix focus reporting leak: seed initial f..." | Re-trigger Greptile

Comment thread scripts/reload.sh
exit 1
fi

"$PWD/scripts/ensure-ghosttykit.sh"
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 Fragile $PWD-based path vs $SCRIPT_DIR

reload.sh uses $PWD/scripts/ensure-ghosttykit.sh, but setup.sh uses the more robust $SCRIPT_DIR/ensure-ghosttykit.sh. If reload.sh is invoked with an absolute path from a directory other than the project root (e.g., /abs/path/to/scripts/reload.sh --tag foo from /tmp), $PWD will not be the project root and the path to ensure-ghosttykit.sh won't resolve correctly.

Consider deriving SCRIPT_DIR at the top of reload.sh (the same way setup.sh and ensure-ghosttykit.sh do) and using it here:

Suggested change
"$PWD/scripts/ensure-ghosttykit.sh"
"$SCRIPT_DIR/scripts/ensure-ghosttykit.sh"

Or, since reload.sh itself lives in scripts/, simply compute SCRIPT_DIR at the top of reload.sh:

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

and then call:

"$SCRIPT_DIR/ensure-ghosttykit.sh"

Comment thread scripts/ensure-ghosttykit.sh Outdated
Comment on lines +26 to +37
extract_surface_config_block() {
local path="$1"
python3 - "$path" <<'PY'
from pathlib import Path
import sys

text = Path(sys.argv[1]).read_text()
start = text.index("typedef struct {\n ghostty_platform_e platform_tag;")
end = text.index("} ghostty_surface_config_s;") + len("} ghostty_surface_config_s;")
print(text[start:end], end="")
PY
}
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 str.index() raises ValueError on a missing substring

Path.index() in Python raises ValueError (not a clean exit) when the target string is not found. Because this function is called from a $(...) subshell under set -euo pipefail, the outer script will abort on failure — but the developer/user will see a raw Python traceback rather than the informative "out of sync" message. Consider using a try/except block for a cleaner error:

try:
    start = text.index("typedef struct {\n  ghostty_platform_e platform_tag;")
    end   = text.index("} ghostty_surface_config_s;") + len("} ghostty_surface_config_s;")
except ValueError as e:
    print(f"error: ghostty_surface_config_s block not found in {sys.argv[1]}: {e}", file=sys.stderr)
    sys.exit(1)

Comment on lines +92 to +101
LOCK_START=$SECONDS
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
if (( SECONDS - LOCK_START > LOCK_TIMEOUT )); then
echo "==> Lock stale (>${LOCK_TIMEOUT}s), removing and retrying..."
rmdir "$LOCK_DIR" 2>/dev/null || rm -rf "$LOCK_DIR"
continue
fi
echo "==> Waiting for GhosttyKit cache lock for $GHOSTTY_KEY..."
sleep 1
done
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 Stale-lock timeout doesn't reset LOCK_START, causing tight spin after first timeout

After the lock is forcibly removed at line 96 (rmdir/rm -rf) and continue jumps to the top of the loop, LOCK_START is never reset. If another process immediately re-creates the lock directory (active contention), the condition (( SECONDS - LOCK_START > LOCK_TIMEOUT )) will be true on the very next check, so the loop will spin without the sleep 1 delay — repeatedly force-removing the lock in a tight loop rather than once and then falling back to polite polling.

Reset LOCK_START after clearing the stale lock so the full 300-second window applies before the next forced eviction:

Suggested change
LOCK_START=$SECONDS
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
if (( SECONDS - LOCK_START > LOCK_TIMEOUT )); then
echo "==> Lock stale (>${LOCK_TIMEOUT}s), removing and retrying..."
rmdir "$LOCK_DIR" 2>/dev/null || rm -rf "$LOCK_DIR"
continue
fi
echo "==> Waiting for GhosttyKit cache lock for $GHOSTTY_KEY..."
sleep 1
done
if (( SECONDS - LOCK_START > LOCK_TIMEOUT )); then
echo "==> Lock stale (>${LOCK_TIMEOUT}s), removing and retrying..."
rmdir "$LOCK_DIR" 2>/dev/null || rm -rf "$LOCK_DIR"
LOCK_START=$SECONDS
continue
fi

Comment thread ghostty.h Outdated
Comment on lines 440 to 464
@@ -450,6 +458,9 @@ typedef struct {
const char* initial_input;
bool wait_after_command;
ghostty_surface_context_e context;
ghostty_surface_io_mode_e io_mode;
ghostty_io_write_cb io_write_cb;
void* io_write_userdata;
} ghostty_surface_config_s;
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.

P1 ghostty.h is ahead of the submodule — clean checkouts will fail the sync guard

ghostty.h now declares focused, ghostty_surface_io_mode_e, ghostty_io_write_cb, io_mode, io_write_cb, and io_write_userdata, but the PR description explicitly states the corresponding Zig changes in the submodule (Surface.zig, embedded.zig, stream_handler.zig, and ghostty/include/ghostty.h) are not yet committed.

The sync check introduced in ensure-ghosttykit.sh (lines 50–56) compares ghostty_surface_config_s in the root ghostty.h against ghostty/include/ghostty.h inside the submodule. For any developer or CI job that checks out this branch without the uncommitted submodule working-tree changes present, that check will fail immediately with:

error: ghostty_surface_config_s is out of sync between ghostty.h and ghostty/include/ghostty.h.

This also means the core fix (surfaceConfig.focused = desiredFocusState) won't actually seed the initial focus state in the C surface until the Zig implementation that reads that field is shipped and the submodule pointer is updated. Without the committed Zig side, the desiredFocusState = false change still prevents the post-creation ghostty_surface_set_focus(surface, false) call from being a no-op (it will still send a focus-loss transition to an already-running surface), partially defeating the goal.

Before merging, please:

  1. Commit the Zig changes to the manaflow-ai/ghostty fork.
  2. Update the submodule pointer so ghostty/include/ghostty.h matches the root ghostty.h.
  3. Verify CI passes end-to-end on a clean clone.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/ensure-ghosttykit.sh">

<violation number="1" location="scripts/ensure-ghosttykit.sh:94">
P1: Do not delete the cache lock solely because this process waited >300s; it can remove an active lock and allow concurrent GhosttyKit builds/copies.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

LOCK_TIMEOUT=300
LOCK_START=$SECONDS
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
if (( SECONDS - LOCK_START > LOCK_TIMEOUT )); then
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026

Choose a reason for hiding this comment

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

P1: Do not delete the cache lock solely because this process waited >300s; it can remove an active lock and allow concurrent GhosttyKit builds/copies.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/ensure-ghosttykit.sh, line 94:

<comment>Do not delete the cache lock solely because this process waited >300s; it can remove an active lock and allow concurrent GhosttyKit builds/copies.</comment>

<file context>
@@ -0,0 +1,141 @@
+LOCK_TIMEOUT=300
+LOCK_START=$SECONDS
+while ! mkdir "$LOCK_DIR" 2>/dev/null; do
+  if (( SECONDS - LOCK_START > LOCK_TIMEOUT )); then
+    echo "==> Lock stale (>${LOCK_TIMEOUT}s), removing and retrying..."
+    rmdir "$LOCK_DIR" 2>/dev/null || rm -rf "$LOCK_DIR"
</file context>
Fix with Cubic

…rting-leak

# Conflicts:
#	ghostty
#	ghostty.h
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/ensure-ghosttykit.sh`:
- Around line 96-100: The current mkdir-based lock loop (using LOCK_DIR,
LOCK_START, LOCK_TIMEOUT and SECONDS) force-removes the lock directory after
timeout which can delete a live lock; instead, implement a safe-stale-check:
when the timeout is reached, inspect a pid/owner file inside "$LOCK_DIR" (e.g.,
"$LOCK_DIR/pid"), read the PID, and use kill -0 PID to determine if the locking
process is still alive; only remove rmdir/"rm -rf $LOCK_DIR" if the owner
process is gone (or pid file missing/outdated), otherwise continue waiting (or
renew timeout); apply the same change to the later removal logic around the
other timeout branch as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6dd8d00f-e1d7-4da7-95f2-6e53283f159e

📥 Commits

Reviewing files that changed from the base of the PR and between 58e5b34 and 90152a1.

📒 Files selected for processing (4)
  • Sources/GhosttyTerminalView.swift
  • docs/ghostty-fork.md
  • ghostty
  • scripts/ensure-ghosttykit.sh
✅ Files skipped from review due to trivial changes (2)
  • ghostty
  • Sources/GhosttyTerminalView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/ghostty-fork.md

Comment on lines +96 to +100
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
if (( SECONDS - LOCK_START > LOCK_TIMEOUT )); then
echo "==> Lock stale (>${LOCK_TIMEOUT}s), removing and retrying..."
rmdir "$LOCK_DIR" 2>/dev/null || rm -rf "$LOCK_DIR"
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid deleting potentially live locks after timeout.

At Line 98-100, a waiting process force-removes the lock directory after 300s. A legitimate long build can exceed that and get its live lock deleted, allowing concurrent cache writes for the same key.

Suggested fix
 LOCK_TIMEOUT=300
 LOCK_START=$SECONDS
+LOCK_OWNER_PID_FILE="$LOCK_DIR/pid"
 while ! mkdir "$LOCK_DIR" 2>/dev/null; do
+  if [[ -f "$LOCK_OWNER_PID_FILE" ]]; then
+    read -r lock_pid < "$LOCK_OWNER_PID_FILE" || lock_pid=""
+    if [[ -n "$lock_pid" ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
+      echo "==> Removing orphaned GhosttyKit cache lock (pid $lock_pid)..."
+      rm -rf "$LOCK_DIR"
+      continue
+    fi
+  fi
   if (( SECONDS - LOCK_START > LOCK_TIMEOUT )); then
-    echo "==> Lock stale (>${LOCK_TIMEOUT}s), removing and retrying..."
-    rmdir "$LOCK_DIR" 2>/dev/null || rm -rf "$LOCK_DIR"
-    continue
+    echo "error: timed out waiting for GhosttyKit cache lock for $GHOSTTY_KEY" >&2
+    exit 1
   fi
   echo "==> Waiting for GhosttyKit cache lock for $GHOSTTY_KEY..."
   sleep 1
 done
-trap 'rmdir "$LOCK_DIR" >/dev/null 2>&1 || true' EXIT
+echo "$$" > "$LOCK_OWNER_PID_FILE"
+trap 'rm -rf "$LOCK_DIR" >/dev/null 2>&1 || true' EXIT

Also applies to: 105-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ensure-ghosttykit.sh` around lines 96 - 100, The current mkdir-based
lock loop (using LOCK_DIR, LOCK_START, LOCK_TIMEOUT and SECONDS) force-removes
the lock directory after timeout which can delete a live lock; instead,
implement a safe-stale-check: when the timeout is reached, inspect a pid/owner
file inside "$LOCK_DIR" (e.g., "$LOCK_DIR/pid"), read the PID, and use kill -0
PID to determine if the locking process is still alive; only remove rmdir/"rm
-rf $LOCK_DIR" if the owner process is gone (or pid file missing/outdated),
otherwise continue waiting (or renew timeout); apply the same change to the
later removal logic around the other timeout branch as well.

@austinywang austinywang merged commit 2e9c318 into main Apr 6, 2026
16 checks passed
@austinywang austinywang mentioned this pull request Apr 6, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Focus reporting escape sequences leak to panes that never requested them

1 participant