📌 Updated 2026-05-25: a lighter phased approach (presence-only first cut, full ACP translation as follow-up) is proposed in this comment. The body below documents the original full-Plan-C analysis for reference.
Agent-pane Claude (and Codex) sessions never trigger wt-agent-hooks — session registry stays uninitialized
Summary
When the agent pane CLI is set to Claude (or Codex), wt-agent-hooks events never fire for those sessions. The SessionRegistry therefore receives no SessionStarted / ToolStarting / ToolCompleted updates, and downstream UI surfaces (tab status badge, F2 list) miss state changes that work correctly for agent-pane Copilot / Gemini.
For Copilot and Gemini in agent pane, hooks fire normally and everything works. The root cause is structural and specific to adapter-based CLIs.
Reproduction
- Open an agent pane with Claude as the CLI.
- Send a prompt; let it run a tool call.
- Observe:
- A row is appended to
%LOCALAPPDATA%\IntelligentTerminal\agent-pane-sessions.jsonl (good — the wta-side bookkeeping records it).
- No corresponding line appears in
%LOCALAPPDATA%\IntelligentTerminal\logs\hook-trace.log for that session id (no ENTER, no OK, no ERROR).
- Switch to a normal tab, run
claude directly, send a prompt: hook-trace.log now records ENTER + OK lines as expected.
Concrete example from my machine (today):
agent-pane-sessions.jsonl (today):
04bf17d7-55ed-4f50-b26e-3a98021a2899
ab05fa1c-bb10-4838-a625-49a5a2b7351e
hook-trace.log (today, cli=claude entries):
sessId=27152147 ← the *tab* Claude, not agent-pane
The two agent-pane session IDs do not appear in hook-trace.log at all. Same machine, same install, same wt-agent-hooks plugin — works in tab, never fires in agent pane.
Root cause
Agent-pane Claude does not run the real claude CLI binary. The agent registry profile (tools/wta/src/agent_registry.rs:95-112) sets:
AgentProfile {
id: "claude",
...
acp_launch_command: "npx -y @zed-industries/claude-code-acp",
...
}
with the explanatory comment:
"Claude CLI itself doesn't speak ACP. We launch the Zed-maintained adapter via npx; npm-installed claude shim implies node/npx are present, so this works whenever delegate mode does."
So in agent pane we run:
wta.exe --owner-tab-id "{...}" --agent "npx -y @zed-industries/claude-code-acp"
└─ cmd /c npx.cmd -y @zed-industries/claude-code-acp
└─ node @zed-industries/claude-code-acp/dist/index.js ← the "Claude" agent pane talks to
└─ node @anthropic-ai/claude-agent-sdk/cli.js ← driven via SDK, NOT Claude Code CLI
The @anthropic-ai/claude-code binary (the one that loads ~/.claude/plugins/...) never runs.
Inspecting the Zed adapter source (@zed-industries/claude-code-acp v0.16.2):
- It does call the Claude Agent SDK with
settingSources: ["user", "project", "local"] (dist/acp-agent.js:777), so a user putting hook definitions directly into ~/.claude/settings.json would have them honored.
- It does not pass
plugins: to the SDK (grep -i 'plugins\s*:' dist/ returns no matches). The SDK's plugin-loading machinery (@anthropic-ai/claude-agent-sdk/sdk.d.ts:721-734) is only invoked when the host explicitly passes a SdkPluginConfig[] — there is no auto-discovery of ~/.claude/plugins/installed_plugins.json.
Result: wt-agent-hooks is installed via Claude's plugin marketplace mechanism, so the adapter never loads it. Codex has the same architecture (agent_registry.rs:113-128: acp_launch_command: "npx -y @zed-industries/codex-acp") and presumably the same problem.
Why this matters
WTA has two parallel paths that populate SessionRegistry:
- Hook path:
wt-agent-hooks → PowerShell → wtcli send-event → app.rs::handle_send_event (lines 370-440) → reg.apply(SessionEvent::*).
- ACP path: agent process →
session/update notification → client.rs::session_notification (line 1491) → AppEvent::ToolCall → consumed in app.rs:2924-2954 to update agent-pane UI state only — SessionRegistry is never touched.
So for agent-pane Claude/Codex, the registry receives zero events from either path. UI surfaces that read from the registry (tab status badge, F2 list, status persistence) display incorrect or missing state.
Proposed fix — Plan C: synthesize hook events from ACP traffic in wta
Translate ACP session_notification and new_session events into SessionEvent variants and feed them into SessionRegistry via a new AppEvent::SyntheticSessionEvent(SessionEvent). Treat ACP as a third ingress path equivalent to the hook path.
Why not just fix the hook side
Two simpler alternatives were considered and rejected:
- Plan A — install hooks via
~/.claude/settings.json instead of plugin: Works for Claude only (adapter does honor settingSources), doesn't help Codex, conflicts with users' own settings.json edits.
- Plan B — have wta push hooks to the adapter via ACP
_meta.claudeCode.options.hooks: Adapter-specific, doesn't generalize to other CLIs that might use different adapter patterns in the future.
Plan C generalizes to any ACP agent (current adapter-based ones + any future ones) without depending on third-party adapter implementation details.
Scope (this issue)
- Claude (agent pane) only. Codex is structurally the same but
CliSource enum (agent_sessions.rs:34-40) doesn't have a Codex variant; adding it is a separate ripple-change ticket.
- Native ACP agents (Copilot, Gemini) get
is_agent_pane && is_adapter_based gate set to false → no change for them; their existing hook path keeps working as-is.
Key design points (validated against the code)
-
Adapter detection must use canonical agent_id, NOT agent_cmd string parsing. WT passes --agent-id (TerminalPage.cpp:1829-1840) and main.rs reads it (main.rs:1671-1684), but it isn't currently threaded into run_acp_client (main.rs:1626-1635). After resolve_agent_cmd (app.rs:5925-5940) the cmd string can be a fully-resolved absolute path that fails resolve_agent_id_from_cmd's prefix matcher (agent_registry.rs:216-232). Thread the id explicitly.
-
Per-session in-flight HashSet<tool_call_id> to avoid premature Idle. SessionEvent::ToolStarting/ToolCompleted are tool-id-less (agent_sessions.rs:123-124), but ACP runs concurrent tool calls each with its own id. Emit registry ToolStarting only on 0→1, ToolCompleted only on 1→0. Otherwise a completion for tool A flips the session Idle while tool B is still running.
-
SessionOrigin::AgentPane stamping on the app-side handler. The hook path stamps origin in app.rs:449-460. A raw reg.apply(ev) leaves origin as Unknown, breaking the SessionStopped(reason="complete") lifecycle at agent_sessions.rs:373-390. Add an apply_with_agent_pane_origin helper that mirrors the hook path.
-
Pane GUID from wta's own WT_SESSION env. wta runs as the agent pane's process; ConPTY sets WT_SESSION for every child (ConptyConnection.cpp:56-62; matches TerminalPage.cpp:4794-4809). Read once in main.rs, thread through to ACP runner.
-
Key equality verification first, dedup later. Hook path keys by agent_session_id from hook JSON; synthetic path keys by ACP session_id. If they differ for the same logical Claude session, hook + synthetic events create competing rows for the same pane (and drop_synthetic_for_pane at agent_sessions.rs:589-600 only cleans pane:-prefixed keys). Add tracing::info! for one-shot manual verification before deciding whether dedup is needed.
Files
| File |
Change |
tools/wta/src/main.rs |
thread WT_SESSION + canonical agent_id into ACP runner |
tools/wta/src/protocol/acp/client.rs |
new params; per-session in-flight set; emit synthetic events in session_notification + after new_session |
tools/wta/src/app.rs |
new AppEvent::SyntheticSessionEvent variant + apply_with_agent_pane_origin helper |
tools/wta/src/agent_registry.rs |
pub fn is_adapter_based(profile_id: &str) -> bool helper |
Tests in client.rs |
gate + in-flight transitions |
Rough size: ~120-180 lines net. No public API changes.
Verification plan
- Build, repro: badge stays wrong for agent-pane Claude.
- Apply fix, retry: badge transitions Idle → Working → Idle.
- Cross-check no regression: agent-pane Copilot/Gemini still work (no double events).
- Cross-check no regression: tab Claude still works (hook path still updates registry, synthetic events not emitted).
- Inspect
wta-main.log for acp_synthetic_hook trace entries during the test.
Follow-up tickets (not this issue)
- Add
CliSource::Codex to agent_sessions.rs:34-40 + cascade through parse/filter/history/resume so Codex agent-pane gets the same coverage.
- If key-equality verification (point 5) shows ACP id != hook id for Claude, implement dedup (extend
drop_synthetic_for_pane or maintain an alias map).
Agent-pane Claude (and Codex) sessions never trigger
wt-agent-hooks— session registry stays uninitializedSummary
When the agent pane CLI is set to Claude (or Codex),
wt-agent-hooksevents never fire for those sessions. TheSessionRegistrytherefore receives noSessionStarted/ToolStarting/ToolCompletedupdates, and downstream UI surfaces (tab status badge, F2 list) miss state changes that work correctly for agent-pane Copilot / Gemini.For Copilot and Gemini in agent pane, hooks fire normally and everything works. The root cause is structural and specific to adapter-based CLIs.
Reproduction
%LOCALAPPDATA%\IntelligentTerminal\agent-pane-sessions.jsonl(good — the wta-side bookkeeping records it).%LOCALAPPDATA%\IntelligentTerminal\logs\hook-trace.logfor that session id (noENTER, noOK, noERROR).claudedirectly, send a prompt:hook-trace.lognow recordsENTER+OKlines as expected.Concrete example from my machine (today):
The two agent-pane session IDs do not appear in
hook-trace.logat all. Same machine, same install, samewt-agent-hooksplugin — works in tab, never fires in agent pane.Root cause
Agent-pane Claude does not run the real
claudeCLI binary. The agent registry profile (tools/wta/src/agent_registry.rs:95-112) sets:with the explanatory comment:
So in agent pane we run:
The
@anthropic-ai/claude-codebinary (the one that loads~/.claude/plugins/...) never runs.Inspecting the Zed adapter source (
@zed-industries/claude-code-acpv0.16.2):settingSources: ["user", "project", "local"](dist/acp-agent.js:777), so a user putting hook definitions directly into~/.claude/settings.jsonwould have them honored.plugins:to the SDK (grep -i 'plugins\s*:' dist/returns no matches). The SDK's plugin-loading machinery (@anthropic-ai/claude-agent-sdk/sdk.d.ts:721-734) is only invoked when the host explicitly passes aSdkPluginConfig[]— there is no auto-discovery of~/.claude/plugins/installed_plugins.json.Result:
wt-agent-hooksis installed via Claude's plugin marketplace mechanism, so the adapter never loads it. Codex has the same architecture (agent_registry.rs:113-128:acp_launch_command: "npx -y @zed-industries/codex-acp") and presumably the same problem.Why this matters
WTA has two parallel paths that populate
SessionRegistry:wt-agent-hooks→ PowerShell →wtcli send-event→app.rs::handle_send_event(lines 370-440) →reg.apply(SessionEvent::*).session/updatenotification →client.rs::session_notification(line 1491) →AppEvent::ToolCall→ consumed inapp.rs:2924-2954to update agent-pane UI state only —SessionRegistryis never touched.So for agent-pane Claude/Codex, the registry receives zero events from either path. UI surfaces that read from the registry (tab status badge, F2 list, status persistence) display incorrect or missing state.
Proposed fix — Plan C: synthesize hook events from ACP traffic in wta
Translate ACP
session_notificationandnew_sessionevents intoSessionEventvariants and feed them intoSessionRegistryvia a newAppEvent::SyntheticSessionEvent(SessionEvent). Treat ACP as a third ingress path equivalent to the hook path.Why not just fix the hook side
Two simpler alternatives were considered and rejected:
~/.claude/settings.jsoninstead of plugin: Works for Claude only (adapter does honorsettingSources), doesn't help Codex, conflicts with users' own settings.json edits._meta.claudeCode.options.hooks: Adapter-specific, doesn't generalize to other CLIs that might use different adapter patterns in the future.Plan C generalizes to any ACP agent (current adapter-based ones + any future ones) without depending on third-party adapter implementation details.
Scope (this issue)
CliSourceenum (agent_sessions.rs:34-40) doesn't have aCodexvariant; adding it is a separate ripple-change ticket.is_agent_pane && is_adapter_basedgate set to false → no change for them; their existing hook path keeps working as-is.Key design points (validated against the code)
Adapter detection must use canonical
agent_id, NOTagent_cmdstring parsing. WT passes--agent-id(TerminalPage.cpp:1829-1840) and main.rs reads it (main.rs:1671-1684), but it isn't currently threaded intorun_acp_client(main.rs:1626-1635). Afterresolve_agent_cmd(app.rs:5925-5940) the cmd string can be a fully-resolved absolute path that failsresolve_agent_id_from_cmd's prefix matcher (agent_registry.rs:216-232). Thread the id explicitly.Per-session in-flight
HashSet<tool_call_id>to avoid premature Idle.SessionEvent::ToolStarting/ToolCompletedare tool-id-less (agent_sessions.rs:123-124), but ACP runs concurrent tool calls each with its own id. Emit registryToolStartingonly on0→1,ToolCompletedonly on1→0. Otherwise a completion for tool A flips the session Idle while tool B is still running.SessionOrigin::AgentPanestamping on the app-side handler. The hook path stamps origin inapp.rs:449-460. A rawreg.apply(ev)leaves origin asUnknown, breaking theSessionStopped(reason="complete")lifecycle atagent_sessions.rs:373-390. Add anapply_with_agent_pane_originhelper that mirrors the hook path.Pane GUID from wta's own
WT_SESSIONenv. wta runs as the agent pane's process; ConPTY setsWT_SESSIONfor every child (ConptyConnection.cpp:56-62; matchesTerminalPage.cpp:4794-4809). Read once inmain.rs, thread through to ACP runner.Key equality verification first, dedup later. Hook path keys by
agent_session_idfrom hook JSON; synthetic path keys by ACPsession_id. If they differ for the same logical Claude session, hook + synthetic events create competing rows for the same pane (anddrop_synthetic_for_paneatagent_sessions.rs:589-600only cleanspane:-prefixed keys). Addtracing::info!for one-shot manual verification before deciding whether dedup is needed.Files
tools/wta/src/main.rsWT_SESSION+ canonicalagent_idinto ACP runnertools/wta/src/protocol/acp/client.rssession_notification+ afternew_sessiontools/wta/src/app.rsAppEvent::SyntheticSessionEventvariant +apply_with_agent_pane_originhelpertools/wta/src/agent_registry.rspub fn is_adapter_based(profile_id: &str) -> boolhelperclient.rsRough size: ~120-180 lines net. No public API changes.
Verification plan
wta-main.logforacp_synthetic_hooktrace entries during the test.Follow-up tickets (not this issue)
CliSource::Codextoagent_sessions.rs:34-40+ cascade through parse/filter/history/resume so Codex agent-pane gets the same coverage.drop_synthetic_for_paneor maintain an alias map).