Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
652b92b
feat(wta): promote Codex to first-class CliSource variant
May 28, 2026
70b5dff
feat(wta): enable Codex resume via 'codex resume <id>' subcommand
May 28, 2026
34491e3
feat(wta): scan ~/.codex/sessions for historical rollouts
May 28, 2026
86a040a
feat(wta): wire codex_title_for_key into lookup_title_for_session
May 28, 2026
111401b
feat(wta): codex lenient + strict resumability probes
May 28, 2026
c160795
refactor(wta): centralize CliSource -> cli_id via known_cli_id helper
May 28, 2026
76494cb
test(wta): pin codex wire-format round-trip through SessionHookCliSource
May 28, 2026
de1896e
test(wta): pin codex live-registry fan-in contract
May 28, 2026
cae8d74
test(wta): pin Codex Class A behavior in session_mgmt cells
May 28, 2026
5d2ed55
feat(wta): show 'codex' suffix on selected Codex rows in F2 list
May 28, 2026
61daca1
feat(wta): include Codex in display-name mapping
May 28, 2026
f3d37db
test(wta): add Codex row to populate_demo_data
May 28, 2026
cce37a3
docs(wta): add Codex layout to history_loader header
May 28, 2026
3572714
Merge remote-tracking branch 'origin/main' into dev/yuazha/codex-session
May 29, 2026
5532ca9
docs: add Codex hooks slice B design spec
May 29, 2026
4e2e847
docs: add Codex hooks slice B implementation plan
May 29, 2026
5c807d6
feat(wta): add CliKind::Codex variant
May 29, 2026
0695212
feat(wta): add Codex hooks bundle (marketplace + plugin + 4 hook events)
May 29, 2026
1ec076f
feat(wta): add install_for_codex + dispatch wiring + stub helpers
May 29, 2026
461fe8f
feat(wta): parse codex plugin/marketplace list text output
May 29, 2026
830b291
feat(wta): codex_status with CLI + filesystem fallback
May 29, 2026
5210d43
feat(wta): uninstall_for_codex + uninstall dispatch arm
May 29, 2026
0128096
test(wta): bundle resolver finds codex/ in dev tree
May 29, 2026
637dad1
docs(cascadia): mention codex in CliStatus.name comment
May 29, 2026
de8c93e
feat(settings): add Codex row + RemoveCodexHooks to AI Agents page
May 29, 2026
fd4069e
Merge remote-tracking branch 'origin/main' into dev/yuazha/codex-session
May 30, 2026
3f0021b
Merge remote-tracking branch 'origin/main' into dev/yeelam/pr-98-code…
yeelam-gordon May 31, 2026
43d7191
fix(wta): address PR #98 review comments
May 31, 2026
7dd2830
fix(codex-hooks): Settings remove + unify send-event.ps1
May 31, 2026
37e49dc
l10n(codex-hooks): translate AIAgents_HooksRemovingCodexSummary
May 31, 2026
f3d05f6
feat(codex-hooks): auto-upgrade plugin on IT version change
May 31, 2026
6620605
fix(codex-hooks): add Verify-AgentHooks codex support, send-event env…
May 31, 2026
315dbc8
Flatten Codex bundle layout to match Claude/Copilot/Gemini
May 31, 2026
9454563
Restore UTF-8 BOM on translated Resources.resw files
May 31, 2026
2ed5dfd
Merge remote-tracking branch 'origin/main' into dev/yuazha/codex-session
May 31, 2026
fc914d3
update version to 0.1.2 for wt-agent-hooks across all plugins and scr…
Jun 1, 2026
a1bdd19
wta(codex): scope plugin list to wt-local marketplace
Jun 1, 2026
5842412
l10n(codex-hooks): include Codex in translator comments for HooksInst…
Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,313 changes: 1,313 additions & 0 deletions docs/superpowers/plans/2026-05-29-codex-hooks-slice-b.md

Large diffs are not rendered by default.

223 changes: 223 additions & 0 deletions docs/superpowers/specs/2026-05-29-codex-hooks-slice-b-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Codex hooks — slice B design

**Date:** 2026-05-29
**Branch:** `dev/yuazha/codex-session`

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error documentation

yuazha is not a recognized word
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
**Builds on:** slice A (read-only Codex session discovery, merged into PR #98)
**Successor:** slice C (l10n, docs, ut_app fixtures, ACP `loadSession`)

## Goal

Extend `wta hooks {install,status,uninstall}` to register a **wt-agent-hooks** plugin with the Codex CLI so live Codex hook events flow into the Windows Terminal session-management UI (Ctrl+Shift+/), bringing Codex to parity with Copilot / Claude / Gemini for hook plumbing. Add a fourth "Codex CLI" row to the **AI Agents** settings page so users can install / remove hooks from the GUI.

## Non-goals (slice C)

- l10n (.resw) strings for "Codex CLI" display name / status messages
- README.md / CLAUDE.md doc updates mentioning Codex
- `agent_check::install()` adding `winget install OpenAI.Codex`
- ACP `loadSession` support for Shift+Enter in-pane resume
- C++ `ut_app/AgentHooksStatusTests.cpp` fixture rows for Codex
- `STATUS_SCHEMA_VERSION` bump (would require coordinated C++ change)
- Trust-status field in `CliStatus` (Codex requires interactive `/hooks` trust step; surfaced via README in slice C)

## Architecture

### Rust: `tools/wta/src/agent_hooks_installer.rs`

| Existing symbol | Change |
| --- | --- |
| `CliKind` enum | Add `Codex` variant |
| `CliKind::ALL` | Append `CliKind::Codex` |
| `CliKind::name()` | `Codex => "codex"` |
| `CliKind::from_name()` | `"codex" => Codex` |
| `CliKind::dir_name()` | `Codex => "codex"` |
| `install_for_*` dispatch | New `install_for_codex` (mirror of `install_for_claude`) |
| `status_for_*` dispatch | New `status_for_codex` |
| `uninstall_for_*` dispatch | New `uninstall_for_codex` |
| `maybe_stage_bundle_for_claude` | Sibling `maybe_stage_bundle_for_codex` (or parameterize on `CliKind`) |
| `STATUS_SCHEMA_VERSION` | **unchanged (3)** — Codex row uses existing `CliStatus` shape |
| `PLUGIN_NAME` / `MARKETPLACE_NAME` | reused (`wt-agent-hooks` / `wt-local`) |

#### `install_for_codex(home, opts)`

1. Skip cleanly when `~/.codex/` is absent (CLI never used on this machine).
2. Resolve bundle dir via `bundle::resolve_cli_dir(CliKind::Codex)` — same lookup chain (env var → exe-sibling → dev-tree).
3. If the bundle resolves under `WindowsApps`, restage to `LOCALAPPDATA\Microsoft\IntelligentTerminal\hook-bundle-staging\codex` (mirror existing Claude workaround in case `codex plugin add` Rust-side copy has any similar issue; harmless if not).
4. Spawn `codex plugin marketplace add <bundle_path>` — registers the `wt-local` marketplace.
5. Spawn `codex plugin add wt-agent-hooks@wt-local` — installs the plugin into Codex's config.

Both commands run with stdin closed and 30-second timeout. Output captured to tracing logs at `target: "agent_hooks"`.

#### `status_for_codex(home)`

**Primary path** (CLI on PATH):
- `codex plugin marketplace list` → text-parse columns `MARKETPLACE ROOT`, look for row whose name == `wt-local`. Set `marketplace_registered` and `marketplace_path` from the `ROOT` column. Compute `marketplace_path_valid` by stat'ing the path (`directory` exists check, same logic as Claude/Copilot).

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error documentation

stat'ing is not a recognized word
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
- `codex plugin list` → text-parse for a row whose `PLUGIN` column == `wt-agent-hooks`. Set `plugin_installed`. Codex has no enable/disable distinction in `plugin list` output → `plugin_enabled := plugin_installed`.

**Filesystem fallback** (CLI not on PATH or commands fail):
- Read `~/.codex/config.toml`. Parse TOML for plugin/marketplace entries (exact key names verified during plan-task probe). Set `detection_fallback = "fs"` on the returned `CliStatus`.

`binary_on_path` / `binary_path` come from the standard `which`-lookup helper that the other CLIs use.

#### `uninstall_for_codex(home)`

1. `codex plugin remove wt-agent-hooks@wt-local` — remove plugin first.
2. `codex plugin marketplace remove wt-local` — then remove the marketplace registration.
3. Best-effort cleanup of any LOCALAPPDATA staging dir created by step 3 of install.
4. Populate `CliUninstallResult.messages` with command outcomes.

### Bundle: `tools/wta/wt-agent-hooks/codex/`

```
codex/
.agents/plugins/marketplace.json ← per developers.openai.com/codex/plugins/build
plugins/wt-agent-hooks/
.codex-plugin/plugin.json ← Codex plugin manifest
hooks/hooks.json ← 4 events
hooks/send-event.ps1 ← byte-identical copy of claude/wt-agent-hooks/hooks/send-event.ps1
```

**`marketplace.json`** (required Codex schema):

```json
{
"$schema": "https://developers.openai.com/codex/marketplace.schema.json",
"name": "wt-local",
"displayName": "Windows Terminal (local)",
"plugins": {
"wt-agent-hooks": {
"source": { "source": "local", "path": "./plugins/wt-agent-hooks" },
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
}
}
```

(Exact key names — including whether `plugins` is an object map or array, and the precise shape of `source` / `policy` — will be cross-checked against the live `~/.codex/.tmp/plugins/openai-curated` reference during the bundle-creation task. The shape above is the working hypothesis from the discovery probe; the task will lock it in and add a unit test that round-trips through `codex plugin marketplace add`.)

**`plugin.json`** (minimal Codex plugin manifest):

```json
{
"$schema": "https://developers.openai.com/codex/plugin.schema.json",
"name": "wt-agent-hooks",
"version": "0.1.0",
"displayName": "WT Agent Hooks",
"description": "Forward Codex hook events to Windows Terminal for session-management UI."
}
```

**`hooks.json`** (4 events; Codex env var `${PLUGIN_ROOT}`):

```json
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{ "type": "command",
"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\hooks\\send-event.ps1\" -CliSource codex -EventName SessionStart" }
]
}
],
"PermissionRequest": [
{ "hooks": [
{ "type": "command",
"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\hooks\\send-event.ps1\" -CliSource codex -EventName PermissionRequest" }
] }
],
"UserPromptSubmit": [
{ "hooks": [
{ "type": "command",
"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\hooks\\send-event.ps1\" -CliSource codex -EventName UserPromptSubmit" }
] }
],
"Stop": [
{ "hooks": [
{ "type": "command",
"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\hooks\\send-event.ps1\" -CliSource codex -EventName Stop" }
] }
]
}
}
```

The stable `powershell ... -File <fixed path>` wrapper means trust-on-hash survives `send-event.ps1` content updates — same trick used for Claude.

**`send-event.ps1`** — byte-for-byte copy of `claude/wt-agent-hooks/hooks/send-event.ps1`. The script accepts `-CliSource codex` from the installer-baked command line, so its env-var fallback chain (which knows about `CLAUDE_SESSION_ID`/`COPILOT_SESSION_ID`/`GEMINI_SESSION_ID` but not `CODEX_SESSION_ID`) never fires in this code path. Slice C may add `CODEX_SESSION_ID` to that chain for defensive correctness.

### C++ / XAML side

`src/cascadia/TerminalSettingsEditor/`:

| File | Change |
| --- | --- |
| `AIAgentsViewModel.idl` | Add `CodexHooksSubtitle`, `ShowCodexHooksSubtitle`, `RemoveCodexHooks` |
| `AIAgentsViewModel.h` | Add `_codexHooksSubtitle` field; mirror three Claude getter methods |
| `AIAgentsViewModel.cpp` | Populate `_codexHooksSubtitle` from the `clis[]` entry whose `name == "codex"`; add `RemoveCodexHooks` body; add `L"CodexHooksSubtitle"` + `L"ShowCodexHooksSubtitle"` to the property-change broadcast list (lines 905–910) |
| `AIAgents.xaml` | New `<Grid>` row for "Codex CLI" — ~30 lines mirroring the Gemini block at lines ~327–346 |

`src/cascadia/inc/AgentHooksStatus.h`:

- Line 42 doc comment: extend `"copilot" \| "claude" \| "gemini"` → `"copilot" \| "claude" \| "gemini" \| "codex"`.

**No changes to:**
- `AgentHooksStatus.h` parser logic — already CLI-name-agnostic.
- `ut_app/AgentHooksStatusTests.cpp` — fixture rows are illustrative only; parser test coverage stays equivalent.
- `CascadiaPackage.wapproj` — content glob `tools\wta\wt-agent-hooks\**` auto-picks up new `codex/` subtree.

## Data flow

```
Codex CLI runs hook → powershell -File send-event.ps1 -CliSource codex -EventName ...
→ send-event.ps1 POSTs to wta IPC endpoint
→ wta receives event, tags with cli=codex, persists to history_loader
→ Settings UI / Ctrl+Shift+/ list pick up via StatusReport / session enumeration (slice A)
```

## Error handling

- `~/.codex/` missing → `install_for_codex` returns `Skipped` with reason. Status reports `binary_on_path: false`, all plugin fields `false`.
- `codex` not on PATH → status falls back to filesystem (`detection_fallback = "fs"`); install errors with clear log.
- `codex plugin add` non-zero exit → captured to log + `messages`; partial state surfaced via existing `marketplace_registered: true, plugin_installed: false` ("partially installed") C++ formatter.
- Trust step (user must run `/hooks`) is **outside** wta's control — surfaced via slice-C README; slice B's `install` returns success on registration even though events won't fire until trusted.

## Testing strategy

### Rust unit tests (`agent_hooks_installer.rs` tests module)

1. `CliKind::Codex` round-trips: `from_name("codex")` and `Codex.name() == "codex"`; appears in `CliKind::ALL`.
2. `bundle::resolve_cli_dir(CliKind::Codex)` finds `codex/` via env-var override / exe-sibling / dev-tree fixtures.
3. `install_for_codex` skips cleanly when `~/.codex/` missing.
4. `install_for_codex` invokes the two expected commands in order when `~/.codex/` present (mock executor verifies args).
5. `parse_codex_marketplace_list` extracts the `wt-local` row from a golden text sample.
6. `parse_codex_plugin_list` extracts the `wt-agent-hooks` row from a golden text sample.
7. Filesystem fallback parses a fixture `config.toml`.
8. `uninstall_for_codex` issues `plugin remove` then `marketplace remove` in order.
9. Existing parameterized tests (e.g. `installer_skips_when_home_missing`) get a `CliKind::Codex` arm if they iterate over `CliKind::ALL`.

### Build verification

- `cargo test --manifest-path tools/wta/Cargo.toml` — expect 585 (current) → ~605+ passing.
- Visual Studio build of `CascadiaPackage` solution — verifies XAML / IDL compile and the new ViewModel members link.

### Manual smoke test (documented in PR body, not gated)

1. Install Codex CLI (`winget install OpenAI.Codex` or download).
2. `wta hooks install` → expect successful Codex registration in command output.
3. Open Codex, run `/hooks`, trust the **wt-agent-hooks** plugin.
4. Start a session, submit a prompt → confirm the session appears live in Ctrl+Shift+/ with cli=codex.
5. Open **Settings → AI Agents** → verify the new "Codex CLI" row shows "hooks installed".
6. Click **Remove hooks** → verify `Codex CLI — hooks not installed`.
7. `wta hooks uninstall` → idempotent cleanup.

## Open verification items (resolved during task execution, not blocking design)

- Exact `marketplace.json` / `plugin.json` key spelling against live `~/.codex/.tmp/plugins/openai-curated` (probe in the bundle-creation task).
- `codex plugin add` non-interactive behavior under WindowsApps subtree (verified during install task; staging fallback already accounts for it).
- Empirical `Stop` event reliability (verified during manual smoke; if poor, fallback fix is slice C).
- TOML key names for `[[plugin.marketplaces]]` in `~/.codex/config.toml` (verified during filesystem-fallback task).
24 changes: 24 additions & 0 deletions src/cascadia/TerminalSettingsEditor/AIAgents.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,30 @@
MinWidth="120" />
</Grid>
</Border>
<Border Visibility="{x:Bind ViewModel.ShowCodexHookRow, Mode=OneWay}"
BorderThickness="0,1,0,0"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Padding="0,16,0,16">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="2">
<TextBlock Text="Codex CLI" TextWrapping="Wrap" />
<TextBlock Text="{x:Bind ViewModel.CodexHooksSubtitle, Mode=OneWay}"
Visibility="{x:Bind ViewModel.ShowCodexHooksSubtitle, Mode=OneWay}"
Style="{StaticResource CaptionTextBlockStyle}"
Opacity="0.6"
TextWrapping="Wrap" />
</StackPanel>
<Button Grid.Column="1"
x:Uid="AIAgents_HooksRemoveButton"
Click="{x:Bind ViewModel.RemoveCodexHooks}"
IsEnabled="{x:Bind ViewModel.CanInstallAgentHooks, Mode=OneWay}"
MinWidth="120" />
</Grid>
</Border>
</StackPanel>
</local:SettingContainer>

Expand Down
22 changes: 21 additions & 1 deletion src/cascadia/TerminalSettingsEditor/AIAgentsViewModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -865,49 +865,60 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_copilotCliDetected = false;
_claudeCliDetected = false;
_geminiCliDetected = false;
_codexCliDetected = false;
_showCopilotHookRow = false;
_showClaudeHookRow = false;
_showGeminiHookRow = false;
_showCodexHookRow = false;
_copilotHooksSubtitle = {};
_claudeHooksSubtitle = {};
_geminiHooksSubtitle = {};
_codexHooksSubtitle = {};
}
else
{
const auto* copilot = FindCli(*report, "copilot");
const auto* claude = FindCli(*report, "claude");
const auto* gemini = FindCli(*report, "gemini");
const auto* codex = FindCli(*report, "codex");

_copilotCliDetected = copilot && copilot->binaryOnPath;
_claudeCliDetected = claude && claude->binaryOnPath;
_geminiCliDetected = gemini && gemini->binaryOnPath;
_codexCliDetected = codex && codex->binaryOnPath;

const auto hasState = [](const CliStatus* cli) {
return cli && (cli->marketplaceRegistered || cli->pluginInstalled);
};
_showCopilotHookRow = hasState(copilot);
_showClaudeHookRow = hasState(claude);
_showGeminiHookRow = hasState(gemini);
_showCodexHookRow = hasState(codex);

_copilotHooksSubtitle = _ComputeHooksSubtitle(copilot);
_claudeHooksSubtitle = _ComputeHooksSubtitle(claude);
_geminiHooksSubtitle = _ComputeHooksSubtitle(gemini);
_codexHooksSubtitle = _ComputeHooksSubtitle(codex);
}

_NotifyChanges(L"IsCopilotCliDetected",
L"IsClaudeCliDetected",
L"IsGeminiCliDetected",
L"IsCodexCliDetected",
L"IsAnyAgentCliDetected",
L"CanInstallAgentHooks",
L"ShowCopilotHookRow",
L"ShowClaudeHookRow",
L"ShowGeminiHookRow",
L"ShowCodexHookRow",
L"CopilotHooksSubtitle",
L"ClaudeHooksSubtitle",
L"GeminiHooksSubtitle",
L"CodexHooksSubtitle",
L"ShowCopilotHooksSubtitle",
L"ShowClaudeHooksSubtitle",
L"ShowGeminiHooksSubtitle");
L"ShowGeminiHooksSubtitle",
L"ShowCodexHooksSubtitle");
}

void AIAgentsViewModel::RefreshAgentHooksStatus()
Expand Down Expand Up @@ -973,6 +984,15 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
_RunHooksWtaAsync(L"hooks uninstall --cli gemini");
}

void AIAgentsViewModel::RemoveCodexHooks()
{
if (_installingAgentHooks) return;
_installingAgentHooks = true;
_agentHooksInstallSummary = RS_(L"AIAgents_HooksRemovingCodexSummary");
_NotifyChanges(L"IsInstallingAgentHooks", L"AgentHooksInstallSummary", L"HasAgentHooksInstallSummary");
_RunHooksWtaAsync(L"hooks uninstall --cli codex");
}

winrt::fire_and_forget AIAgentsViewModel::_RunHooksWtaAsync(std::wstring wtaArgs)
{
auto strongThis = get_strong();
Expand Down
Loading
Loading