Skip to content

feat(iterm2): add tab-level notification and click-to-restore#9

Merged
trentmcnitt merged 5 commits into
trentmcnitt:mainfrom
dgokcin:iterm-tab-notifications
Apr 18, 2026
Merged

feat(iterm2): add tab-level notification and click-to-restore#9
trentmcnitt merged 5 commits into
trentmcnitt:mainfrom
dgokcin:iterm-tab-notifications

Conversation

@dgokcin
Copy link
Copy Markdown
Contributor

@dgokcin dgokcin commented Apr 15, 2026

Closes #8

Summary

  • Captures iTerm2 session ID at init via AppleScript (id of current session of current window)
  • Detects tab switches: same window ID but different iTerm2 session ID → send notification
  • Click-to-focus chains Hammerspoon window focus + AppleScript session restore to bring back exact tab
  • Session file gains optional 5th line for iTerm2 session ID; backward-compatible with existing 3/4-line files

Test plan

  • make test passes (5 new tests added)
  • make check passes (lint, typecheck, deadcode)
  • On iTerm2: switch tabs during Claude Code task → notification fires
  • Click notification → correct tab restored
  • Non-iTerm2 users: no behavior change

dgokcin added 3 commits April 15, 2026 14:24
- capture iterm2 session id at init for tab identity tracking
- detect tab switches in same window via session id comparison
- extend click-to-focus to restore original iterm2 tab via applescript
- preserve iterm2 session id through deduplication file rewrites
- add tests for init capture, tab-switch detection, focus command
- mention iterm2 tab restore in readme features and how-it-works
- document iterm2 session id in session file format
- update context flows for init/notify with iterm2 tab tracking
- sync test context count to 70 tests (61 core + 9 integration)
- collapse conditional save_window_id call into single unconditional call
- fix applescript string quoting to use consistent single-quoted style
- collapse multi-line escaped_session_id assignment to one line
- update test assertion to match new save_window_id signature
@dgokcin dgokcin force-pushed the iterm-tab-notifications branch from 80990a1 to 120292b Compare April 15, 2026 15:04
@trentmcnitt
Copy link
Copy Markdown
Owner

Thanks for putting this together

The iTerm2 gating is clean, backward compatibility is preserved, and the AppleScript escaping is the feelsl ike the right defensive move.

A few quick things before merging:

1. Add a dedup test for the 5th line

check_deduplication rewrites the session file via manual string concatenation (cc_notifier.py:240-244). If someone later reorders or adds a field, it's easy to silently drop the iTerm2 session ID — which would break click-to-focus on the second-and-later notifications in a session but pass the first. Worth locking down.

Suggested test (sibling to test_file_locking_prevents_race_conditions):

def test_dedup_preserves_iterm2_session_id(self, tmp_path):
    """check_deduplication must preserve the iTerm2 session ID on rewrite."""
    session_dir = tmp_path / "cc_notifier"
    session_dir.mkdir()
    session_file = session_dir / "dedup-iterm"
    # Timestamp old enough to bypass the dedup window
    session_file.write_text(
        f"win123\n/Applications/iTerm.app\n0\n$20\nw0t1p0"
    )

    assert cc_notifier.check_deduplication(session_file) is False

    lines = session_file.read_text().strip().split("\n")
    assert len(lines) == 5
    assert lines[4] == "w0t1p0"

2. Tighten the tab-switch notify test

test_notify_sent_when_same_iterm2_window_but_different_tab currently only asserts that terminal-notifier was invoked. That passes even if the iTerm2 restore script is silently dropped from the -execute chain. One extra assertion closes the gap:

execute_args = terminal_notifier_calls[0]
execute_idx = execute_args.index("-execute")
assert "osascript" in execute_args[execute_idx + 1]
assert "w0t1p0" in execute_args[execute_idx + 1]

3. Update two docstrings

  • send_local_notification_if_needed — mention the iTerm2 tab-switch path alongside the window/tmux cases.
  • create_focus_command — document the iterm2_session_id parameter and that it chains an AppleScript tab restore after the Hammerspoon focus.

Non-blocking notes

  • check_deduplication changed lines[1] to app_path = lines[1] if len(lines) > 1 else "" — functionally equivalent given how the file is produced, but it silently masks truncation of a file that should always have ≥2 lines. Reverting to lines[1] keeps the diff tighter.
  • is_iterm2_app matches by path suffix, so a relocated/renamed iTerm2 (e.g. iTerm-Beta.app) would silently skip tab tracking. Fine for the 99% case; flagging only so it's a known limitation.

Happy to merge once the dedup test and docstrings are in — the tighter assertion is nice-to-have.

And I'm guessing you tested this on your machine and are happy with the iterm2 tab restoration? I'll test it myself after the changes and your confirmation that it's working well for you.

dgokcin added 2 commits April 16, 2026 01:51
…args

- document three switched-away detection scenarios in send_local_notification_if_needed
- add iterm2_session_id parameter description to create_focus_command
- add test_dedup_preserves_iterm2_session_id to verify timestamp rewrites keep session id
- enhance tab notification test to verify osascript restore in execute chain
- update tests.context.md with new test count (71 total, 62 core)
@dgokcin
Copy link
Copy Markdown
Contributor Author

dgokcin commented Apr 16, 2026

Thanks for putting this together

The iTerm2 gating is clean, backward compatibility is preserved, and the AppleScript escaping is the feelsl ike the right defensive move.

A few quick things before merging:

1. Add a dedup test for the 5th line

check_deduplication rewrites the session file via manual string concatenation (cc_notifier.py:240-244). If someone later reorders or adds a field, it's easy to silently drop the iTerm2 session ID — which would break click-to-focus on the second-and-later notifications in a session but pass the first. Worth locking down.

Suggested test (sibling to test_file_locking_prevents_race_conditions):

def test_dedup_preserves_iterm2_session_id(self, tmp_path):
    """check_deduplication must preserve the iTerm2 session ID on rewrite."""
    session_dir = tmp_path / "cc_notifier"
    session_dir.mkdir()
    session_file = session_dir / "dedup-iterm"
    # Timestamp old enough to bypass the dedup window
    session_file.write_text(
        f"win123\n/Applications/iTerm.app\n0\n$20\nw0t1p0"
    )

    assert cc_notifier.check_deduplication(session_file) is False

    lines = session_file.read_text().strip().split("\n")
    assert len(lines) == 5
    assert lines[4] == "w0t1p0"

2. Tighten the tab-switch notify test

test_notify_sent_when_same_iterm2_window_but_different_tab currently only asserts that terminal-notifier was invoked. That passes even if the iTerm2 restore script is silently dropped from the -execute chain. One extra assertion closes the gap:

execute_args = terminal_notifier_calls[0]
execute_idx = execute_args.index("-execute")
assert "osascript" in execute_args[execute_idx + 1]
assert "w0t1p0" in execute_args[execute_idx + 1]

3. Update two docstrings

  • send_local_notification_if_needed — mention the iTerm2 tab-switch path alongside the window/tmux cases.
  • create_focus_command — document the iterm2_session_id parameter and that it chains an AppleScript tab restore after the Hammerspoon focus.

Non-blocking notes

  • check_deduplication changed lines[1] to app_path = lines[1] if len(lines) > 1 else "" — functionally equivalent given how the file is produced, but it silently masks truncation of a file that should always have ≥2 lines. Reverting to lines[1] keeps the diff tighter.
  • is_iterm2_app matches by path suffix, so a relocated/renamed iTerm2 (e.g. iTerm-Beta.app) would silently skip tab tracking. Fine for the 99% case; flagging only so it's a known limitation.

Happy to merge once the dedup test and docstrings are in — the tighter assertion is nice-to-have.

And I'm guessing you tested this on your machine and are happy with the iterm2 tab restoration? I'll test it myself after the changes and your confirmation that it's working well for you.

I think I adressed your points. I will test it from my local ./install.sh for a couple of days and let you know how it goes. today if worked nicely for a while but at some point just stopped. so I need to test it a little bit more :)

@trentmcnitt
Copy link
Copy Markdown
Owner

Looks clean to me @dgokcin once you report back that it's working well for you, I'll give it a try, and then we can merge it.

@dgokcin
Copy link
Copy Markdown
Contributor Author

dgokcin commented Apr 17, 2026

@trentmcnitt I think it's working pretty good. you can give it a test yourself and we can merge if you also do not see any issues 🙌

@trentmcnitt trentmcnitt merged commit b76f8d9 into trentmcnitt:main Apr 18, 2026
@trentmcnitt
Copy link
Copy Markdown
Owner

Great work, @dgokcin – Played with it today, and it's working nicely 💪

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.

feat: iTerm2 tab-level notification and click-to-restore

2 participants