Browse Source

feat(kitty): add kitty terminal multiplexer backend

Adds a kitty multiplexer that opens subagent panes via kitty's remote
control CLI (kitten @ launch). OpenCode must run inside a kitty window
(KITTY_PID/KITTY_WINDOW_ID) and kitty.conf must set allow_remote_control
and listen_on.

Key behaviors:
- Remote control goes through the KITTY_LISTEN_ON UNIX socket (inferred
  from the env kitty exports to its children) instead of the controlling
  tty, which does not work for OpenCode's detached subagent processes.
- Panes launch in the user's actual shell (detected via SHELL: fish, nu,
  zsh, bash, cmd, powershell, or default) with the absolute opencode
  binary, so the attach command resolves correctly under any shell.
- Plugin layouts map to kitty built-ins (main-vertical->tall,
  main-horizontal->fat, tiled->grid, even-*->horizontal/vertical).
- Graceful close sends Ctrl+C then closes the window; spawn/close/apply
  are guarded when not inside kitty or when KITTY_LISTEN_ON is absent
  (clear actionable error instead of silent failure).
- closePane threads KITTY_LISTEN_ON through to match spawnPane/ensureLayout.

Includes schema enum, factory wiring, docs, README, and tests.
adikpb 2 weeks ago
parent
commit
fa36caa5b2

+ 2 - 2
CONTEXT.md

@@ -31,8 +31,8 @@ A glossary of the terms used in this project's domain. Definitions describe what
 
 
 ## Multiplexer & Sessions
 ## Multiplexer & Sessions
 
 
-- **Multiplexer** — A terminal backend (tmux, zellij, or herdr) that hosts child agent panes. Set via \`multiplexer.type\`, which also accepts \`auto\` (auto-detect) and \`none\` (disabled).
-- **Multiplexer type** — The selected backend: `auto`, `tmux`, `zellij`, `herdr`, or `none`.
+- **Multiplexer** — A terminal backend (tmux, zellij, herdr, or kitty) that hosts child agent panes. Set via \`multiplexer.type\`, which also accepts \`auto\` (auto-detect) and \`none\` (disabled).
+- **Multiplexer type** — The selected backend: `auto`, `tmux`, `zellij`, `herdr`, `kitty`, or `none`.
 - **Pane** — A terminal region spawned by the multiplexer to run a child agent session.
 - **Pane** — A terminal region spawned by the multiplexer to run a child agent session.
 - **Child session** — A background agent session hosted in a multiplexer pane and tracked by the session manager.
 - **Child session** — A background agent session hosted in a multiplexer pane and tracked by the session manager.
 - **Session manager** — Tracks child sessions, spawns and closes multiplexer panes, and reacts to session lifecycle events. Note: `TmuxSessionManager` is a deprecated alias — use `MultiplexerSessionManager`.
 - **Session manager** — Tracks child sessions, spawns and closes multiplexer panes, and reacts to session lifecycle events. Note: `TmuxSessionManager` is a deprecated alias — use `MultiplexerSessionManager`.

+ 3 - 3
README.ja-JP.md

@@ -35,7 +35,7 @@ oh-my-opencode-slim は OpenCode 向けのエージェントオーケストレ
 - **[同梱スキル](#skills)** - `deepwork`、`codemap`、`verification-planning`、`reflect` などのプロンプトベースのワークフローを、エージェントごとに割り当てます。
 - **[同梱スキル](#skills)** - `deepwork`、`codemap`、`verification-planning`、`reflect` などのプロンプトベースのワークフローを、エージェントごとに割り当てます。
 - **[Council](docs/council.md)** - `@council` で同じ質問を複数モデルに並列で投げ、1 つの回答に統合します。
 - **[Council](docs/council.md)** - `@council` で同じ質問を複数モデルに並列で投げ、1 つの回答に統合します。
 - **[Companion](docs/companion.md)** - 並列のバックグラウンド専門家を含む、稼働中のエージェントを表示する任意のフローティングデスクトップウィンドウです。
 - **[Companion](docs/companion.md)** - 並列のバックグラウンド専門家を含む、稼働中のエージェントを表示する任意のフローティングデスクトップウィンドウです。
-- **[マルチプレクサー統合](docs/multiplexer-integration.md)** - Tmux、Zellij、Herdr、cmux のペインでエージェントの作業をライブ表示します。
+- **[マルチプレクサー統合](docs/multiplexer-integration.md)** - Tmux、Zellij、Herdr、cmux、kitty のペインでエージェントの作業をライブ表示します。
 - **[プリセット切り替え](docs/preset-switching.md)** - `/preset` でチーム全体のモデルを実行時に切り替えます。
 - **[プリセット切り替え](docs/preset-switching.md)** - `/preset` でチーム全体のモデルを実行時に切り替えます。
 - **[コードインテリジェンスツール](docs/tools.md)** - 25 言語対応の LSP、AST 対応検索、Web 検索・ドキュメント・GitHub コード検索用の組み込み MCP を提供します。
 - **[コードインテリジェンスツール](docs/tools.md)** - 25 言語対応の LSP、AST 対応検索、Web 検索・ドキュメント・GitHub コード検索用の組み込み MCP を提供します。
 - **[完全にカスタマイズ可能](docs/configuration.md)** - カスタムエージェント、プロンプト上書き、エージェントごとのスキル/MCP 権限、[プロジェクトローカルのカスタマイズ](docs/project-local-customization.md)に対応します。
 - **[完全にカスタマイズ可能](docs/configuration.md)** - カスタムエージェント、プロンプト上書き、エージェントごとのスキル/MCP 権限、[プロジェクトローカルのカスタマイズ](docs/project-local-customization.md)に対応します。
@@ -143,7 +143,7 @@ bun run build
 > バックグラウンドオーケストレーションの仕組みを理解しておくことを**推奨**します。**[Orchestrator のプロンプト](https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/src/agents/orchestrator.ts#L28)** には、スケジューラーのルール、専門エージェントへのルーティングロジック、作業をバックグラウンドエージェントへ割り当てるしきい値が記述されています。`@agentName <task>` のようにサブエージェントを呼び出すことで、いつでも手動で委譲できます。
 > バックグラウンドオーケストレーションの仕組みを理解しておくことを**推奨**します。**[Orchestrator のプロンプト](https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/src/agents/orchestrator.ts#L28)** には、スケジューラーのルール、専門エージェントへのルーティングロジック、作業をバックグラウンドエージェントへ割り当てるしきい値が記述されています。`@agentName <task>` のようにサブエージェントを呼び出すことで、いつでも手動で委譲できます。
 
 
 > [!TIP]
 > [!TIP]
-> バックグラウンドエージェントが現在のデフォルトワークフローになっているため、**[Multiplexer Integration](docs/multiplexer-integration.md)** を有効化して設定することを**強く推奨**します。各エージェントが専用の Tmux、Zellij、Herdr または cmux ペインで自動的に開かれるため、Orchestrator がセッションを調整し続けている間も、専門エージェントの作業をリアルタイムで追えます。
+> バックグラウンドエージェントが現在のデフォルトワークフローになっているため、**[Multiplexer Integration](docs/multiplexer-integration.md)** を有効化して設定することを**強く推奨**します。各エージェントが専用の Tmux、Zellij、Herdr、cmux、または kitty ペインで自動的に開かれるため、Orchestrator がセッションを調整し続けている間も、専門エージェントの作業をリアルタイムで追えます。
 
 
 デフォルトで生成される設定には `openai` と `opencode-go` の両方のプリセットが含まれます。
 デフォルトで生成される設定には `openai` と `opencode-go` の両方のプリセットが含まれます。
 
 
@@ -607,7 +607,7 @@ bunx oh-my-opencode-slim@latest install --companion=yes
 | **[Council](docs/council.md)** | 複数のモデルを並列実行し、`@council` で 1 つの回答に統合します |
 | **[Council](docs/council.md)** | 複数のモデルを並列実行し、`@council` で 1 つの回答に統合します |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | カスタムプロンプト、モデル、MCP アクセス、Orchestrator の委譲ルールを備えた独自の専門エージェントを定義します |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | カスタムプロンプト、モデル、MCP アクセス、Orchestrator の委譲ルールを備えた独自の専門エージェントを定義します |
 | **[ACP Agents](docs/acp-agents.md)** | Claude Code ACP や Gemini ACP などの外部 ACP 互換エージェントを委譲可能なサブエージェントとして接続します |
 | **[ACP Agents](docs/acp-agents.md)** | Claude Code ACP や Gemini ACP などの外部 ACP 互換エージェントを委譲可能なサブエージェントとして接続します |
-| **[Multiplexer Integration](docs/multiplexer-integration.md)** | エージェントの動作を Tmux、Zellij、Herdr や cmux のペインでライブ表示します |
+| **[Multiplexer Integration](docs/multiplexer-integration.md)** | エージェントの動作を Tmux、Zellij、Herdr、cmux、や kitty のペインでライブ表示します |
 | **[Codemap](docs/codemap.md)** | 階層的なコードマップを生成し、大規模コードベースを迅速に理解します |
 | **[Codemap](docs/codemap.md)** | 階層的なコードマップを生成し、大規模コードベースを迅速に理解します |
 | **[Clonedeps](docs/clonedeps.md)** | 選択した依存関係のソースを ignore 済みのローカルワークスペースにクローンし、調査できるようにします |
 | **[Clonedeps](docs/clonedeps.md)** | 選択した依存関係のソースを ignore 済みのローカルワークスペースにクローンし、調査できるようにします |
 | **[Worktrees](docs/worktrees.md)** | `.slim/worktrees/` lane を使い、隔離された並列または高リスクなコーディング作業を行います |
 | **[Worktrees](docs/worktrees.md)** | `.slim/worktrees/` lane を使い、隔離された並列または高リスクなコーディング作業を行います |

+ 3 - 3
README.ko-KR.md

@@ -44,7 +44,7 @@ oh-my-opencode-slim은 OpenCode용 에이전트 오케스트레이션 플러그
 - **[Companion](docs/companion.md)** - 병렬 백그라운드 전문 에이전트를 포함해
 - **[Companion](docs/companion.md)** - 병렬 백그라운드 전문 에이전트를 포함해
   활성 에이전트를 보여 주는 선택적 플로팅 데스크톱 창입니다.
   활성 에이전트를 보여 주는 선택적 플로팅 데스크톱 창입니다.
 - **[멀티플렉서 통합](docs/multiplexer-integration.md)** - Tmux, Zellij,
 - **[멀티플렉서 통합](docs/multiplexer-integration.md)** - Tmux, Zellij,
-  Herdr 또는 cmux 페인에서 에이전트 작업을 실시간으로 확인합니다.
+  Herdr, cmux, 또는 kitty 페인에서 에이전트 작업을 실시간으로 확인합니다.
 - **[프리셋 전환](docs/preset-switching.md)** - `/preset`으로 실행 중에 팀 전체의
 - **[프리셋 전환](docs/preset-switching.md)** - `/preset`으로 실행 중에 팀 전체의
   모델을 교체합니다.
   모델을 교체합니다.
 - **[코드 인텔리전스 도구](docs/tools.md)** - 25개 언어를 지원하는 LSP 도구와
 - **[코드 인텔리전스 도구](docs/tools.md)** - 25개 언어를 지원하는 LSP 도구와
@@ -154,7 +154,7 @@ bun run build
 > 자동 위임이 어떻게 동작하는지 이해하는 것을 **권장**합니다. **[Orchestrator 프롬프트](https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/src/agents/orchestrator.ts#L28)** 에는 위임 규칙, 전문 에이전트 라우팅 로직, 메인 에이전트가 언제 서브에이전트로 작업을 넘겨야 하는지에 대한 임계값이 포함되어 있습니다. 수동으로 위임하려면 `@agentName <task>`로 서브에이전트를 호출하면 됩니다.
 > 자동 위임이 어떻게 동작하는지 이해하는 것을 **권장**합니다. **[Orchestrator 프롬프트](https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/src/agents/orchestrator.ts#L28)** 에는 위임 규칙, 전문 에이전트 라우팅 로직, 메인 에이전트가 언제 서브에이전트로 작업을 넘겨야 하는지에 대한 임계값이 포함되어 있습니다. 수동으로 위임하려면 `@agentName <task>`로 서브에이전트를 호출하면 됩니다.
 
 
 > [!TIP]
 > [!TIP]
-> 이제 백그라운드 에이전트가 기본 워크플로이므로 **[Multiplexer Integration](docs/multiplexer-integration.md)** 을 활성화하고 설정하는 것을 **강력히 권장**합니다. 각 에이전트를 전용 Tmux, Zellij, Herdr 또는 cmux 창에서 자동으로 열어 주기 때문에, Orchestrator가 세션을 계속 조율하는 동안 전문 에이전트들의 작업을 실시간으로 따라볼 수 있습니다.
+> 이제 백그라운드 에이전트가 기본 워크플로이므로 **[Multiplexer Integration](docs/multiplexer-integration.md)** 을 활성화하고 설정하는 것을 **강력히 권장**합니다. 각 에이전트를 전용 Tmux, Zellij, Herdr, cmux, 또는 kitty 창에서 자동으로 열어 주기 때문에, Orchestrator가 세션을 계속 조율하는 동안 전문 에이전트들의 작업을 실시간으로 따라볼 수 있습니다.
 
 
 기본 생성 설정에는 `openai`와 `opencode-go` 프리셋이 모두 포함되어 있습니다.
 기본 생성 설정에는 `openai`와 `opencode-go` 프리셋이 모두 포함되어 있습니다.
 
 
@@ -628,7 +628,7 @@ bunx oh-my-opencode-slim@latest install --companion=yes
 | **[Council](docs/council.md)** | `@council`로 여러 모델을 병렬 실행하고 하나의 답변으로 종합 |
 | **[Council](docs/council.md)** | `@council`로 여러 모델을 병렬 실행하고 하나의 답변으로 종합 |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | 커스텀 프롬프트, 모델, MCP 접근, Orchestrator 위임 규칙으로 커스텀 전문 에이전트 정의 |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | 커스텀 프롬프트, 모델, MCP 접근, Orchestrator 위임 규칙으로 커스텀 전문 에이전트 정의 |
 | **[ACP Agents](docs/acp-agents.md)** | Claude Code ACP 또는 Gemini ACP 같은 외부 ACP 호환 에이전트를 위임 가능한 서브에이전트로 연결 |
 | **[ACP Agents](docs/acp-agents.md)** | Claude Code ACP 또는 Gemini ACP 같은 외부 ACP 호환 에이전트를 위임 가능한 서브에이전트로 연결 |
-| **[Multiplexer Integration](docs/multiplexer-integration.md)** | Tmux, Zellij, Herdr 또는 cmux 페인에서 에이전트 작업을 실시간으로 확인 |
+| **[Multiplexer Integration](docs/multiplexer-integration.md)** | Tmux, Zellij, Herdr, cmux, 또는 kitty 페인에서 에이전트 작업을 실시간으로 확인 |
 | **[Codemap](docs/codemap.md)** | 계층형 코드맵을 생성하여 대규모 코드베이스를 빠르게 파악 |
 | **[Codemap](docs/codemap.md)** | 계층형 코드맵을 생성하여 대규모 코드베이스를 빠르게 파악 |
 | **[Clonedeps](docs/clonedeps.md)** | 선택한 의존성 소스를 무시된 로컬 워크스페이스에 복제하여 검사 |
 | **[Clonedeps](docs/clonedeps.md)** | 선택한 의존성 소스를 무시된 로컬 워크스페이스에 복제하여 검사 |
 | **[Worktrees](docs/worktrees.md)** | `.slim/worktrees/` lane을 사용해 격리된 병렬 또는 고위험 코딩 작업 수행 |
 | **[Worktrees](docs/worktrees.md)** | `.slim/worktrees/` lane을 사용해 격리된 병렬 또는 고위험 코딩 작업 수행 |

+ 3 - 3
README.md

@@ -43,7 +43,7 @@ The main idea is simple: instead of forcing one model to do everything, the plug
 - **[Companion](docs/companion.md)** - an optional floating desktop window
 - **[Companion](docs/companion.md)** - an optional floating desktop window
   showing which agents are active, including parallel background specialists.
   showing which agents are active, including parallel background specialists.
 - **[Multiplexer integration](docs/multiplexer-integration.md)** - watch agents
 - **[Multiplexer integration](docs/multiplexer-integration.md)** - watch agents
-  work live in Tmux, Zellij, Herdr, or cmux panes.
+  work live in Tmux, Zellij, Herdr, cmux, or kitty panes.
 - **[Preset switching](docs/preset-switching.md)** - swap the whole team's
 - **[Preset switching](docs/preset-switching.md)** - swap the whole team's
   models at runtime with `/preset`.
   models at runtime with `/preset`.
 - **[Code intelligence tools](docs/tools.md)** - LSP tools, AST-aware search
 - **[Code intelligence tools](docs/tools.md)** - LSP tools, AST-aware search
@@ -155,7 +155,7 @@ Then:
 > It's **recommended** to understand how background orchestration works. The **[Orchestrator prompt](https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/src/agents/orchestrator.ts#L28)** contains the scheduler rules, specialist routing logic, and thresholds for when work should be assigned to background agents. You can always delegate manually by calling a subagent via: `@agentName <task>`
 > It's **recommended** to understand how background orchestration works. The **[Orchestrator prompt](https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/src/agents/orchestrator.ts#L28)** contains the scheduler rules, specialist routing logic, and thresholds for when work should be assigned to background agents. You can always delegate manually by calling a subagent via: `@agentName <task>`
 
 
 > [!TIP]
 > [!TIP]
-> Because background agents are now the default workflow, it is **highly recommended** to enable and configure **[Multiplexer Integration](docs/multiplexer-integration.md)**. It automatically opens each agent in a dedicated Tmux, Zellij, Herdr, or cmux pane, so you can watch specialists work live while the Orchestrator continues coordinating the session.
+> Because background agents are now the default workflow, it is **highly recommended** to enable and configure **[Multiplexer Integration](docs/multiplexer-integration.md)**. It automatically opens each agent in a dedicated Tmux, Zellij, Herdr, cmux, or kitty pane, so you can watch specialists work live while the Orchestrator continues coordinating the session.
 
 
 The default generated configuration includes both `openai` and `opencode-go` presets.
 The default generated configuration includes both `openai` and `opencode-go` presets.
 
 
@@ -631,7 +631,7 @@ Use this section as a map: start with installation, then jump to features, confi
 | **[Council](docs/council.md)** | Run multiple models in parallel and synthesize a single answer with `@council` |
 | **[Council](docs/council.md)** | Run multiple models in parallel and synthesize a single answer with `@council` |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | Define your own specialists with custom prompts, models, MCP access, and Orchestrator delegation rules |
 | **[Custom Agents](docs/configuration.md#custom-agents)** | Define your own specialists with custom prompts, models, MCP access, and Orchestrator delegation rules |
 | **[ACP Agents](docs/acp-agents.md)** | Connect external ACP-compatible agents such as Claude Code ACP or Gemini ACP as delegatable subagents |
 | **[ACP Agents](docs/acp-agents.md)** | Connect external ACP-compatible agents such as Claude Code ACP or Gemini ACP as delegatable subagents |
-| **[Multiplexer Integration](docs/multiplexer-integration.md)** | Watch agents work live in Tmux, Zellij, Herdr, or cmux panes |
+| **[Multiplexer Integration](docs/multiplexer-integration.md)** | Watch agents work live in Tmux, Zellij, Herdr, cmux, or kitty panes |
 | **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |
 | **[Codemap](docs/codemap.md)** | Generate hierarchical codemaps to understand large codebases faster |
 | **[Clonedeps](docs/clonedeps.md)** | Clone selected dependency source into an ignored local workspace for inspection |
 | **[Clonedeps](docs/clonedeps.md)** | Clone selected dependency source into an ignored local workspace for inspection |
 | **[Worktrees](docs/worktrees.md)** | Use `.slim/worktrees/` lanes for isolated parallel or risky coding work |
 | **[Worktrees](docs/worktrees.md)** | Use `.slim/worktrees/` lanes for isolated parallel or risky coding work |

+ 1 - 1
docs/configuration.md

@@ -137,7 +137,7 @@ Presets can also be switched at runtime without restarting using the `/preset` c
 | `disabled_agents` | string[] | `["observer"]` | Agent names to disable globally. Set to `[]` to enable Observer; this is global, not per-preset |
 | `disabled_agents` | string[] | `["observer"]` | Agent names to disable globally. Set to `[]` to enable Observer; this is global, not per-preset |
 | `image_routing` | `"auto"` \| `"direct"` | omitted (legacy conditional) | Optional. When omitted, images are intercepted only when Observer is enabled, preserving existing behavior. Explicit `"auto"` requires Observer enabled and saves image attachments to disk before nudging delegation to @observer. `"direct"`: always pass images to the orchestrator. |
 | `image_routing` | `"auto"` \| `"direct"` | omitted (legacy conditional) | Optional. When omitted, images are intercepted only when Observer is enabled, preserving existing behavior. Explicit `"auto"` requires Observer enabled and saves image attachments to disk before nudging delegation to @observer. `"direct"`: always pass images to the orchestrator. |
 | `autoUpdate` | boolean | `true` | Automatically install plugin updates in the background; set to `false` for notification-only mode |
 | `autoUpdate` | boolean | `true` | Automatically install plugin updates in the background; set to `false` for notification-only mode |
-| `multiplexer.type` | string | `"none"` | Multiplexer mode: `auto`, `tmux`, `zellij`, `herdr`, `cmux`, or `none` |
+| `multiplexer.type` | string | `"none"` | Multiplexer mode: `auto`, `tmux`, `zellij`, `herdr`, `cmux`, `kitty`, or `none` |
 | `multiplexer.layout` | string | `"main-vertical"` | Layout preset: `main-vertical`, `main-horizontal`, `tiled`, `even-horizontal`, `even-vertical`. Tmux applies full layouts; Zellij and Herdr map supported layouts to split directions; cmux maintains a right-hand agent column |
 | `multiplexer.layout` | string | `"main-vertical"` | Layout preset: `main-vertical`, `main-horizontal`, `tiled`, `even-horizontal`, `even-vertical`. Tmux applies full layouts; Zellij and Herdr map supported layouts to split directions; cmux maintains a right-hand agent column |
 | `multiplexer.main_pane_size` | number | `60` | Main pane size as percentage (20–80) for tmux main layouts; ignored by Zellij, Herdr, and cmux |
 | `multiplexer.main_pane_size` | number | `60` | Main pane size as percentage (20–80) for tmux main layouts; ignored by Zellij, Herdr, and cmux |
 | `multiplexer.zellij_pane_mode` | string | `"agent-tab"` | Zellij pane placement: `agent-tab` creates/reuses a dedicated `opencode-agents` tab; `current-tab` opens subagents as panes in the tab containing the parent OpenCode pane, falling back to the focused tab if the parent pane cannot be resolved |
 | `multiplexer.zellij_pane_mode` | string | `"agent-tab"` | Zellij pane placement: `agent-tab` creates/reuses a dedicated `opencode-agents` tab; `current-tab` opens subagents as panes in the tab containing the parent OpenCode pane, falling back to the focused tab if the parent pane cannot be resolved |

+ 51 - 4
docs/multiplexer-integration.md

@@ -1,7 +1,7 @@
 # Multiplexer Integration Guide
 # Multiplexer Integration Guide
 
 
-Use tmux, Zellij, Herdr, or cmux to watch subagents work in live panes while
-OpenCode keeps running in your main session.
+Use tmux, Zellij, Herdr, cmux, or kitty to watch subagents work in live panes
+while OpenCode keeps running in your main session.
 
 
 ## Table of Contents
 ## Table of Contents
 
 
@@ -143,7 +143,34 @@ Edit `~/.config/opencode/oh-my-opencode-slim.json` (or `.jsonc`):
 
 
 cmux 0.64.14 or newer is required; 0.64.17 or newer is recommended.
 cmux 0.64.14 or newer is required; 0.64.17 or newer is recommended.
 
 
-### 2. Start OpenCode inside tmux, Zellij, Herdr, or cmux
+**Kitty only:**
+
+```jsonc
+{
+  "multiplexer": {
+    "type": "kitty"
+  }
+}
+```
+
+Kitty requires `allow_remote_control` **and** `listen_on` in `kitty.conf`.
+`listen_on` opens a UNIX socket and kitty exports `KITTY_LISTEN_ON` to its
+child processes (including OpenCode). The plugin passes that env through to
+every `kitten @` invocation automatically — no extra plugin config is needed.
+This is required because OpenCode spawns subagent commands in a process
+detached from the kitty window's controlling terminal, where the tty-based
+remote-control path does not work.
+
+```conf
+allow_remote_control yes
+listen_on unix:/tmp/kitty-rc-$(USER)
+```
+
+After editing `kitty.conf`, **restart kitty** (quit and relaunch) so the socket
+is created. Verify with `kitten @ ls` from a normal shell — it should print JSON
+instead of timing out.
+
+### 2. Start OpenCode inside tmux, Zellij, Herdr, cmux, or kitty
 
 
 **Tmux:**
 **Tmux:**
 
 
@@ -169,6 +196,13 @@ opencode --port 4096
 **cmux:** Start OpenCode in a cmux surface. Auto-detection requires cmux to
 **cmux:** Start OpenCode in a cmux surface. Auto-detection requires cmux to
 provide `CMUX_SOCKET_PATH`, `CMUX_WORKSPACE_ID`, and `CMUX_SURFACE_ID`.
 provide `CMUX_SOCKET_PATH`, `CMUX_WORKSPACE_ID`, and `CMUX_SURFACE_ID`.
 
 
+**Kitty:**
+
+```bash
+kitty
+opencode --port 4096
+```
+
 ### 3. Trigger delegated work
 ### 3. Trigger delegated work
 
 
 Ask OpenCode to do something that launches subagents. New panes should appear automatically.
 Ask OpenCode to do something that launches subagents. New panes should appear automatically.
@@ -197,7 +231,7 @@ Please analyze this codebase and create a documentation structure.
 
 
 | Setting | Type | Default | Description |
 | Setting | Type | Default | Description |
 |---------|------|---------|-------------|
 |---------|------|---------|-------------|
-| `type` | string | `"none"` | `"auto"`, `"tmux"`, `"zellij"`, `"herdr"`, `"cmux"`, or `"none"` |
+| `type` | string | `"none"` | `"auto"`, `"tmux"`, `"zellij"`, `"herdr"`, `"cmux"`, `"kitty"`, or `"none"` |
 | `layout` | string | `"main-vertical"` | Layout preset for tmux; mapped to Zellij/Herdr pane directions where possible; ignored by cmux |
 | `layout` | string | `"main-vertical"` | Layout preset for tmux; mapped to Zellij/Herdr pane directions where possible; ignored by cmux |
 | `main_pane_size` | number | `60` | Main pane size percentage for tmux only (`20`-`80`); ignored by Zellij, Herdr, and cmux |
 | `main_pane_size` | number | `60` | Main pane size percentage for tmux only (`20`-`80`); ignored by Zellij, Herdr, and cmux |
 | `zellij_pane_mode` | string | `"agent-tab"` | Zellij pane placement: `"agent-tab"` creates/reuses a dedicated tab; `"current-tab"` opens panes in the tab containing the parent OpenCode pane |
 | `zellij_pane_mode` | string | `"agent-tab"` | Zellij pane placement: `"agent-tab"` creates/reuses a dedicated tab; `"current-tab"` opens panes in the tab containing the parent OpenCode pane |
@@ -210,6 +244,7 @@ Please analyze this codebase and create a documentation structure.
 | **Zellij** | ✅ Supported | Creates a dedicated `opencode-agents` tab by default; can open panes in the parent OpenCode tab with `zellij_pane_mode: "current-tab"`; maps `main-*` layouts to pane directions |
 | **Zellij** | ✅ Supported | Creates a dedicated `opencode-agents` tab by default; can open panes in the parent OpenCode tab with `zellij_pane_mode: "current-tab"`; maps `main-*` layouts to pane directions |
 | **Herdr** | ✅ Supported | Splits panes in the current Herdr workspace; maps `main-vertical`/`even-horizontal`/`tiled` layouts to right splits and `main-horizontal`/`even-vertical` to down splits; no layout rebalancing (like Zellij) |
 | **Herdr** | ✅ Supported | Splits panes in the current Herdr workspace; maps `main-vertical`/`even-horizontal`/`tiled` layouts to right splits and `main-horizontal`/`even-vertical` to down splits; no layout rebalancing (like Zellij) |
 | **cmux** | ✅ Supported | Requires cmux 0.64.14+ (0.64.17+ recommended); creates the agent column to the right and stacks subsequent agents downward without moving focus |
 | **cmux** | ✅ Supported | Requires cmux 0.64.14+ (0.64.17+ recommended); creates the agent column to the right and stacks subsequent agents downward without moving focus |
+| **Kitty** | ✅ Supported | Uses `kitten @ launch` to open new windows; requires `allow_remote_control` **and** `listen_on` in kitty.conf (OpenCode must run inside a kitty window; kitty exports `KITTY_LISTEN_ON` which the plugin passes through to reach kitty from detached subagent processes). No layout rebalancing (like Zellij/Herdr) |
 
 
 The cmux adapter equalizes vertical splits after each successful add and close.
 The cmux adapter equalizes vertical splits after each successful add and close.
 It always creates the first agent to the right and subsequent agents downward;
 It always creates the first agent to the right and subsequent agents downward;
@@ -315,6 +350,18 @@ For Zellij:
 | `even-vertical` | Uses Zellij's native pane placement |
 | `even-vertical` | Uses Zellij's native pane placement |
 | `tiled` | Uses Zellij's native pane placement |
 | `tiled` | Uses Zellij's native pane placement |
 
 
+For kitty:
+
+| Layout | Kitty behavior |
+|--------|----------------|
+| `main-vertical` | Uses `tall` layout (full-height main pane on left, side panes stacked on right) |
+| `main-horizontal` | Uses `fat` layout (full-width main pane on top, side panes tiled below) |
+| `even-horizontal` | Uses `horizontal` layout (all panes side-by-side, equal width) |
+| `even-vertical` | Uses `vertical` layout (all panes stacked, equal height) |
+| `tiled` | Uses `grid` layout (all panes in equal-sized grid) |
+
+> **Note:** kitty has no layout rebalancing API like tmux's `select-layout`, and no per-window layout — so the multiplexer applies the mapped kitty layout (`tall`, `fat`, `grid`, `horizontal`, or `vertical`) as a **global change to the active tab**, overriding whatever layout you had there. The `main_pane_size` config is ignored. The layout is only re-applied when it differs from the currently applied one (e.g., via `applyLayout`).
+
 For Herdr:
 For Herdr:
 
 
 | Layout | Herdr behavior |
 | Layout | Herdr behavior |

+ 1 - 0
oh-my-opencode-slim.schema.json

@@ -941,6 +941,7 @@
             "tmux",
             "tmux",
             "zellij",
             "zellij",
             "herdr",
             "herdr",
+            "kitty",
             "cmux",
             "cmux",
             "none"
             "none"
           ]
           ]

+ 1 - 0
src/config/schema.ts

@@ -131,6 +131,7 @@ export const MultiplexerTypeSchema = z.enum([
   'tmux',
   'tmux',
   'zellij',
   'zellij',
   'herdr',
   'herdr',
+  'kitty',
   'cmux',
   'cmux',
   'none',
   'none',
 ]);
 ]);

+ 10 - 6
src/multiplexer/codemap.md

@@ -2,7 +2,8 @@
 
 
 ## Responsibility
 ## Responsibility
 
 
-Provides a unified abstraction for tmux, Zellij, Herdr, and cmux to spawn,
+Provides a unified abstraction for tmux, Zellij, Herdr, cmux, and kitty to
+spawn,
 manage, and close panes for child OpenCode agent sessions.
 manage, and close panes for child OpenCode agent sessions.
 
 
 ## Design
 ## Design
@@ -14,6 +15,7 @@ manage, and close panes for child OpenCode agent sessions.
   - `TmuxMultiplexer`: tmux-specific implementation using `tmux` CLI commands
   - `TmuxMultiplexer`: tmux-specific implementation using `tmux` CLI commands
   - `ZellijMultiplexer`: zellij-specific implementation using zellij plugin API
   - `ZellijMultiplexer`: zellij-specific implementation using zellij plugin API
   - `HerdrMultiplexer`: herdr-specific implementation using `herdr` CLI commands
   - `HerdrMultiplexer`: herdr-specific implementation using `herdr` CLI commands
+  - `KittyMultiplexer`: kitty-specific implementation using `kitten @` CLI commands
   - `CmuxMultiplexer`: cmux UUID surface implementation using the cmux CLI
   - `CmuxMultiplexer`: cmux UUID surface implementation using the cmux CLI
 - **Shared Utilities** (`shared.ts`): `quoteShellArg`, `buildOpencodeAttachCommand`, and `findBinary` — extracted from the three adapters to eliminate copy-paste duplication.
 - **Shared Utilities** (`shared.ts`): `quoteShellArg`, `buildOpencodeAttachCommand`, and `findBinary` — extracted from the three adapters to eliminate copy-paste duplication.
 - **Session Manager** (`session-manager.ts`): Tracks child session lifecycle and coordinates pane operations via event-driven architecture.
 - **Session Manager** (`session-manager.ts`): Tracks child session lifecycle and coordinates pane operations via event-driven architecture.
@@ -32,7 +34,7 @@ manage, and close panes for child OpenCode agent sessions.
 
 
 ```typescript
 ```typescript
 export interface Multiplexer {
 export interface Multiplexer {
-  readonly type: 'tmux' | 'zellij' | 'herdr' | 'cmux';
+  readonly type: 'tmux' | 'zellij' | 'herdr' | 'cmux' | 'kitty';
   isAvailable(): Promise<boolean>;
   isAvailable(): Promise<boolean>;
   isInsideSession(): boolean;
   isInsideSession(): boolean;
   spawnPane(sessionId: string, description: string, serverUrl: string, directory: string): Promise<PaneResult>;
   spawnPane(sessionId: string, description: string, serverUrl: string, directory: string): Promise<PaneResult>;
@@ -129,7 +131,7 @@ The session manager reacts to OpenCode session events:
 
 
 ```typescript
 ```typescript
 interface MultiplexerConfig {
 interface MultiplexerConfig {
-  type: 'tmux' | 'zellij' | 'herdr' | 'cmux' | 'auto' | 'none';
+  type: 'tmux' | 'zellij' | 'herdr' | 'cmux' | 'kitty' | 'auto' | 'none';
   layout: MultiplexerLayout; // 'tiled' | 'main-horizontal' | 'main-vertical' | 'grid'
   layout: MultiplexerLayout; // 'tiled' | 'main-horizontal' | 'main-vertical' | 'grid'
   main_pane_size?: number; // Percentage for main pane (0-100)
   main_pane_size?: number; // Percentage for main pane (0-100)
   zellij_pane_mode?: string; // Zellij-specific pane mode
   zellij_pane_mode?: string; // Zellij-specific pane mode
@@ -138,9 +140,10 @@ interface MultiplexerConfig {
 
 
 ### Environment Detection
 ### Environment Detection
 
 
-- **Auto Mode**: Detects tmux (`TMUX`), Zellij (`ZELLIJ`), Herdr
-  (`HERDR_ENV`/`HERDR_PANE_ID`), or cmux (complete `CMUX_SOCKET_PATH`,
-  `CMUX_WORKSPACE_ID`, and `CMUX_SURFACE_ID` identity).
+- **Auto Mode**: Detects tmux (`TMUX`), Zellij (`ZELLIJ`), kitty
+  (`KITTY_PID`), Herdr (`HERDR_ENV`/`HERDR_PANE_ID`), or cmux
+  (complete `CMUX_SOCKET_PATH`, `CMUX_WORKSPACE_ID`, and `CMUX_SURFACE_ID`
+  identity).
 - **Availability Check**: Validates multiplexer binary is available before use
 - **Availability Check**: Validates multiplexer binary is available before use
 
 
 ## Implementation Details
 ## Implementation Details
@@ -184,6 +187,7 @@ interface MultiplexerConfig {
 | `tmux/index.ts` | tmux-specific implementation |
 | `tmux/index.ts` | tmux-specific implementation |
 | `zellij/index.ts` | zellij-specific implementation |
 | `zellij/index.ts` | zellij-specific implementation |
 | `herdr/index.ts` | herdr-specific implementation |
 | `herdr/index.ts` | herdr-specific implementation |
+| `kitty/index.ts` | kitty-specific implementation |
 | `cmux/index.ts` | cmux adapter and encoded surface handles |
 | `cmux/index.ts` | cmux adapter and encoded surface handles |
 | `cmux/session-lifecycle.ts` | cmux event, polling, spawn, close, orphan, and cleanup ownership |
 | `cmux/session-lifecycle.ts` | cmux event, polling, spawn, close, orphan, and cleanup ownership |
 | `cmux/session-state.ts` | process-global cmux session registry |
 | `cmux/session-state.ts` | process-global cmux session registry |

+ 40 - 0
src/multiplexer/factory.test.ts

@@ -12,11 +12,15 @@ describe('multiplexer factory', () => {
   const originalCmuxSocket = process.env.CMUX_SOCKET_PATH;
   const originalCmuxSocket = process.env.CMUX_SOCKET_PATH;
   const originalCmuxWorkspace = process.env.CMUX_WORKSPACE_ID;
   const originalCmuxWorkspace = process.env.CMUX_WORKSPACE_ID;
   const originalCmuxSurface = process.env.CMUX_SURFACE_ID;
   const originalCmuxSurface = process.env.CMUX_SURFACE_ID;
+  const originalKittyPid = process.env.KITTY_PID;
+  const originalKittyWindowId = process.env.KITTY_WINDOW_ID;
 
 
   beforeEach(() => {
   beforeEach(() => {
     delete process.env.CMUX_SOCKET_PATH;
     delete process.env.CMUX_SOCKET_PATH;
     delete process.env.CMUX_WORKSPACE_ID;
     delete process.env.CMUX_WORKSPACE_ID;
     delete process.env.CMUX_SURFACE_ID;
     delete process.env.CMUX_SURFACE_ID;
+    delete process.env.KITTY_PID;
+    delete process.env.KITTY_WINDOW_ID;
   });
   });
 
 
   afterEach(() => {
   afterEach(() => {
@@ -27,6 +31,8 @@ describe('multiplexer factory', () => {
     process.env.CMUX_SOCKET_PATH = originalCmuxSocket;
     process.env.CMUX_SOCKET_PATH = originalCmuxSocket;
     process.env.CMUX_WORKSPACE_ID = originalCmuxWorkspace;
     process.env.CMUX_WORKSPACE_ID = originalCmuxWorkspace;
     process.env.CMUX_SURFACE_ID = originalCmuxSurface;
     process.env.CMUX_SURFACE_ID = originalCmuxSurface;
+    process.env.KITTY_PID = originalKittyPid;
+    process.env.KITTY_WINDOW_ID = originalKittyWindowId;
   });
   });
 
 
   test('returns a fresh tmux instance per call', async () => {
   test('returns a fresh tmux instance per call', async () => {
@@ -173,4 +179,38 @@ describe('multiplexer factory', () => {
       })?.type,
       })?.type,
     ).toBe('cmux');
     ).toBe('cmux');
   });
   });
+
+  test('returns a kitty instance when type is kitty', async () => {
+    delete process.env.KITTY_PID;
+    const { getMultiplexer } = await importFreshFactory('kitty-explicit');
+    const multiplexer = getMultiplexer({
+      type: 'kitty',
+      layout: 'main-vertical',
+      main_pane_size: 60,
+      zellij_pane_mode: 'agent-tab',
+    });
+    expect(multiplexer).not.toBeNull();
+    expect(multiplexer?.type).toBe('kitty');
+  });
+
+  test('auto-detects kitty when KITTY_PID is set', async () => {
+    delete process.env.TMUX;
+    delete process.env.TMUX_PANE;
+    delete process.env.ZELLIJ;
+    delete process.env.HERDR_ENV;
+    delete process.env.HERDR_PANE_ID;
+    delete process.env.CMUX_SOCKET_PATH;
+    delete process.env.CMUX_WORKSPACE_ID;
+    delete process.env.CMUX_SURFACE_ID;
+    process.env.KITTY_PID = '12345';
+    const { getMultiplexer } = await importFreshFactory('auto-kitty');
+    expect(
+      getMultiplexer({
+        type: 'auto',
+        layout: 'main-vertical',
+        main_pane_size: 60,
+        zellij_pane_mode: 'agent-tab',
+      })?.type,
+    ).toBe('kitty');
+  });
 });
 });

+ 11 - 0
src/multiplexer/factory.ts

@@ -6,6 +6,7 @@ import type { MultiplexerConfig, MultiplexerType } from '../config/schema';
 import { log } from '../utils/logger';
 import { log } from '../utils/logger';
 import { CmuxMultiplexer } from './cmux';
 import { CmuxMultiplexer } from './cmux';
 import { HerdrMultiplexer } from './herdr';
 import { HerdrMultiplexer } from './herdr';
+import { KittyMultiplexer } from './kitty';
 import { TmuxMultiplexer } from './tmux';
 import { TmuxMultiplexer } from './tmux';
 import type { Multiplexer } from './types';
 import type { Multiplexer } from './types';
 import { ZellijMultiplexer } from './zellij';
 import { ZellijMultiplexer } from './zellij';
@@ -49,6 +50,10 @@ export function getMultiplexer(config: MultiplexerConfig): Multiplexer | null {
       multiplexer = new CmuxMultiplexer();
       multiplexer = new CmuxMultiplexer();
       actualType = 'cmux';
       actualType = 'cmux';
       break;
       break;
+    case 'kitty':
+      multiplexer = new KittyMultiplexer(config.layout, config.main_pane_size);
+      actualType = 'kitty';
+      break;
     case 'auto': {
     case 'auto': {
       // Auto-detect based on environment variables only
       // Auto-detect based on environment variables only
       // Note: Does NOT fall back to binary availability checks
       // Note: Does NOT fall back to binary availability checks
@@ -69,6 +74,12 @@ export function getMultiplexer(config: MultiplexerConfig): Multiplexer | null {
           config.zellij_pane_mode,
           config.zellij_pane_mode,
         );
         );
         actualType = 'zellij';
         actualType = 'zellij';
+      } else if (process.env.KITTY_PID || process.env.KITTY_WINDOW_ID) {
+        multiplexer = new KittyMultiplexer(
+          config.layout,
+          config.main_pane_size,
+        );
+        actualType = 'kitty';
       } else if (process.env.HERDR_ENV || process.env.HERDR_PANE_ID) {
       } else if (process.env.HERDR_ENV || process.env.HERDR_PANE_ID) {
         multiplexer = new HerdrMultiplexer(
         multiplexer = new HerdrMultiplexer(
           config.layout,
           config.layout,

+ 1 - 0
src/multiplexer/index.ts

@@ -9,6 +9,7 @@ export {
   startAvailabilityCheck,
   startAvailabilityCheck,
 } from './factory';
 } from './factory';
 export { HerdrMultiplexer } from './herdr';
 export { HerdrMultiplexer } from './herdr';
+export { KittyMultiplexer } from './kitty';
 export {
 export {
   MultiplexerSessionManager,
   MultiplexerSessionManager,
   TmuxSessionManager,
   TmuxSessionManager,

+ 311 - 0
src/multiplexer/kitty/index.test.ts

@@ -0,0 +1,311 @@
+import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
+
+type SpawnResult = {
+  exited: Promise<number>;
+  stdout: () => Promise<string>;
+  stderr: () => Promise<string>;
+  kill: () => boolean;
+  exitCode: number | null;
+  proc: never;
+};
+
+const logMock = mock(() => {});
+const crossSpawnMock = mock((_command: string[]) => createSpawnResult());
+
+mock.module('../../utils/logger', () => ({
+  log: logMock,
+}));
+
+mock.module('../../utils/compat', () => ({
+  crossSpawn: crossSpawnMock,
+}));
+
+let importCounter = 0;
+
+function createSpawnResult(
+  exitCode = 0,
+  stdout = '',
+  stderr = '',
+): SpawnResult {
+  return {
+    exited: Promise.resolve(exitCode),
+    stdout: () => Promise.resolve(stdout),
+    stderr: () => Promise.resolve(stderr),
+    kill: () => true,
+    exitCode,
+    proc: {} as never,
+  };
+}
+
+async function importFreshKitty() {
+  return import(`./index?test=${importCounter++}`);
+}
+
+function commands(): string[][] {
+  return crossSpawnMock.mock.calls.map((call) => call[0] as string[]);
+}
+
+describe('KittyMultiplexer', () => {
+  const originalKittyPid = process.env.KITTY_PID;
+  const originalKittyWindowId = process.env.KITTY_WINDOW_ID;
+
+  beforeEach(() => {
+    process.env.KITTY_PID = '12345';
+    process.env.KITTY_WINDOW_ID = '1';
+
+    logMock.mockClear();
+    crossSpawnMock.mockReset();
+    crossSpawnMock.mockImplementation((command: string[]) => {
+      // which kitten
+      if (command[0] === 'which' && command[1] === 'kitten') {
+        return createSpawnResult(0, '/usr/bin/kitten\n');
+      }
+      // which kitty (fallback)
+      if (command[0] === 'which' && command[1] === 'kitty') {
+        return createSpawnResult(1, '');
+      }
+      // kitten @ launch
+      if (command.includes('@') && command.includes('launch')) {
+        return createSpawnResult(0, '42\n');
+      }
+      return createSpawnResult();
+    });
+  });
+
+  afterEach(() => {
+    process.env.KITTY_PID = originalKittyPid;
+    process.env.KITTY_WINDOW_ID = originalKittyWindowId;
+  });
+
+  test('isAvailable returns true when kitten is found', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+    expect(await kitty.isAvailable()).toBe(true);
+  });
+
+  test('isAvailable returns false when kitten is not found', async () => {
+    crossSpawnMock.mockReset();
+    crossSpawnMock.mockImplementation((command: string[]) => {
+      if (command[0] === 'which' && command[1] === 'kitten') {
+        return createSpawnResult(1, '');
+      }
+      if (command[0] === 'which' && command[1] === 'kitty') {
+        return createSpawnResult(1, '');
+      }
+      return createSpawnResult();
+    });
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+    expect(await kitty.isAvailable()).toBe(false);
+  });
+
+  test('isInsideSession returns true when KITTY_PID is set', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+    expect(kitty.isInsideSession()).toBe(true);
+  });
+
+  test('isInsideSession returns false when KITTY_PID is not set', async () => {
+    delete process.env.KITTY_PID;
+    delete process.env.KITTY_WINDOW_ID;
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+    expect(kitty.isInsideSession()).toBe(false);
+  });
+
+  test('spawnPane returns failure when not inside a kitty session', async () => {
+    delete process.env.KITTY_PID;
+    delete process.env.KITTY_WINDOW_ID;
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    const result = await kitty.spawnPane(
+      'session-1',
+      'First worker',
+      'http://localhost:4096',
+      '/repo',
+    );
+
+    expect(result.success).toBe(false);
+    // No kitten @ launch should have been attempted.
+    const launchCmds = commands().filter(
+      (c) => c.includes('@') && c.includes('launch'),
+    );
+    expect(launchCmds.length).toBe(0);
+  });
+
+  test('closePane returns false when not inside a kitty session', async () => {
+    delete process.env.KITTY_PID;
+    delete process.env.KITTY_WINDOW_ID;
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    const closed = await kitty.closePane('42');
+    expect(closed).toBe(false);
+  });
+
+  test('applyLayout is a no-op when not inside a kitty session', async () => {
+    delete process.env.KITTY_PID;
+    delete process.env.KITTY_WINDOW_ID;
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    await kitty.applyLayout('tiled', 60);
+
+    const layoutCmds = commands().filter((c) => c.includes('goto-layout'));
+    expect(layoutCmds.length).toBe(0);
+  });
+
+  test('spawnPane parses integer window id from launch stdout', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    const result = await kitty.spawnPane(
+      'session-1',
+      'First worker',
+      'http://localhost:4096',
+      '/repo',
+    );
+
+    expect(result.success).toBe(true);
+    expect(result.paneId).toBe('42');
+  });
+
+  test('spawnPane returns failure when exit code is non-zero', async () => {
+    crossSpawnMock.mockImplementation((command: string[]) => {
+      if (command[0] === 'which' && command[1] === 'kitten') {
+        return createSpawnResult(0, '/usr/bin/kitten\n');
+      }
+      if (command.includes('@') && command.includes('launch')) {
+        return createSpawnResult(1, '', 'launch failed');
+      }
+      return createSpawnResult();
+    });
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    const result = await kitty.spawnPane(
+      'session-1',
+      'First worker',
+      'http://localhost:4096',
+      '/repo',
+    );
+
+    expect(result.success).toBe(false);
+  });
+
+  test('closePane issues send-key then close-window', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    const closed = await kitty.closePane('42');
+    expect(closed).toBe(true);
+
+    const cmds = commands();
+    const sendKeyCmds = cmds.filter((c) => c.includes('send-key'));
+    const closeWindowCmds = cmds.filter((c) => c.includes('close-window'));
+
+    expect(sendKeyCmds.length).toBeGreaterThanOrEqual(1);
+    expect(closeWindowCmds.length).toBeGreaterThanOrEqual(1);
+
+    // Verify correct arguments
+    const sendKeyArgs = sendKeyCmds[0];
+    expect(sendKeyArgs).toContain('@');
+    expect(sendKeyArgs).toContain('send-key');
+    expect(sendKeyArgs).toContain('--match');
+    expect(sendKeyArgs).toContain('id:42');
+    expect(sendKeyArgs).toContain('ctrl+c');
+
+    const closeArgs = closeWindowCmds[0];
+    expect(closeArgs).toContain('@');
+    expect(closeArgs).toContain('close-window');
+    expect(closeArgs).toContain('--match');
+    expect(closeArgs).toContain('id:42');
+  });
+
+  test('applyLayout issues goto-layout tall for main-vertical', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    await kitty.applyLayout('main-vertical', 60);
+
+    const cmds = commands();
+    const layoutCmds = cmds.filter((c) => c.includes('goto-layout'));
+    expect(layoutCmds.length).toBeGreaterThanOrEqual(1);
+    expect(layoutCmds[0]).toContain('goto-layout');
+    expect(layoutCmds[0]).toContain('tall');
+  });
+
+  test('applyLayout issues goto-layout fat for main-horizontal', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    await kitty.applyLayout('main-horizontal', 60);
+
+    const cmds = commands();
+    const layoutCmds = cmds.filter((c) => c.includes('goto-layout'));
+    expect(layoutCmds.length).toBeGreaterThanOrEqual(1);
+    expect(layoutCmds[0]).toContain('goto-layout');
+    expect(layoutCmds[0]).toContain('fat');
+  });
+
+  test('applyLayout issues goto-layout grid for tiled', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    await kitty.applyLayout('tiled', 60);
+
+    const cmds = commands();
+    const layoutCmds = cmds.filter((c) => c.includes('goto-layout'));
+    expect(layoutCmds.length).toBeGreaterThanOrEqual(1);
+    expect(layoutCmds[0]).toContain('goto-layout');
+    expect(layoutCmds[0]).toContain('grid');
+  });
+
+  test('applyLayout issues goto-layout horizontal for even-horizontal', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    await kitty.applyLayout('even-horizontal', 60);
+
+    const cmds = commands();
+    const layoutCmds = cmds.filter((c) => c.includes('goto-layout'));
+    expect(layoutCmds.length).toBeGreaterThanOrEqual(1);
+    expect(layoutCmds[0]).toContain('goto-layout');
+    expect(layoutCmds[0]).toContain('horizontal');
+  });
+
+  test('applyLayout issues goto-layout vertical for even-vertical', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer();
+
+    await kitty.applyLayout('even-vertical', 60);
+
+    const cmds = commands();
+    const layoutCmds = cmds.filter((c) => c.includes('goto-layout'));
+    expect(layoutCmds.length).toBeGreaterThanOrEqual(1);
+    expect(layoutCmds[0]).toContain('goto-layout');
+    expect(layoutCmds[0]).toContain('vertical');
+  });
+
+  test('applyLayout switches layout after a spawn', async () => {
+    const { KittyMultiplexer } = await importFreshKitty();
+    const kitty = new KittyMultiplexer('main-vertical');
+
+    // First spawn applies 'tall'
+    await kitty.spawnPane(
+      'session-1',
+      'First worker',
+      'http://localhost:4096',
+      '/repo',
+    );
+    // applyLayout to a different layout must re-apply (goto-layout grid)
+    await kitty.applyLayout('tiled', 60);
+
+    const cmds = commands();
+    const layoutCmds = cmds.filter((c) => c.includes('goto-layout'));
+    expect(layoutCmds.length).toBe(2);
+    expect(layoutCmds[0]).toContain('tall');
+    expect(layoutCmds[1]).toContain('grid');
+  });
+});

+ 338 - 0
src/multiplexer/kitty/index.ts

@@ -0,0 +1,338 @@
+/**
+ * Kitty multiplexer implementation
+ *
+ * Uses kitty's remote-control CLI (`kitten @`) to manage windows.
+ * A kitty WINDOW is the 1:1 equivalent of a tmux pane (full PTY).
+ *
+ * Requires `allow_remote_control` **and** `listen_on` in kitty.conf. When
+ * `listen_on` is set, kitty exports `KITTY_LISTEN_ON` to its child processes
+ * (including OpenCode). The plugin passes that env through to `kitten @` so
+ * remote control goes via the socket instead of the controlling terminal,
+ * because OpenCode spawns subagent commands in a process detached from the
+ * kitty window's tty, where the tty-based remote-control path does not work.
+ *
+ * Layout mapping:
+ * - main-vertical → tall layout (full-height main pane on left, side panes stacked on right)
+ * - main-horizontal → fat layout (full-width main pane on top, side panes tiled below)
+ * - tiled → grid layout (equal-sized cells)
+ * - even-horizontal → horizontal layout (all panes side-by-side)
+ * - even-vertical → vertical layout (all panes stacked)
+ *
+ * Maps each plugin layout to the closest kitty built-in layout. kitty has no
+ * per-window layout API, so the chosen layout is applied as a global change to
+ * the active tab (overriding whatever layout was active there).
+ */
+
+import type { MultiplexerLayout } from '../../config/schema';
+import { crossSpawn } from '../../utils/compat';
+import { log } from '../../utils/logger';
+import {
+  buildOpencodeAttachCommand,
+  buildShellLaunchArgs,
+  findBinary,
+  gracefulClosePane,
+  normalizePathForShell,
+  resolveOpencodeExecutable,
+} from '../shared';
+import type { Multiplexer, PaneResult } from '../types';
+
+export class KittyMultiplexer implements Multiplexer {
+  readonly type = 'kitty' as const;
+
+  private binaryPath: string | null = null;
+  private hasChecked = false;
+  private storedLayout: MultiplexerLayout;
+  private appliedLayout: string | null = null;
+
+  constructor(layout: MultiplexerLayout = 'main-vertical', mainPaneSize = 60) {
+    void mainPaneSize; // kitty uses layout bias, not main pane size
+    this.storedLayout = layout;
+  }
+
+  async isAvailable(): Promise<boolean> {
+    if (this.hasChecked) {
+      return this.binaryPath !== null;
+    }
+
+    // Try `kitten` first (the recommended remote-control CLI). Fall back to
+    // `kitty` only if `kitten` is absent — `kitty @` is an alias for
+    // `kitten @` (remote control), not a request to open a new window.
+    this.binaryPath =
+      (await findBinary('kitten')) ?? (await findBinary('kitty'));
+    this.hasChecked = true;
+    return this.binaryPath !== null;
+  }
+
+  isInsideSession(): boolean {
+    return !!process.env.KITTY_PID || !!process.env.KITTY_WINDOW_ID;
+  }
+
+  /**
+   * The kitty multiplexer needs `listen_on` configured in kitty.conf so kitty
+   * exports `KITTY_LISTEN_ON` to its children. Without it, `kitten @` cannot
+   * reach kitty from OpenCode's detached subagent processes (the tty path does
+   * not work). Returns true when the socket env is present.
+   */
+  private hasListenOn(): boolean {
+    return !!process.env.KITTY_LISTEN_ON;
+  }
+
+  async spawnPane(
+    sessionId: string,
+    description: string,
+    serverUrl: string,
+    directory: string,
+  ): Promise<PaneResult> {
+    const kitten = await this.getBinary();
+    if (!kitten) {
+      log('[kitty] spawnPane: kitten/kitty binary not found');
+      return { success: false };
+    }
+
+    // The kitty multiplexer only works when OpenCode is itself running inside
+    // a kitty window (so `kitten @` can reach the instance). If not, fail
+    // cleanly instead of shelling out to a command that cannot succeed.
+    if (!this.isInsideSession()) {
+      log(
+        '[kitty] spawnPane: OpenCode is not running inside a kitty session; ' +
+          'set multiplexer.type to a different backend or run OpenCode inside kitty',
+      );
+      return { success: false };
+    }
+
+    // `listen_on` must be set in kitty.conf so kitty exports KITTY_LISTEN_ON.
+    // Without it, remote control cannot reach kitty from detached subagent
+    // processes. Fail with an actionable message instead of a silent error.
+    if (!this.hasListenOn()) {
+      log(
+        '[kitty] spawnPane: KITTY_LISTEN_ON is not set. Add `listen_on ' +
+          'unix:/tmp/kitty-rc-$(USER)` to kitty.conf and restart kitty so the ' +
+          'plugin can drive kitty via the socket.',
+      );
+      return { success: false };
+    }
+
+    // Map layout to kitty layout and location
+    const { kittyLayout, location } = getKittyLayoutConfig(this.storedLayout);
+
+    // Ensure the correct layout is active
+    await this.ensureLayout(kittyLayout);
+
+    try {
+      const opencodeCmd = buildOpencodeAttachCommand(
+        sessionId,
+        serverUrl,
+        directory,
+        resolveOpencodeExecutable(),
+      );
+
+      // Normalize for Windows/MSYS2/Git Bash (backslashes would be treated as
+      // escape chars). No-op on macOS/Linux. Mirrors the herdr adapter.
+      const attachDir = normalizePathForShell(directory);
+
+      // Launch in the user's interactive shell (fish/bash/zsh/sh) so the
+      // command resolves correctly — a hardcoded `sh -c` breaks under fish
+      // and misses login startup files where `opencode` may live on PATH.
+      const shellArgs = buildShellLaunchArgs(opencodeCmd);
+
+      const args = [
+        '@',
+        'launch',
+        '--type=window',
+        `--location=${location}`,
+        `--title=${description.slice(0, 60)}`,
+        `--cwd=${attachDir}`,
+        '--keep-focus',
+        '--',
+        ...shellArgs,
+      ];
+
+      log('[kitty] spawnPane: executing', { kitten, args });
+
+      const proc = crossSpawn([kitten, ...args], {
+        stdout: 'pipe',
+        stderr: 'pipe',
+        env: this.kittyEnv(),
+      });
+
+      const exitCode = await proc.exited;
+      const stdout = await proc.stdout();
+      const stderr = await proc.stderr();
+
+      // Kitty prints the integer window id on stdout (last non-empty line)
+      const lines = stdout
+        .trim()
+        .split('\n')
+        .map((l) => l.trim())
+        .filter(Boolean);
+      const windowId = lines.length > 0 ? lines[lines.length - 1] : '';
+
+      log('[kitty] spawnPane: result', {
+        exitCode,
+        windowId,
+        stderr: stderr.trim(),
+      });
+
+      if (exitCode === 0 && windowId) {
+        log('[kitty] spawnPane: SUCCESS', { windowId });
+        return { success: true, paneId: windowId };
+      }
+
+      return { success: false };
+    } catch (err) {
+      log('[kitty] spawnPane: exception', { error: String(err) });
+      return { success: false };
+    }
+  }
+
+  async closePane(paneId: string): Promise<boolean> {
+    if (!this.isInsideSession()) {
+      log(
+        '[kitty] closePane: OpenCode is not running inside a kitty session; ' +
+          'cannot target a kitty instance',
+      );
+      return false;
+    }
+    if (!this.hasListenOn()) {
+      log(
+        '[kitty] closePane: KITTY_LISTEN_ON is not set; cannot target kitty. ' +
+          'Add `listen_on` to kitty.conf and restart kitty.',
+      );
+      return false;
+    }
+    const kitten = await this.getBinary();
+    return await gracefulClosePane(kitten, paneId, {
+      ctrlC: ['@', 'send-key', '--match', `id:${paneId}`, 'ctrl+c'],
+      close: ['@', 'close-window', '--match', `id:${paneId}`],
+      acceptExitCode1: true,
+      emptyPaneReturnsTrue: true,
+      env: this.kittyEnv(),
+    });
+  }
+
+  async applyLayout(
+    layout: MultiplexerLayout,
+    mainPaneSize: number,
+  ): Promise<void> {
+    void mainPaneSize; // kitty uses layout bias, not main pane size
+    if (!this.isInsideSession()) {
+      log(
+        '[kitty] applyLayout: OpenCode is not running inside a kitty session; ' +
+          'cannot target a kitty instance',
+      );
+      return;
+    }
+    if (!this.hasListenOn()) {
+      log(
+        '[kitty] applyLayout: KITTY_LISTEN_ON is not set; cannot target kitty. ' +
+          'Add `listen_on` to kitty.conf and restart kitty.',
+      );
+      return;
+    }
+    this.storedLayout = layout;
+    const { kittyLayout } = getKittyLayoutConfig(layout);
+    await this.ensureLayout(kittyLayout);
+  }
+
+  private async runKitty(kitten: string, args: string[]): Promise<number> {
+    const proc = crossSpawn([kitten, ...args], {
+      stdout: 'pipe',
+      stderr: 'pipe',
+      env: this.kittyEnv(),
+    });
+    const [exitCode, , stderr] = await Promise.all([
+      proc.exited,
+      proc.stdout(),
+      proc.stderr(),
+    ]);
+
+    if (exitCode !== 0) {
+      log('[kitty] command failed', {
+        command: args[1],
+        args: [kitten, ...args],
+        exitCode,
+        stderr: stderr.trim(),
+      });
+    }
+
+    return exitCode;
+  }
+
+  private async ensureLayout(kittyLayout: string): Promise<void> {
+    // Skip if this layout is already applied (avoids redundant global switches).
+    // Tracking the applied layout (not a boolean) lets applyLayout switch to a
+    // different layout even after panes have spawned.
+    if (this.appliedLayout === kittyLayout) return;
+    const kitten = await this.getBinary();
+    if (!kitten) return;
+    try {
+      const exitCode = await this.runKitty(kitten, [
+        '@',
+        'goto-layout',
+        kittyLayout,
+      ]);
+      // Only record on success so a transient failure can retry.
+      if (exitCode === 0) this.appliedLayout = kittyLayout;
+    } catch (err) {
+      log('[kitty] ensureLayout: exception', { error: String(err) });
+    }
+  }
+
+  private async getBinary(): Promise<string | null> {
+    await this.isAvailable();
+    return this.binaryPath;
+  }
+
+  /**
+   * Build the env for `kitten @` invocations. When kitty's `listen_on` socket
+   * is configured, kitty sets `KITTY_LISTEN_ON` in its child processes (which
+   * includes OpenCode). We pass that env through so remote control goes via the
+   * socket instead of the controlling terminal (required for the plugin's
+   * detached subagent processes). When the var is absent we return undefined
+   * and `kitten @` falls back to the tty path.
+   */
+  private kittyEnv(): Record<string, string | undefined> | undefined {
+    const listenOn = process.env.KITTY_LISTEN_ON;
+    if (!listenOn) return undefined;
+    return { ...process.env, KITTY_LISTEN_ON: listenOn };
+  }
+}
+
+/**
+ * Map plugin layout to kitty layout and launch location.
+ *
+ * Each plugin layout maps to the closest kitty built-in layout
+ * (tall/fat/grid/horizontal/vertical). kitty has no per-window layout, so the
+ * chosen layout is applied globally to the active tab. `--location=after` is
+ * used for spawning since the layout engine places new windows.
+ */
+function getKittyLayoutConfig(layout: MultiplexerLayout): {
+  kittyLayout: string;
+  location: string;
+} {
+  switch (layout) {
+    case 'main-vertical':
+      // tall = full-height main pane on left, side panes stacked on right
+      // after = place new window after active window (stacked vertically)
+      return { kittyLayout: 'tall', location: 'after' };
+    case 'main-horizontal':
+      // fat = full-width main pane on top, side panes tiled below
+      // after = place new window after active window (tiled horizontally)
+      return { kittyLayout: 'fat', location: 'after' };
+    case 'tiled':
+      // grid = all windows in a balanced grid, same size
+      return { kittyLayout: 'grid', location: 'after' };
+    case 'even-horizontal':
+      // horizontal = all windows side-by-side, equal width
+      return { kittyLayout: 'horizontal', location: 'after' };
+    case 'even-vertical':
+      // vertical = all windows stacked, equal height
+      return { kittyLayout: 'vertical', location: 'after' };
+    default: {
+      // Exhaustiveness check: a new MultiplexerLayout value must be added
+      // above, otherwise this assignment fails to type-check.
+      const _exhaustive: never = layout;
+      return _exhaustive;
+    }
+  }
+}

+ 71 - 0
src/multiplexer/shared.test.ts

@@ -182,3 +182,74 @@ describe('buildOpencodeAttachCommand', () => {
     }
     }
   });
   });
 });
 });
+
+describe('buildShellLaunchArgs', () => {
+  const cases: Array<{
+    shell: string;
+    expected: (cmd: string) => string[];
+  }> = [
+    {
+      shell: '/opt/homebrew/bin/fish',
+      expected: (cmd) => ['/opt/homebrew/bin/fish', '-c', cmd],
+    },
+    {
+      shell: '/usr/bin/nu',
+      expected: (cmd) => ['/usr/bin/nu', '-c', cmd],
+    },
+    {
+      shell: '/bin/zsh',
+      expected: (cmd) => ['/bin/zsh', '-l', '-c', expect.stringContaining(cmd)],
+    },
+    {
+      shell: '/bin/bash',
+      expected: (cmd) => [
+        '/bin/bash',
+        '-l',
+        '-c',
+        expect.stringContaining(cmd),
+      ],
+    },
+    {
+      shell: 'C:\\Windows\\System32\\cmd.exe',
+      expected: (cmd) => ['C:\\Windows\\System32\\cmd.exe', '/c', cmd],
+    },
+    {
+      shell: '/usr/bin/pwsh',
+      expected: (cmd) => ['/usr/bin/pwsh', '-NoProfile', '-Command', cmd],
+    },
+    {
+      shell: '/bin/dash',
+      expected: (cmd) => ['/bin/dash', '-c', cmd],
+    },
+    {
+      shell: '/usr/bin/elvish',
+      expected: (cmd) => ['/usr/bin/elvish', '-c', cmd],
+    },
+  ];
+
+  for (const { shell, expected } of cases) {
+    test(`uses correct args for ${shell}`, async () => {
+      const original = process.env.SHELL;
+      process.env.SHELL = shell;
+      try {
+        const { buildShellLaunchArgs } = await importShared();
+        const cmd = 'opencode attach url --session s';
+        expect(buildShellLaunchArgs(cmd)).toEqual(expected(cmd));
+      } finally {
+        process.env.SHELL = original;
+      }
+    });
+  }
+
+  test('falls back to /bin/sh when SHELL is unset', async () => {
+    const original = process.env.SHELL;
+    delete process.env.SHELL;
+    try {
+      const { buildShellLaunchArgs } = await importShared();
+      const cmd = 'opencode attach url';
+      expect(buildShellLaunchArgs(cmd)).toEqual(['/bin/sh', '-c', cmd]);
+    } finally {
+      process.env.SHELL = original;
+    }
+  });
+});

+ 66 - 0
src/multiplexer/shared.ts

@@ -39,6 +39,68 @@ export function buildOpencodeAttachCommand(
   ].join(' ');
   ].join(' ');
 }
 }
 
 
+/**
+ * Resolve the absolute path to the running OpenCode binary so child shells
+ * (e.g. a kitty-launched window) don't need `opencode` on their PATH. Falls
+ * back to the bare `opencode` name when no absolute path can be determined.
+ */
+export function resolveOpencodeExecutable(): string {
+  return resolveHostOpencodeBinary() ?? 'opencode';
+}
+
+/**
+ * Build the `[shell, ...shellArgs, command]` array for launching a command in
+ * the user's interactive shell. OpenCode respects the user's shell when running
+ * commands; multiplexer panes must do the same, otherwise a hardcoded `sh -c`
+ * breaks under non-POSIX shells (fish, nu, powershell, ...) or misses login
+ * startup files (where `opencode` may be on PATH).
+ *
+ * Mirrors OpenCode's own `Shell.args()` resolution:
+ * - nu / fish: `<shell> -c <command>` (no login mode)
+ * - zsh: login mode, sources zshenv/zshrc, then runs command
+ * - bash: login mode, sources bashrc, then runs command
+ * - cmd: `cmd /c <command>`
+ * - powershell: `pwsh -NoProfile -Command <command>`
+ * - default (sh/dash/elvish/xonsh/...): `<shell> -c <command>`
+ *
+ * Note: the working directory is supplied by the launcher (e.g. kitty's
+ * `--cwd`), not by a `cd` inside the shell command.
+ */
+export function buildShellLaunchArgs(command: string): string[] {
+  const shell = process.env.SHELL || '/bin/sh';
+  const name = (shell.split(/[/\\]/).at(-1) ?? 'sh').replace(
+    /\.(exe|EXE)$/,
+    '',
+  );
+
+  if (name === 'nu' || name === 'fish') {
+    return [shell, '-c', command];
+  }
+  if (name === 'zsh') {
+    return [
+      shell,
+      '-l',
+      '-c',
+      `[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true\n[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1\n${command}`,
+    ];
+  }
+  if (name === 'bash') {
+    return [
+      shell,
+      '-l',
+      '-c',
+      `shopt -s expand_aliases\n[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true\n${command}`,
+    ];
+  }
+  if (name === 'cmd') {
+    return [shell, '/c', command];
+  }
+  if (name === 'pwsh' || name === 'powershell') {
+    return [shell, '-NoProfile', '-Command', command];
+  }
+  return [shell, '-c', command];
+}
+
 export function resolveHostOpencodeBinary(
 export function resolveHostOpencodeBinary(
   options: {
   options: {
     override?: string;
     override?: string;
@@ -140,6 +202,8 @@ export interface GracefulClosePaneOptions {
   acceptExitCode1?: boolean;
   acceptExitCode1?: boolean;
   /** Return true for empty/unknown paneId instead of false (zellij/herdr behavior). */
   /** Return true for empty/unknown paneId instead of false (zellij/herdr behavior). */
   emptyPaneReturnsTrue?: boolean;
   emptyPaneReturnsTrue?: boolean;
+  /** Env to pass to the kitten/kitty invocations (e.g. KITTY_LISTEN_ON). */
+  env?: Record<string, string | undefined>;
 }
 }
 
 
 export async function gracefulClosePane(
 export async function gracefulClosePane(
@@ -156,6 +220,7 @@ export async function gracefulClosePane(
     const ctrlCProc = crossSpawn([binary, ...options.ctrlC], {
     const ctrlCProc = crossSpawn([binary, ...options.ctrlC], {
       stdout: 'ignore',
       stdout: 'ignore',
       stderr: 'ignore',
       stderr: 'ignore',
+      env: options.env,
     });
     });
     await ctrlCProc.exited;
     await ctrlCProc.exited;
 
 
@@ -164,6 +229,7 @@ export async function gracefulClosePane(
     const proc = crossSpawn([binary, ...options.close], {
     const proc = crossSpawn([binary, ...options.close], {
       stdout: 'ignore',
       stdout: 'ignore',
       stderr: 'ignore',
       stderr: 'ignore',
+      env: options.env,
     });
     });
     const exitCode = await proc.exited;
     const exitCode = await proc.exited;
 
 

+ 2 - 2
src/multiplexer/types.ts

@@ -17,10 +17,10 @@ export interface PaneResult {
 /**
 /**
  * Core multiplexer interface
  * Core multiplexer interface
  * Implementations: TmuxMultiplexer, ZellijMultiplexer, HerdrMultiplexer,
  * Implementations: TmuxMultiplexer, ZellijMultiplexer, HerdrMultiplexer,
- * CmuxMultiplexer
+ * CmuxMultiplexer, KittyMultiplexer
  */
  */
 export interface Multiplexer {
 export interface Multiplexer {
-  readonly type: 'tmux' | 'zellij' | 'herdr' | 'cmux';
+  readonly type: 'tmux' | 'zellij' | 'herdr' | 'cmux' | 'kitty';
 
 
   /**
   /**
    * Check if the multiplexer binary is available on the system
    * Check if the multiplexer binary is available on the system