Skip to content

feat: add Kitty follow integration#341

Open
reVrost wants to merge 1 commit into
modem-dev:mainfrom
reVrost:codex/kitty-follow
Open

feat: add Kitty follow integration#341
reVrost wants to merge 1 commit into
modem-dev:mainfrom
reVrost:codex/kitty-follow

Conversation

@reVrost
Copy link
Copy Markdown

@reVrost reVrost commented May 20, 2026

No description provided.

@reVrost
Copy link
Copy Markdown
Author

reVrost commented May 20, 2026

@codex review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR adds a Kitty terminal follow integration: a Python watcher (kitty/hunk-follow.py) fires hunk kitty sync on every Kitty focus change, and a new TypeScript module (src/kitty/sync.ts) handles the logic of identifying the active pane's repo and reloading the marked Hunk session accordingly. Supporting changes thread a kittyFollow opt-in flag through the CLI, session registration, wire protocol, and artifact packaging.

  • New hunk kitty command surfacewatcher-path prints the bundled script path; sync --window-id queries kitten @ ls, resolves the active pane's repo, and calls the session daemon's reload endpoint. Guarded correctly to only allow --kitty-follow on working-tree diffs.
  • Session lifecyclekittyFollow is persisted on createSessionRegistration and preserved via OR across updateSessionRegistration reloads, so the flag survives diff refreshes.
  • Packagingkitty/ is added to package.json files, prebuilt artifact scripts, and the Homebrew formula; validation tests cover all new artifact paths.

Confidence Score: 3/5

The core new feature works correctly for the happy path, but isHunkCommand will silently suppress syncs in a real-world editing scenario that's easy to hit on this repo itself.

The isHunkCommand fallback scans every argument of every foreground process, so focusing any pane that has hunk as a filename argument (e.g., vim /path/to/hunk, cat hunk) will be misidentified as a Hunk window and cause the follow-sync to silently no-op. Developers working inside the Hunk repository — the exact audience for this feature — are likely to open the hunk binary or source in an editor, making this a reproducible failure on typical workflows.

src/kitty/sync.ts — specifically the isHunkCommand function and the listSessions catch block.

Important Files Changed

Filename Overview
src/kitty/sync.ts Core sync logic for Kitty follow integration; isHunkCommand scans all cmdline args (P1 false-positive) and daemon errors are misreported as "no-marked-hunk-session" (P2).
kitty/hunk-follow.py New Kitty watcher script; debounce and subprocess fire-and-forget look correct, but _last_sync_at_by_window grows without cleanup on window close.
src/core/cli.ts Adds hunk kitty subcommand and --kitty-follow flag with correct validation (rejects non-working-tree diffs).
src/core/paths.ts Adds resolveBundledKittyWatcherPath mirroring the existing skill path resolver; search-root logic is consistent and correct.
src/hunk-session/sessionRegistration.ts Correctly persists kittyFollow on create and preserves it via OR across reloads so the flag survives session refreshes.
src/session/protocol.ts Daemon version bumped from 3 to 4 to reflect the new kittyFollow field in session info wire format.
packages/session-broker-core/src/sessionTerminalMetadata.ts Adds Kitty terminal location extraction from KITTY_WINDOW_ID; WINDOWID (X11) stored as terminalId is metadata-only and not used in sync path.
bin/hunk.cjs Short-circuits hunk kitty watcher-path before forwarding to the main binary using a simple static path; consistent with existing skill-path shortcut.
scripts/build-prebuilt-artifact.ts Validates and copies the kitty/ directory into prebuilt artifacts; error messages are actionable.

Sequence Diagram

sequenceDiagram
    participant KW as hunk-follow.py
    participant KC as hunk kitty sync
    participant KK as kitten @ ls
    participant HD as Hunk daemon
    participant HS as Hunk session

    Note over KW: on_focus_change fires
    KW->>KW: debounce check
    KW->>KC: spawn hunk kitty sync --window-id N
    KC->>KK: "spawnSync kitten @ ls"
    KK-->>KC: KittyOsWindow[] JSON
    KC->>KC: resolveActiveKittyPane
    alt stale event
        KC-->>KW: noop kitty-window-not-active
    end
    KC->>HD: listSessions()
    alt daemon unavailable
        KC-->>KW: noop no-marked-hunk-session
    end
    KC->>KC: isActiveHunkWindow check
    KC->>KC: detectVcs(pane.cwd)
    alt not a repo
        KC-->>KW: noop not-a-repo
    end
    KC->>KC: selectKittyFollowTarget
    KC->>HD: reloadSession with new sourcePath
    HD->>HS: reload diff from new repo root
    HD-->>KC: ReloadResult
    KC-->>KW: status reloaded
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/kitty/sync.ts:233-238
`isHunkCommand` scans every argument in `cmdline`, not just the executable. This means any foreground process whose argument list contains an element with basename `hunk` — e.g., `["vim", "/projects/hunk"]` or `["cat", "/home/user/hunkdiff"]` — will be treated as an active Hunk window and suppress the sync silently. Users of this project who edit the `hunk` binary/source in a side pane will hit this constantly. The `matchesRegisteredHunkWindow` check already handles the registered case (including `bun hunk diff`), so the fallback only needs to check `cmdline[0]`.

```suggestion
function isHunkCommand(cmdline: string[]) {
  if (cmdline.length === 0) return false;
  const name = basename(cmdline[0]).toLowerCase();
  return name === "hunk" || name === "hunkdiff" || name.startsWith("hunkdiff-");
}
```

### Issue 2 of 3
src/kitty/sync.ts:328-332
When the daemon HTTP client throws (e.g., daemon not running, connection refused), the catch block returns `"no-marked-hunk-session"` — a reason that implies the session list was fetched and came back empty. In practice this makes it impossible to distinguish "no follow sessions registered" from "can't reach the daemon at all", which complicates debugging silently failed syncs.

```suggestion
  try {
    sessions = await client.listSessions();
  } catch {
    return { status: "noop", reason: "daemon-unavailable", windowId: input.windowId };
  }
```

### Issue 3 of 3
kitty/hunk-follow.py:12
**Unbounded growth of `_last_sync_at_by_window`**

`_last_sync_at_by_window` is a module-level dict that accumulates one entry per unique `window.id` but never evicts stale entries. Kitty recycles integer window IDs within a session, so the actual risk of unbounded growth is low — but there is no `on_close` hook to clean up closed windows. Over a long Kitty session with many windows, this will hold a non-trivial number of stale float entries. Registering an `on_close` callback (`def on_close(boss, window): _last_sync_at_by_window.pop(int(window.id), None)`) would keep the dict bounded.

Reviews (1): Last reviewed commit: "feat: add Kitty follow integration" | Re-trigger Greptile

Comment thread src/kitty/sync.ts
Comment on lines +233 to +238
function isHunkCommand(cmdline: string[]) {
return cmdline.some((part) => {
const name = basename(part).toLowerCase();
return name === "hunk" || name === "hunkdiff" || name.startsWith("hunkdiff-");
});
}
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 isHunkCommand scans every argument in cmdline, not just the executable. This means any foreground process whose argument list contains an element with basename hunk — e.g., ["vim", "/projects/hunk"] or ["cat", "/home/user/hunkdiff"] — will be treated as an active Hunk window and suppress the sync silently. Users of this project who edit the hunk binary/source in a side pane will hit this constantly. The matchesRegisteredHunkWindow check already handles the registered case (including bun hunk diff), so the fallback only needs to check cmdline[0].

Suggested change
function isHunkCommand(cmdline: string[]) {
return cmdline.some((part) => {
const name = basename(part).toLowerCase();
return name === "hunk" || name === "hunkdiff" || name.startsWith("hunkdiff-");
});
}
function isHunkCommand(cmdline: string[]) {
if (cmdline.length === 0) return false;
const name = basename(cmdline[0]).toLowerCase();
return name === "hunk" || name === "hunkdiff" || name.startsWith("hunkdiff-");
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/kitty/sync.ts
Line: 233-238

Comment:
`isHunkCommand` scans every argument in `cmdline`, not just the executable. This means any foreground process whose argument list contains an element with basename `hunk` — e.g., `["vim", "/projects/hunk"]` or `["cat", "/home/user/hunkdiff"]` — will be treated as an active Hunk window and suppress the sync silently. Users of this project who edit the `hunk` binary/source in a side pane will hit this constantly. The `matchesRegisteredHunkWindow` check already handles the registered case (including `bun hunk diff`), so the fallback only needs to check `cmdline[0]`.

```suggestion
function isHunkCommand(cmdline: string[]) {
  if (cmdline.length === 0) return false;
  const name = basename(cmdline[0]).toLowerCase();
  return name === "hunk" || name === "hunkdiff" || name.startsWith("hunkdiff-");
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/kitty/sync.ts
Comment on lines +328 to +332
try {
sessions = await client.listSessions();
} catch {
return { status: "noop", reason: "no-marked-hunk-session", windowId: input.windowId };
}
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 When the daemon HTTP client throws (e.g., daemon not running, connection refused), the catch block returns "no-marked-hunk-session" — a reason that implies the session list was fetched and came back empty. In practice this makes it impossible to distinguish "no follow sessions registered" from "can't reach the daemon at all", which complicates debugging silently failed syncs.

Suggested change
try {
sessions = await client.listSessions();
} catch {
return { status: "noop", reason: "no-marked-hunk-session", windowId: input.windowId };
}
try {
sessions = await client.listSessions();
} catch {
return { status: "noop", reason: "daemon-unavailable", windowId: input.windowId };
}

Would you prefer to keep the catch reason as "no-marked-hunk-session" for simplicity, or add "daemon-unavailable" to KittySyncNoopReason so callers can distinguish the two cases?

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/kitty/sync.ts
Line: 328-332

Comment:
When the daemon HTTP client throws (e.g., daemon not running, connection refused), the catch block returns `"no-marked-hunk-session"` — a reason that implies the session list was fetched and came back empty. In practice this makes it impossible to distinguish "no follow sessions registered" from "can't reach the daemon at all", which complicates debugging silently failed syncs.

```suggestion
  try {
    sessions = await client.listSessions();
  } catch {
    return { status: "noop", reason: "daemon-unavailable", windowId: input.windowId };
  }
```

 Would you prefer to keep the catch reason as `"no-marked-hunk-session"` for simplicity, or add `"daemon-unavailable"` to `KittySyncNoopReason` so callers can distinguish the two cases?

How can I resolve this? If you propose a fix, please make it concise.

Comment thread kitty/hunk-follow.py
from typing import Any


_last_sync_at_by_window: dict[int, float] = {}
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 Unbounded growth of _last_sync_at_by_window

_last_sync_at_by_window is a module-level dict that accumulates one entry per unique window.id but never evicts stale entries. Kitty recycles integer window IDs within a session, so the actual risk of unbounded growth is low — but there is no on_close hook to clean up closed windows. Over a long Kitty session with many windows, this will hold a non-trivial number of stale float entries. Registering an on_close callback (def on_close(boss, window): _last_sync_at_by_window.pop(int(window.id), None)) would keep the dict bounded.

Prompt To Fix With AI
This is a comment left during a code review.
Path: kitty/hunk-follow.py
Line: 12

Comment:
**Unbounded growth of `_last_sync_at_by_window`**

`_last_sync_at_by_window` is a module-level dict that accumulates one entry per unique `window.id` but never evicts stale entries. Kitty recycles integer window IDs within a session, so the actual risk of unbounded growth is low — but there is no `on_close` hook to clean up closed windows. Over a long Kitty session with many windows, this will hold a non-trivial number of stale float entries. Registering an `on_close` callback (`def on_close(boss, window): _last_sync_at_by_window.pop(int(window.id), None)`) would keep the dict bounded.

How can I resolve this? If you propose a fix, please make it concise.

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.

1 participant