Skip to content

feat(desktop): runnable Codex-on-agent-kit swap behind PWRAGENT_CODEX_KIT#679

Closed
huntharo wants to merge 685 commits into
mainfrom
claude/codex-kit-swap
Closed

feat(desktop): runnable Codex-on-agent-kit swap behind PWRAGENT_CODEX_KIT#679
huntharo wants to merge 685 commits into
mainfrom
claude/codex-kit-swap

Conversation

@huntharo

@huntharo huntharo commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Draft / WIP tracker — the Phase-B Codex swap: PwrAgent's live Codex thread driven through the kit's CodexThreadClient (@pwrdrvr/agent-client) behind PWRAGENT_CODEX_KIT=1, instead of the in-tree CodexAppServerClient. Flag-off keeps the in-tree client byte-for-byte unchanged (A/B-able).

What runs through the kit now

CodexKitBackendClient (codex-kit-adapter.ts) implements the registry's BackendClient surface over CodexThreadClient; codexEventToNotifications maps the kit's neutral NormalizedThreadEvents → the AppServerNotification shapes the registry + renderer consume live. Gated at one seam (the codexClient construction in backend-registry.ts); downstream wiring untouched.

Depends on: pwrdrvr/agent-kit#7

The ported kit methods aren't published yet, so this branch uses a local bridge: a root pnpm override points @pwrdrvr/agent-client at a tarball built from the agent-kit feature-port branch. CI is red until agent-kit#7 lands, a version is published, the apps/desktop pin is bumped, and the override is dropped. Expected for the draft.

Run it (local bridge in place)

PWRAGENT_CODEX_KIT=1 pnpm --filter @pwragent/desktop dev

Status

Built green locally against the linked kit build: desktop typecheck · build · 6 adapter tests · 230 backend-registry tests. Agent-kit side: 46 tests (6 new).

Not yet validated end-to-end with a live GUI + real Codex — that's the launch test. Relationship to #672: independent; #672 is the parity gate (normalizer fidelity), this is the runtime wiring + feature port.

🤖 Generated with Claude Code

huntharo and others added 30 commits May 14, 2026 22:00
* fix(desktop): auto-close stale context rail

* fix(desktop): shorten context rail stale timeout
* fix(desktop): repair selected worktree directory metadata

* fix(desktop): separate directory backfill cache entries

* test(desktop): allow pre-archive replay refresh
* fix(desktop): require context rail hover intent

* test(desktop): open context rail deterministically

* fix(desktop): lengthen context rail hover intent

* fix(desktop): increase context rail hover dwell
* fix(desktop): submit launchpad prompt after setup failure

* fix(desktop): hide stale setup failure banner

* fix(desktop): refresh launchpad environment changes
* feat(messaging): repost last reply on resume

* fix(messaging): preserve resume replay backend receiver

* fix(messaging): typecheck resume replay receiver call

* feat(messaging): label resumed last reply age

* fix(messaging): preserve resumed reply timestamps

* fix(messaging): choose latest resume replay message
* fix(desktop): run env setup in interactive shell

* fix(desktop): avoid interactive env setup noise

* fix(desktop): keep selected codex environment on running threads

* fix(desktop): strip parent electron env from codex actions
… Codex guide (#377)

* docs: stand up docs.pwragent.ai operator site under docs-site/

Adds a GitHub-Pages-served operator-facing site at docs.pwragent.ai,
sourced from docs-site/ on main. Uses Jekyll's default minima theme —
no custom layouts, no versioning, no Docusaurus / VitePress build.

Content layout follows the principle the user laid out: "What you need
to get started" leads each platform page, defaults are the
recommendation, and each setting documents what it does and why you'd
change it rather than dumping the field list.

Site structure:

- index.md — landing with a platform-at-a-glance table.
- messaging/overview.md — bindings, two-keyed authorization, commands,
  status card, typing, slow-mode/cool-off, attachments, where state
  lives. Each per-platform page assumes this is read.
- messaging/streaming.md — the streaming explainer the user asked for.
  Walks the "I will " → "I will explore the files " → full-sentence
  example, explains why voice readers only hear the first message,
  documents the Telegram supergroup ~20 msg/min and DM ~60 msg/min
  practical budgets, and frames streaming as a small-special-case
  rather than a sensible default.
- messaging/webhook-dangers.md — outbound socket vs. HTTP-callback
  security note. LINE and Mattermost are the only HTTP-callback
  adapters today; everything else has an outbound-socket option.
  Honest about what Cloudflare Tunnel / Tailscale Funnel do and don't
  protect against. The "if you think it's easy, you don't need this
  page" framing is preserved.
- messaging/telegram.md — bot token is the single piece of info you
  need. Pairing-DM flow is the recommended way to populate the
  user allowlist; activity-log discovery is the fallback. Notes the
  no-webhook-needed conclusion from the author's OpenClaw work.
- messaging/discord.md — Gateway WebSocket, Message Content Intent,
  Application ID requirements. Status-card limitations called out.
- messaging/slack.md — Socket Mode setup order matters (enable Socket
  Mode before Event Subscriptions or Slack steers toward the
  Request-URL flow). Minimum scopes documented. One Slack app per
  desktop bot identity.
- messaging/feishu.md — persistent connection by default, scope and
  event subscription list, publish-a-version reminder, group-mention
  invocation path.
- messaging/mattermost.md — HTTP callback platform; Cloudflare Tunnel
  and Tailscale Funnel walkthroughs, HMAC-secret pinning is required
  to survive restarts, slash-commands-in-threads workaround for
  Mattermost v10.x.
- messaging/line.md — webhook-only platform; same tunnel options as
  Mattermost on a different default port (47822); channel secret
  vs. channel access token split.
- CNAME — docs.pwragent.ai.
- _config.yml — Jekyll minima theme.
- docs-site/README.md — orientation for contributors editing the site.

Also splits docs/messaging-platform-integration.md as planned: the old
986-line operator doc becomes a thin pointer noting the operator
content has moved to docs.pwragent.ai/messaging/, with cross-links to
the contributor-facing docs/messaging-architecture.md,
messaging-adapter-contract.md, and messaging-adding-a-provider.md.
The "Chat SDK decision" context survives as the one piece of
contributor framing the doc carries that has nowhere better to live.

apps/desktop/src/main/__tests__/messaging-docs-links.test.ts passes
unchanged — the trimmed file still exists, still links to
messaging-adapter-contract.md, and still contributes to the
top-level docs link union.

README.md and root AGENTS.md updated to surface docs.pwragent.ai as
the operator-setup destination and to point contributors at
docs-site/ for editing the new content.

DNS for docs.pwragent.ai → pwrdrvr.github.io will land out of band.
The site renders to the GitHub-rendered Pages URL until DNS is in
place.

🤖 Generated with Claude Opus 4.7 (200K context, extended thinking) via [Claude Code](https://claude.com/claude-code) + Compound Engineering v2.49.0

Co-Authored-By: Claude Opus 4.7 (200K context, extended thinking) <noreply@anthropic.com>

* docs(docs-site): custom layout + Tangerine Terminal theme

Replaces the default minima theme with custom layouts under _layouts/
and a single stylesheet at assets/css/site.css. Tokens lifted from
docs/UI-THEME.md and apps/desktop/src/renderer/src/styles/app.css —
absolute black canvas, warm-white primary text, neutral-gray
secondary, sparse tangerine accent applied to links on hover, blockquote
left bar, inline code, table-row hover, and text selection (paint-
contained to the content column so multi-block selection doesn't bleed
into the empty rails on either side).

Custom layout matches the desktop's exact wordmark spec (font-weight
700, letter-spacing -0.01em, line-height 1) so the brand reads
identically across the app sidebar, the Settings nav, and docs.pwragent.ai.

Adds a hero wordmark chip on index.md modeled after the DMG install
background — dark rounded panel, chunky wordmark, "threads / transcripts"
tagline beneath.

_config.yml drops `theme: minima` (no longer used) and adds kramdown
syntax-highlighter config for code fences.

* docs(docs-site): add local preview Dockerfile and Gemfile

GitHub Pages serves the site directly from main; this tooling is for
local preview only.

The Gemfile pins the `github-pages` gem (~> 232) so local builds use
the same Jekyll + plugin versions Pages uses in production. `webrick`
is pinned for Ruby 3.x compatibility.

The Dockerfile bakes the site into a self-contained `ruby:3.2-slim`
image that runs `bundle exec jekyll serve` on port 4000. No bind
mount needed (works under restricted Docker Desktop / colima
configurations that don't expose the host filesystem to the VM).

Build + run from the repo root:
  docker build -t pwragent-docs-site:local docs-site/
  docker run --rm -it -p 4000:4000 pwragent-docs-site:local

The .gitignore lists Jekyll's build artifacts (_site/, .jekyll-cache/,
vendor/, Gemfile.lock) so dev iteration doesn't pollute the repo.

* docs(streaming): correct the per-message-edit model

Previous draft framed streaming as "you get one big message edited
many times" — that's wrong. A Codex turn already produces a handful
to a dozen separate bot messages (assistant emissions, tool-progress
summaries, completion). Streaming doesn't change that count; it adds
2-3 edits to each of those messages while they finalize.

Rewrites "What you think it does" and "What it actually does" along
those lines. Reframes the voice-reader beat to "first version of each
message." Drops the "you don't get tool-by-tool visibility" point —
tool-by-tool visibility comes from Tools: <mode> verbosity regardless
of streaming, so it was never streaming's job to provide. Narrows
"when streaming might actually be the right call" to the genuine
single-message head-loaded case. Adds the auto-disable beat: streaming
edits are the least critical traffic, so Slow Mode drops them first
when budgets tighten.

* docs(plans): brainstorm + plan for Codex via Messaging usage guide

Captures the brainstorm-then-plan trail for the next chunk of
docs-site work: a single-page "Using Codex via Messaging" guide
covering 13 sub-topics (the brainstorm's 14, minus Skills Browser
which has no messaging-layer implementation today), a dedicated
/rate-limits/ reference page, top-level nav restructure to
[Using Codex | Providers | Streaming | Webhooks | GitHub], fold of
messaging/overview.md into the new guide, and URL moves for the six
per-provider pages plus streaming and webhook-dangers, all behind
jekyll-redirect-from for the old paths.

Brainstorm: docs/brainstorms/2026-05-13-codex-via-messaging-docs-requirements.md
Plan: docs/plans/2026-05-13-001-docs-codex-via-messaging-guide-plan.md

Plan execution starts immediately on this branch (PR #377).

* docs(docs-site): restructure nav around Using Codex

Phase 1 of the Codex via Messaging guide work
(docs/plans/2026-05-13-001-docs-codex-via-messaging-guide-plan.md).

URL moves
- docs-site/messaging/{telegram,discord,slack,mattermost,feishu,line}.md
  -> docs-site/providers/<platform>.md
- docs-site/messaging/streaming.md -> docs-site/streaming.md
- docs-site/messaging/webhook-dangers.md -> docs-site/webhook-dangers.md
- docs-site/messaging/overview.md is left in place; it gets folded
  into the new Using Codex guide and deleted in Phase 4 of the plan.

Redirects
- Each moved page carries a redirect_from front-matter entry listing
  its old /messaging/<page>/ paths (with and without trailing slash).
- jekyll-redirect-from plugin enabled via _config.yml plugins list
  (the plugin ships in the github-pages gem bundle so no Gemfile
  change is needed).
- Verified locally: every old /messaging/<page>/ URL serves a
  canonical+meta-refresh stub pointing at the new location.

New pages
- docs-site/providers/index.md: lists the six platforms and links to
  each setup page; cross-links to using-codex/ and webhook-dangers/.
  Sits at /providers/.

Nav restructure
- Top nav becomes [Using Codex | Providers | Streaming | Webhooks |
  GitHub]. "Using Codex" points at /using-codex/ which lands in Phase 3
  of the plan; until then it 404s locally but the rest of the nav is
  live.

Internal-link sweep
- All in-page references to overview.md / streaming.md / webhook-
  dangers.md / sibling provider .md files inside the moved pages
  rewritten to absolute pretty URLs (e.g. /streaming/, /providers/
  mattermost/).
- "Messaging concepts overview" link text updated to "Using Codex via
  Messaging" since the destination changed semantically.
- index.md table column header "Page" -> "Setup" to match the new
  framing where /providers/ is setup and /using-codex/ is usage.
- docs-site/README.md updated to reflect the new file layout, the
  Dockerfile-based local preview path, and the Tangerine Terminal
  custom-layout setup (the old "default minima theme" wording was
  stale after the styling commits).

* docs(rate-limits): add dedicated rate-limits reference page

Phase 2 of the Codex via Messaging guide work.

New page at /rate-limits/ with seven anchors (#telegram, #discord,
#slack, #mattermost, #feishu, #line, #pwragent-budget-protection)
intended to be deep-linked from streaming, webhook-dangers, every
provider setup page, and the upcoming using-codex guide. Single
source of truth for per-platform write budgets, edit semantics, and
label-cap reference.

Content:
- Practical budgets per platform from May 2026 PwrAgent team probes
  plus public platform docs.
- PwrAgent's Slow Mode + Cool Off priority order spelled out: what
  Slow Mode drops first (streaming edits), what it coalesces (status
  refreshes), what it batches (tool updates), what it always sends
  (approval prompts, final assistant, turn completion, queue notices).
- Per-platform sections cover send/edit budgets and the label cap,
  flagging Mattermost as the worst label cap (40 chars), Telegram
  supergroups as the tightest send budget (~20/min shared across
  topics), LINE as edit-unsupported, and Feishu as "not yet probed."
- Summary table at the bottom.

streaming.md trims its own per-platform rate-limit table down to a
single paragraph plus a link to /rate-limits/ for the matrix.
streaming keeps the headline beats (~60 DM, ~20 supergroup, LINE
no-edits) inline because they're load-bearing for the streaming
argument.

Feishu heading uses an explicit {#feishu} kramdown ID so the anchor
is /rate-limits/#feishu rather than #feishu--lark (the default
slugify of "Feishu / Lark").

* docs(using-codex): add the operator-facing usage guide

Phase 3 of the Codex via Messaging guide work.

New single-page operator guide at /using-codex/ covering the 12
sub-topics planned for v1 (R1.9 Skills Browser cut — no messaging-
layer implementation exists today, deferred to a follow-up).

In-page TOC at the top groups the sections by lifecycle: Access and
invocation; Starting a thread; During a turn; Ending or rebinding.

Sections (with stable anchors):
- #who-can-talk — closed-by-default + two-keyed authorization model
  + discovery mode. Allowlists live in config.toml, not sqlite.
- #slash-commands-and-buttons — canonical six verbs; per-provider
  registration; Mattermost slash-in-thread workaround + Discord
  applicationId requirement called out in the exceptions block.
- #at-mention-commands — @bot verb pattern; works without slash
  registration; Telegram getMe + Discord applicationId required
  for mention parsing.
- #what-is-a-bound-thread — raw text routes directly into the
  Codex thread; multi-binding semantics.
- #resume-thread-browser — page-size 8 across all providers (the
  brainstorm's "20 for text-only" was unreachable); text-fallback
  + arg parser.
- #new-thread-starter — project picker -> start card flow.
- #start-card-buttons — per-thread model/reasoning/Fast/permissions
  before the first prompt; calls out the per-thread vs. Codex-Desktop-
  global distinction. Includes the permission-mode-queue beat
  (mid-turn toggles queue at turn boundary, do NOT apply immediately
  — per docs/solutions/2026-05-07-codex-permission-mode-state-
  machine.md). Per-provider exceptions table calls out Mattermost's
  40-char label cap and the Feishu/LINE 20-char caps as the worst
  cases — not "Discord-specific" as the brainstorm originally
  suggested.
- #first-prompt — sending the next message completes the bind and
  starts the turn.
- #debounce-queue-steer — 500ms debounce; queued-turn notice with
  Steer/Cancel; FIFO drain on completion. Explicitly distinguishes
  from the permission-mode queue (different concept, different
  Cancel button). LINE-no-edits exception noted.
- #monitor-card — full mechanics: 60s default interval, button row
  (Interval/Pins/Recent/Snippet/Refresh/Stop/Status), one card per
  binding, survives restarts, auto-stops on archive. LINE fresh-card-
  every-tick exception noted.
- #detaching-a-thread — single platform-agnostic detach pipeline
  (interrupt + flush + monitor stop + status retire + revoke +
  confirm). Same path from /detach, status-card button, or right-
  click Unbind.
- #archiving-a-thread — auto-detaches all bindings via the same
  pipeline.

Six <details markdown="1"> blocks for per-provider exceptions.
Kramdown does not re-enter markdown parsing inside HTML blocks by
default, so the markdown="1" attribute is required for the tables
and inline code spans inside each <details> to render correctly.
Without it, literal `<bot>`, `<botusername>`, `<verb>` tokens inside
the description text get interpreted as HTML tags and break the
trailing structure.

redirect_from in the frontmatter folds /messaging/overview/ to
/using-codex/. The old overview.md file still exists in docs-site/
messaging/ and gets removed in Phase 4 of the plan.

* docs(using-codex): fold messaging/overview.md into the guide

Phase 4 of the Codex via Messaging guide work — deletes the legacy
overview.md and absorbs its remaining unique content into using-
codex.md as new lifecycle sub-sections.

Walkthrough of where each overview section landed:

- Bindings -> already in #what-is-a-bound-thread (skip).
- Authorization -> already in #who-can-talk (skip).
- Commands -> already in #slash-commands-and-buttons + #at-mention-
  commands (skip).
- Status card -> new section #status-card-bound covers the bound-
  thread status card (the runtime evolution of the Start Card)
  including all 8 buttons (4 state + Tools, Stream, Refresh, Detach)
  and the cross-surface state contract.
- Tool-update verbosity -> new section #tool-update-verbosity covers
  the Tools: <mode> button on the status card with the full Show
  None / Less / Some / More / All matrix and the
  "flush-before-assistant-output" guarantee.
- Typing indicators -> new section #typing-indicators covers when
  the indicator turns on/off and why intermediate output doesn't
  stop it.
- Streaming responses -> already covered with the dedicated
  /streaming/ page; status card's Stream: <mode> button is referenced
  from #status-card-bound (skip standalone section here).
- Slow Mode and Cool Off -> already in /rate-limits/ (skip).
- Attachments -> new section #attachments covers accepted file types
  (text-like, images, GIFs as stills, PDFs) and rejection rules
  (audio/video, archives, oversized, OCR-only PDFs).
- Where state lives -> already in #what-is-a-bound-thread (skip).
- What's next -> the new TOC at the top of using-codex.md replaces
  this entirely.

TOC at the top of using-codex.md gains a new "While the thread is
bound" group covering the four new sub-sections. The previous
"During a turn" / "Ending or rebinding" groups are unchanged.

The redirect_from: /messaging/overview/ entry in using-codex.md's
front matter (added in Phase 3) routes the old URL to the new page;
jekyll-redirect-from generates a meta-refresh stub at the old path
even though the source file no longer exists. Verified locally.

* docs: point repo-root docs at the new docs.pwragent.ai URLs

Phase 5 of the Codex via Messaging guide work — final-pass repo-root
sweep after the docs-site restructure.

README.md Quick Start step 4 now points operators at the two
relevant top-level URLs:
- /providers/ for per-platform setup
- /using-codex/ for usage (bound threads, commands, queue/steer,
  monitor cards, detach)

The "Going deeper" section's note on docs/messaging-platform-
integration.md updates to reflect that operator content lives at
/using-codex/ + /providers/ now (the previous /messaging/ URL still
redirects via jekyll-redirect-from, but pointing readers at the
canonical path is friendlier).

docs/messaging-platform-integration.md (the trimmed contributor
pointer) gets its "operator content has moved to ..." block
expanded to a six-bullet map covering setup, usage, rate limits,
streaming, and webhook-dangers — each URL now points at its actual
top-level location instead of the now-redirected /messaging/* paths.

messaging-docs-links.test.ts still passes (verified).

* chore(plans): mark Codex via Messaging guide plan completed

All five phases (site restructure, rate-limits page, using-codex
guide, fold overview, final verify) shipped on PR #377 across
commits 754b69d1, 60497adf, 33e75d81, 18be8511, and earlier.

* docs(docs-site): use PwrAgent icon as the favicon

Copies apps/desktop/build/icon.png (512x512 RGBA, the desktop app
icon) into docs-site/assets/favicon.png and wires it into the
default layout's <head>:

  <link rel="icon" type="image/png" href="/assets/favicon.png">
  <link rel="apple-touch-icon" href="/assets/favicon.png">

Browsers downscale the 512px PNG for the 16-32px tab icon; the same
file doubles as the iOS / Android home-screen icon at full
resolution. ~10 KB on the wire, single asset, no duplicates to keep
in sync.

The default Pages favicon was a generic 'L' (presumably the first
character of the title 'localhost' from the local-preview hostname
falling through). With this change tabs show the tangerine PwrAgent
mark on docs.pwragent.ai.

* docs(docs-site): add 'Public port?' column to platform tables

Both the landing page and the /providers/ index now make explicit
which platforms require exposing an HTTP listener to the internet
and which don't:

| Platform | Public port? |
|---|---|
| Telegram | ✅ No (long polling) |
| Discord | ✅ No (Gateway WebSocket) |
| Slack | ✅ No (Socket Mode) |
| Feishu / Lark | ✅ No (persistent SDK WebSocket) |
| Mattermost | ❌ Yes (HTTP callback) |
| LINE | ❌ Yes (HTTP webhook) |

The transport column already implied this, but the inference required
reading 'Inbound transport' and knowing what each option means.
Surfacing it as its own column makes the safer (outbound-socket)
default obvious at a glance and prefigures the
webhook-dangers.md security note for the two ❌ Yes rows.

Each table gets a short lead-in paragraph defining ✅ / ❌ before
the table renders, plus a link to webhook-dangers.md for the
❌ Yes platforms.

* docs(docs-site): serve favicon.ico at root for auto-discovery

Browsers and bots auto-request /favicon.ico regardless of any
<link rel="icon"> tag in the document. With only the explicit
/assets/favicon.png reference, any client that didn't parse the
<head> first (RSS readers, link-preview crawlers, some scrapers, the
browser's pre-render probe before the HTML finishes loading) would
404 looking for the favicon.

docs-site/favicon.ico is a byte-for-byte copy of the existing
/assets/favicon.png. Browsers content-sniff the magic bytes rather
than trusting the extension, so a PNG named .ico renders correctly
across every modern browser. The two paths now both serve the same
PwrAgent mark — explicit <link> for modern clients, root .ico for
legacy auto-discovery.

* docs(docs-site): properly-sized favicon variants for Safari + retina

The previous favicon setup served the desktop app's 512x512 PNG at
both /favicon.ico and /assets/favicon.png. That works on Chrome but
Safari is notoriously picky about favicon sizes and was either
caching the 'no favicon' fallback or refusing to render the
oversized variant.

Generated three properly-sized variants via macOS sips:

  favicon-16.png       1.2 KB  (16x16 — tab favicon on standard DPI)
  favicon-32.png       1.7 KB  (32x32 — tab favicon on retina)
  apple-touch-icon.png 5.9 KB  (180x180 — iOS Home Screen)

/favicon.ico at the repo root is now the 32x32 PNG bytes (browsers
content-sniff regardless of extension; smaller is more favicon-
canonical and reduces transfer for auto-discovery requests).

<link> tags in _layouts/default.html now declare explicit sizes:

  <link rel="icon" type="image/png" sizes="32x32" href=".../favicon-32.png">
  <link rel="icon" type="image/png" sizes="16x16" href=".../favicon-16.png">
  <link rel="apple-touch-icon" sizes="180x180" href=".../apple-touch-icon.png">

Safari prefers explicit-size declarations and was the canary; this
fixes its rendering. Chrome and Firefox already worked and continue
to work (they pick the closest declared size automatically).

The original 512x512 favicon.png is left in place for any client
that requests the full-res version (preview cards, some link
unfurlers); it's no longer the primary <link>.

* docs(docs-site): strip the rounded-square frame from the favicon

The previous favicon was the desktop app icon verbatim — a dark
rounded-square background with the four-bar mark inside, on a
transparent canvas. On any rendering surface with a light background
(Safari light-mode tab bar, Finder list view, etc.), the transparent
canvas corners revealed the surrounding background and read as a
"white box framing the icon." Same root cause as npmjs.com's
favicon; claude.ai avoids it because their orb-shape has no implied
rectangular reference frame around it.

There is no flag that fixes this — browsers don't have a
"this-is-a-dark-icon" attribute that paints a dark background behind
the favicon. The fix is to redesign the favicon so its visible
silhouette doesn't carry a rectangular frame.

New favicon = just the four-bar transcript mark on a transparent
canvas, no dark rounded-square behind it. Authored as SVG at
docs-site/assets/favicon.svg so it scales crisply at any tab size:

  - Bar 1 (top, full tangerine #ff8a1f) — bright accent
  - Bar 2 (rust #c4671d)
  - Bar 3 (mid copper #8a4818)
  - Bar 4 (deep copper #6b3a14) — dim but still in the tangerine
    family, readable on both light and dark tab chrome

Sized PNG fallbacks regenerated from the SVG via macOS qlmanage at
16, 32, 180, and 512px. Root /favicon.ico replaced with the 32px
variant. _layouts/default.html now declares the SVG first so modern
browsers (Chrome 80+, Firefox 41+, Safari 12+, Edge 80+) pick it,
falling back to the size-appropriate PNG for older clients.

This preserves the app-icon framing where it matters — the macOS
.icns bundle, the DMG installer background, the screenshots in the
README — while shipping a web-tab variant designed for arbitrary
tab chrome.

* Revert \"docs(docs-site): strip the rounded-square frame from the favicon\"

The bars-on-transparent variant lost the brand identity at favicon
size — it read as a generic hamburger/menu icon, not as PwrAgent.
Reverting to the framed desktop icon as the favicon source.

Trade-off accepted: Safari light-mode tabs and the macOS Finder
list-view plate will show the icon's transparent rounded-corner
gutters as a 'white box' framing the dark square. That's a known
Safari quirk for any rounded-square favicon (npmjs.com has the same
artifact); the alternative — full square with no rounded corners
— would lose the rounded shape that ties the favicon to the desktop
app icon and the DMG installer. The framed look matters more here
than the perfect tab rendering.

If we want to revisit later, the cleanest fix that keeps brand and
loses the white box is a full-square variant (rounded corners
removed, dark color extends to canvas edges) — same icon shape but
with the corner gutters filled. Leaving as a TODO.

This revert restores:
- favicon-16.png, favicon-32.png, apple-touch-icon.png, favicon.png
  regenerated via sips from apps/desktop/build/icon.png (the framed
  desktop icon).
- favicon.ico at root is a copy of the 32px variant.
- _layouts/default.html drops the SVG <link> and keeps the three
  sized PNG <link>s.

Deletes docs-site/assets/favicon.svg.

* docs(webhook-dangers): fix HMAC overclaim + add Mattermost-private nuance

Two structural fixes:

1. **Drop the broad 'PwrAgent does HMAC verification' framing.**
   The previous draft implied PwrAgent provides webhook signature
   verification generally. In reality, that's a Mattermost-specific
   callback-button mechanism that's also fiddly to set up. Replacing
   with: signature verification belongs at the tunnel layer
   (Cloudflare Worker, Tailscale-side middleware, reverse-proxy
   check), and the exact mechanism varies by platform AND by
   tunneling provider — figuring it out for your specific
   combination is beyond the scope of this guide. PwrAgent does
   not stand between the public internet and the agent here.

2. **Split Mattermost from LINE in the recommendation.**
   - Mattermost: if the server is yours (same machine, LAN, or
     private VPC), the callback can stay on the private network.
     127.0.0.1 dial-in works; no public tunnel, no public attack
     surface. Functionally equivalent to the outbound-socket
     platforms in that posture.
   - LINE: no private-network mode available. LINE's servers live
     on the public internet and must reach your webhook over the
     public internet. Stand up a public tunnel and do the
     operational hardening yourself; there's no shortcut. This
     is the platform we'd most steer people away from unless
     they need it.

Also fixes the See also links that still pointed at the old flat
mattermost.md / line.md paths — updated to /providers/mattermost/ +
/providers/line/.

The point of this rewrite is to make clear that operating a public
webhook safely is a real ops skill, not something PwrAgent papers
over for the user; and that whether you need that ops skill depends
on whose Mattermost server you're talking to.

* docs(using-codex): correct the pairing flow — universal, two-phase

The 'pairing-DM flow on Telegram is the friendlier version of this
same path' line was wrong on three counts:

1. Pairing is **not Telegram-specific.** It's a PwrAgent feature
   available on every supported provider — Telegram, Discord,
   Slack, Mattermost, Feishu/Lark, LINE.

2. Pairing is **not DM-only.** It has two phases: first you pair
   yourself as a user (via DM on platforms where DMs exist, or
   from inside a space otherwise), then once you're an authorized
   user you can use the same flow to pair a shared space — a
   Slack workspace, Discord server, Telegram supergroup,
   Mattermost team, channel within a workspace/server/team,
   Feishu group chat, or LINE group/room.

3. Pairing is **not just 'the friendlier discovery-mode path.'**
   It's a distinct affordance with its own UX (generate token →
   send to bot → approve on desktop). The discovery-mode fallback
   still exists for both phases, but it's the cruder copy-paste
   alternative.

Restructured the 'Who can talk to the bot' section to split:

- The closed-by-default + two-keyed model (unchanged).
- A new 'Pairing — how you populate the allowlists' subsection
  walking the two-phase flow, with the per-platform space-type
  list (Telegram = DMs + supergroups; Discord = DMs + servers +
  channels; Slack = DMs + workspaces + channels; Mattermost =
  DMs + teams + channels; Feishu = DMs + group chats; LINE =
  1:1 chats + group chats + multi-person rooms).
- A 'Discovery-mode fallback' subsection that frames the
  copy-from-Activity path as the alternative for either phase.
- A 'Stable IDs only' callout — pulled out of the prose so it's
  scannable.

Follow-up to consider: each per-provider setup page under
docs-site/providers/ currently mentions pairing only briefly (or
not at all for Slack / Mattermost / Feishu / LINE). Those pages
should pick up the space-pair step explicitly when we know the
exact UX for each. Tracked for a future pass.

* docs(using-codex): note LINE's button restrictions in slash-commands exceptions

Adds a LINE row to the Per-provider exceptions block under
'Slash commands and buttons' covering the three reasons LINE is the
hardest provider for button-driven flows:

1. **Action budget = 13 per message** — tightest of any supported
   platform. Mattermost/Discord/Slack get 25; Feishu gets 20;
   Telegram gets 100.
2. **Label cap = 20 characters** — tied with Feishu for shortest;
   long model and reasoning names truncate hardest.
3. **No message edits** — LINE's Messaging API has no edit
   primitive, so the Resume browser, the New Thread starter, the
   /help menu, and every other paginated button surface post a
   fresh card on each Prev / Next / page-change click rather than
   editing the existing card in place.

The combination makes button-driven invocation noisier on LINE
than anywhere else; recommends text fallback (reply 1 / next /
back / cancel) when buttons read as ambiguous or when chains of
replacement cards would be disruptive.

The LINE-no-edits beat is already mentioned in
#debounce-queue-steer and #monitor-card exception blocks for those
specific surfaces; this adds the consequence for the button-paginated
flows specifically, where the missing-edits behavior is most
disruptive.

* docs(using-codex): add Skills browser section (R1.9 un-cut)

When the original plan was written, PwrAgent had no messaging-layer
Skills browser implementation — grep returned nothing, so R1.9 was
deliberately cut from v1 with a follow-up flag in the plan's
Outstanding section. The merged status-skills-browser PR (#354 on
main as of the latest rebase) adds the feature, and the existing
docs/messaging-platform-integration.md was updated upstream with a
~12-line Skills Browser section. This commit ports that operator
content into the docs-site usage guide so the public docs reflect
shipped behavior instead of the stale "cut" decision.

What the section covers:

- The Skills button on the bound-thread status card opens a paged
  picker with the same Next/Prev/Cancel mechanics as the Resume
  browser. Search action makes the next free-form reply the skill
  query (re-renders the browser with matches).
- Selection posts a confirmation showing the full $skill name and
  available metadata (description, workspace, enabled status, skill
  path) — does NOT start a turn.
- The skill is staged on the binding and prepended once to the next
  real user message (survives queueing through the
  debounce/queue/steer state machine).
- Commands, browser navigation, button callbacks, and skill-search
  replies do NOT consume the staged skill — safe operations between
  staging and sending the real prompt.
- The selection confirmation message has a Remove button to clear
  the staged skill.
- Staging is per-binding; multiple bindings on the same thread
  carry independent staged skills.

Also updates:
- TOC under "While the thread is bound" gains a "Skills browser"
  entry (now 5 sub-sections in that group).
- Status-card buttons table gains a Skills row between Stream and
  Refresh (matches the order the bound-thread status card actually
  renders).

* docs(using-codex): note Discord + Feishu typing-indicator quirks

Adds a Per-provider exceptions <details> block to #typing-indicators
covering the two known platform-specific behaviors:

- **Discord**: the API has no 'stop typing' call. Typing is
  fire-and-forget and the platform auto-expires it on its own clock
  (~5 seconds after the bot's last typing event). When a bound turn
  ends, the indicator can linger for several seconds before Discord
  clears it. PwrAgent can't shorten that tail.

- **Feishu / Lark**: the messaging API doesn't surface bot typing
  indicators at all. No 'thinking' cue while a turn runs — the
  reply just shows up when it arrives.

Other platforms are silent on this (no documented exception); if
LINE or Slack also have restrictions worth calling out, they can be
added to the same block in a follow-up.

* docs(using-codex): correct attachment claims — PDF + image-profile

Three corrections after grep-checking the actual code in
apps/desktop/src/main/messaging/core/messaging-attachment-processor.ts:

1. **GIF claim was actually correct** — the processor does call
   normalizeMessagingImageAttachment with kind 'gif' and posts a
   note that the first frame was used. Kept this behavior but
   tightened the phrasing: animated GIFs convert to a still
   image (first frame), animation is not delivered to the model.

2. **PDF claim was misleading.** The 'forwarded when text can be
   extracted, OCR-only rejected' table row implied robust PDF
   text extraction. Reality (extractBasicPdfText, lines 271-277):
   a single regex over raw bytes looking for '(text) Tj' operators
   decoded as latin1. Works on the simplest PDFs and rejects
   everything else — including multi-column, font-subsetted,
   compressed, or scanned PDFs (which is most real-world PDFs).

   Replaced the table row with a dedicated 'A note on PDFs'
   subsection that's honest about the limited support and tells
   the operator what actually works: either let the model
   render-and-analyze the page images itself (when supported), or
   — recommended for anything you care about — convert the PDF
   pages to PNG/JPEG yourself and send those as image attachments.

3. **Image-upload-profile UI claim was wrong.** The previous table
   row said images are 'uploaded through the same upload-profile
   setting as desktop paste (Low / Medium / High / Actual)' —
   implying a Settings UI control. The control exists only as
   the attachment_image_profile TOML key and the
   PWRAGNT_MESSAGING_ATTACHMENT_IMAGE_PROFILE env var (verified
   via grep: zero matches for the label in renderer Settings
   code outside __tests__). New 'Image upload profile (TOML /
   env only)' subsection states the available values, where to
   set them, and that the Settings UI does NOT currently expose
   this.

Also restructured the section slightly: the rejection list now
covers only file-type / size / count rejections, and 'OCR-only
PDFs (no extractable text)' is gone from the bullet list because
the PDF subsection covers the whole story more honestly. The
debounce-ordering note moved to its own subsection at the bottom
for scannability.

* docs(using-codex): PDFs forward to the model as-is

PwrAgent's in-app PDF text extraction (the basic regex-over-bytes
path in messaging-attachment-processor.ts) is being removed.
Updates the docs to match: PDFs are now forwarded to the model
as-is; PwrAgent does no text extraction or preprocessing on its
side.

Adds PDFs as an Accepted-types row in the attachments table:
'Forwarded to the model as-is. PwrAgent does no text extraction
or preprocessing — see A note on PDFs.'

The 'A note on PDFs' subsection no longer claims PwrAgent does
any extraction. It splits behavior by what the model does instead:
modern Codex models try to render the PDF pages and analyze the
images themselves (layout-dependent; multi-column tends to
confuse them); other models either produce a worse result or
refuse the document. The recommendation for anything worth
getting right remains: convert the pages to PNG/JPEG yourself
and send those as image attachments — predictable across models,
you control which pages get sent.

* docs(using-codex): note that @PwrAgent is a stand-in for the bot's actual handle

Throughout the guide, @PwrAgent was being used in at-mention
examples as if it were the literal handle, but the actual handle
depends on what the operator named their bot on each platform.

Adds a callout at the top of the At-mention commands section
defining the convention: @PwrAgent in this guide is a stand-in for
whatever handle the bot actually has, with per-platform notes for
what to substitute (Telegram BotFather @username, Discord/Slack
display name, Mattermost bot account username). Read every
'@PwrAgent' below as the at-mention syntax for your bot on your
platform.

Single edit, no anchor changes — the at-mention examples elsewhere
in the guide can stay as-is since the convention is now defined in
one place.

* docs(using-codex): correct monitor-card cycle values and surface scope

Two corrections after grep-checking
apps/desktop/src/main/messaging/core/messaging-monitor-card.ts:

1. **Pins / Recent count cycle** was wrong. Doc said '0/1/2/3/5' for
   both buttons. Reality (line 37): MESSAGING_MONITOR_THREAD_LIMIT_OPTIONS
   = [0, 5, 10]. Both buttons cycle 0 -> 5 -> 10. Default 5.

2. **Snippet was a length cycle in the doc, but it's a toggle in the
   code.** Doc said 'Cycle snippet length (50/100/200/400 chars)'.
   Reality: MESSAGING_MONITOR_SNIPPET_LENGTH = 100 (fixed) and the
   action posts 'monitor snippet on/off' (line 263). It's a boolean
   toggle on a ~100-char preview, not a length cycle.

3. **Scope of the monitor card** was framed wrong in the intro. The
   doc described 'pinned items / recent items on the thread' as if
   the card showed a feed of the BOUND thread's contents. Reality:
   selectMonitorThreads pulls threads from the navigation snapshot
   (the whole workspace), splits them into pinned and recent, and
   renders each as a row on the card. It's a workspace-level
   dashboard, not a bound-thread feed.

Updated:
- The intro now says 'pinned and recent threads across the
  workspace — not items inside the bound thread.'
- Pinned-threads bullet now reads 'Default 5; can cycle to 0 (hide
  the section) or 10.' Same for recent.
- Snippet bullet describes it as 'optional ~100-character preview
  of each thread's latest response. Toggle on or off; default on.'
- Button-row table values: 0 -> 5 -> 10 for Pins and Recent; Snippet
  is a toggle (default on; fixed ~100-char preview).
- Interval defaults clarified (default 60s).

* docs(using-codex): monitor is a binding, not a per-thread overlay

Two corrections after user feedback on monitor binding semantics:

1. **The monitor occupies the conversation's binding slot.** A
   conversation hosts EITHER a thread binding OR a monitor binding,
   not both. The previous draft said 'the monitor lives on the
   binding, so multiple bound threads can each have their own
   monitor card in their respective conversations' — that implied
   monitor and thread bindings coexist on the same surface, which
   isn't how it works. Running /monitor on a thread-bound
   conversation replaces the thread binding; the thread keeps
   running on the desktop but the conversation no longer drives it.

2. **Dedicate a separate surface for the monitor.** Added a
   recommendation block with concrete options: a specific topic in
   a Telegram supergroup, a dedicated Telegram DM, a dedicated
   Discord channel — anywhere quiet enough to see the refresh ticks
   without competing with active conversation.

Also corrected two follow-on claims:

- The Stop button is now described as 'Convert the monitor binding
  back to a regular conversation' (more accurate than 'Unsubscribe
  the binding from the monitor' which framed the binding as
  separate from the monitor).
- The 'Archiving the bound thread automatically detaches the
  binding' line was wrong for monitor bindings — the monitor isn't
  tied to a specific thread, so archiving one thread just drops it
  from the pinned/recent rows on the next refresh; the monitor
  binding itself stays. Replaced with the correct: archive removes
  the thread from the dashboard view, doesn't detach the monitor.
- New paragraph on the two paths to switch a monitor surface back
  (Stop button or /detach), then optionally /resume to bind it to
  a thread.

* docs(docs-site): top-level Desktop / Messaging / Settings restructure

Restructures the top nav from
  [Using Codex | Providers | Streaming | Webhooks | GitHub]
to
  [Desktop | Messaging | Settings | GitHub]

The previous label "Using Codex" promised broader content than the
page delivered — the linked page was specifically "Using Codex via
Messaging." This restructure gives each top-level entry a real
section landing page so what you click to is what you find.

### New pages

- **/desktop/** — the PwrAgent desktop app itself: process model,
  Recents lens, thread workspaces (Local vs. Worktree), handoff,
  per-thread settings, auto-naming, access modes, approval surface,
  markdown composer, search / pins / markers. Includes "Not yet"
  (forking, restoring archived threads, tight auto-archiving,
  branch auto-naming via button) and "Coming soon" (environment
  setup on new worktree, environment cleanup on archive/handoff).

- **/messaging/** — section landing for everything messaging:
  Using Codex via Messaging, per-provider setup table (with the
  Public port? ✅/❌ column carried over from index), and the
  cross-cutting reference pages (streaming, webhook-dangers,
  rate-limits). Replaces the previous "Using Codex from your
  messenger" pitch on the index landing.

- **/settings/** — non-messaging settings:
  - Application discovery (terminal, editor, git, gh CLI) and
    per-tool overrides.
  - Worktrees: default storage location (~/.pwragent/worktrees/),
    same-filesystem requirement, override location, the actual
    on-disk shape.
  - Models / Codex App Server: PwrAgent doesn't ship its own,
    discovers from Codex Desktop OR Homebrew CLI OR other CLI
    install paths, uses the newest working version, shown in
    Settings → Models. Includes "keeping the App Server up to
    date" (run Codex Desktop periodically OR update CLI; either
    source on its own is fine) and "authentication" (PwrAgent
    piggybacks on Codex Desktop's auth state; relogin in Codex
    Desktop, not in PwrAgent — by design, no auth duplication).
  - Test button behavior + troubleshooting.

### Landing page

index.md trims its inline platform table and Read-before-you-toggle
list — that content moved to /messaging/. The landing now leads
with a 3-bullet "Browse the docs" section pointing at the three
section landings, keeping the hero wordmark + 2-paragraph pitch +
Get-started instructions on top.

### Existing pages

/using-codex/, /providers/, /streaming/, /webhook-dangers/, and
/rate-limits/ keep their URLs. The Messaging hub lists them all so
they remain discoverable; no redirect changes needed.

Codex Desktop coordination beat:
- Codex Desktop is the auth authority. To relogin: open Codex
  Desktop and re-auth there. There is no PwrAgent-side auth flow
  (by design).
- Codex App Server can come from multiple sources (Codex Desktop,
  Homebrew, other CLI install paths). PwrAgent picks the newest
  working one and shows the version in Settings → Models.
- To stay current you need to update at least one source (Codex
  Desktop self-updates if you run it periodically, OR update the
  CLI via brew/etc).

The screenshot story for /desktop/, /settings/, and the per-
provider setup pages lands in a follow-up commit on this branch:
extending the existing readme-screenshots Playwright spec pattern
to a docs-site-screenshots spec, capturing the desktop-side shots,
and dropping placeholders + a shot list for the messenger-side
captures tracked at pwrdrvr/PwrAgent#345.

* docs(docs-site): screenshot placeholders + shot list for follow-up captures

Adds <!-- screenshot: <filename> — <hint> --> HTML-comment markers
in each new doc page where a desktop or messenger-side capture
should eventually land, plus a comprehensive shot list at
docs-site/assets/screenshots/DOCS_SITE_SHOT_LIST.md.

Placeholder coverage:

- /desktop/: desktop-recents.png (Recents lens), desktop-worktree-
  picker.png (handoff dialog).
- /settings/: settings-applications.png (Applications panel),
  settings-worktrees.png (Worktrees panel), settings-models.png
  (Models / Codex App Server panel).
- /using-codex/: desktop-status-card.png + messenger-status-card-
  telegram.png at the bound-thread status-card section;
  desktop-resume-browser.png + messenger-resume-browser-telegram.png
  at the Resume Thread browser section.
- providers/{telegram,discord,slack,mattermost,feishu,line}.md:
  one settings-messaging-<platform>.png placeholder per page,
  just before the Settings reference table, with a hint about
  what the panel state should look like for the capture.

Shot list documents:

- The reuse-the-readme-pattern recipe: extend
  apps/desktop/e2e/readme-screenshots.inspect.spec.ts to a new
  docs-site-screenshots.inspect.spec.ts, add per-surface state
  seeders in fixtures/docs-site-state-seeding.ts, add a
  pnpm screenshot:docs-site script parallel to
  screenshot:readme, document the regen flow in
  apps/desktop/AGENTS.md next to the existing 'Capturing README
  Screenshots' section.
- One row per shot: filename, target doc page, surface to drive,
  exact state to seed for an interesting-looking capture.
- Cross-link to pwrdrvr/PwrAgent#345 for the messenger-side
  captures (which are materially harder than the desktop-side
  captures — driving messenger native clients is its own
  pipeline build-out).

Deliberately not building the spec or state-seeding code in this
commit. The existing readme-screenshots system is ~1040 lines of
carefully-tested infrastructure; extending it by another 600+
lines blind, without a live desktop to verify captures actually
work, would risk shipping broken tooling on top of a
documentation PR. The shot list gives the eventual implementation
clear acceptance criteria.

* docs(docs-site): ship docs-site-screenshots spec + 10 captured PNGs

Extends the existing readme-screenshots spec pattern to a new
docs-site-screenshots spec that produces the PNGs docs.pwragent.ai
needs. Captures land in docs-site/assets/screenshots/ instead of
docs/assets/screenshots/.

### New tooling

- `apps/desktop/e2e/docs-site-screenshots.inspect.spec.ts` — ten
  tests, one per surface. Gated behind
  PWRAGENT_DOCS_SITE_SCREENSHOT_CAPTURE=1.
- `apps/desktop/e2e/fixtures/docs-site-state-seeding.ts` — a
  config.toml seeder that enables every messaging provider so each
  per-platform Settings section renders fully populated (empty
  credential fields, but full field structure).
- `apps/desktop/package.json` — new `screenshot:docs-site` script
  parallel to `screenshot:readme`.
- `apps/desktop/AGENTS.md` — docs-site capture section added
  alongside the existing README capture section.

### Captured PNGs

Ten captures, all at 1440x900 via the same capture-window.swift
native macOS pipeline the README screenshots use (stoplights + drop
shadow + retina):

- `settings-applications.png` — Settings → Applications with all
  four tools auto-discovered.
- `settings-worktrees.png` — Settings → Worktrees showing the
  default storage path.
- `settings-models.png` — Settings → Models with App Server version
  and source.
- `settings-messaging-{telegram,discord,slack,mattermost,feishu,line}.png`
  — each platform's Settings panel scrolled into view; all six
  enabled via the new docs-site state seeder.
- `desktop-recents.png` — Recents lens populated with realistic
  threads (reuses the readme-recents-hero fixture).

Total spec runtime: ~35 seconds on the dev machine.

### Doc wiring

Replaced 10 `<!-- screenshot: ... -->` placeholder comments with
real `![]()` image references:

- docs-site/desktop.md → desktop-recents.png.
- docs-site/settings.md → settings-applications.png,
  settings-worktrees.png, settings-models.png (each inline next to
  the corresponding section).
- docs-site/providers/{platform}.md → settings-messaging-{platform}.png
  next to the Settings reference table.

The remaining placeholders (desktop-status-card.png,
desktop-resume-browser.png, desktop-worktree-picker.png, and the
messenger-side captures) are intentionally deferred — they need
either new fixtures with bound-thread state or messenger-client
driving that's out-of-scope for this PR. Tracked at
pwrdrvr/PwrAgent#345 for the messenger-side captures.

* docs(desktop): add strikethrough + bulleted/numbered lists to markdown composer

The composer supports more markdown shortcuts than the previous
three-bullet list captured. Adding:

- Strikethrough via ~~text~~ in the inline-formatting list.
- - or * + space starts a bulleted list (Enter on an empty bullet
  exits).
- 1. + space starts a numbered list (subsequent Enters keep the
  numbering until you exit on an empty item).

* docs(desktop): three-lens sidebar — Inbox is the default again

The rebase brought in feat(desktop): restore thread lens modes
(#381), which restores Inbox as the default browsing lens alongside
Recents and Directories. The previous /desktop/ page claimed
'The default browsing surface is the Recents lens' and 'The other
lens you can switch to is Directories' — both stale after the
restore.

Updated:

- Section heading 'Recents lens' -> 'Sidebar lenses' (the section
  is about all three, not just Recents).
- Three-bullet description of each lens: Inbox (default, unread-
  forward), Recents (most-recently-touched), Directories (grouped
  by project / repository).
- Pins live at the top of both Inbox and Recents (was 'top of the
  same list', which only made sense in the single-lens-Recents
  world).

Caught the staleness during a rebase off origin/main; this commit
brings the published docs back in sync with desktop AGENTS.md's
'Non-Negotiables' bullet on the same topic.

* docs(desktop): document the two-axis profile model

New 'Multiple profiles' section on /desktop/ covering the two
independent profile mechanisms and how they compose:

- **PwrAgent profile** (PWRAGENT_PROFILE env var) — isolates
  config.toml, state.db, new-thread sticky settings, and the
  entire messaging profile (different bots per profile, even on
  the same platform).
- **Codex profile** (CODEX_HOME dir under ~/.codex/profiles/<name>/)
  — isolates Codex threads + auth, picked from Settings → Models
  → Codex inside whichever PwrAgent profile you launched.

The two settings live at different layers and don't interact. The
section walks the CLI bootstrap (mkdir, codex login under an
isolated CODEX_HOME, copy config.toml as a starting point) and
the wire-up inside PwrAgent (Settings → Models → Codex → Auth
profile).

Includes:

- Cheekier 4-Codex-Pro-account use case (per user's framing — 'be
  an animal and need four accounts worth of tokens to rule the
  world').
- Worked PWRAGENT_PROFILE example for both source dev and the
  installed .app (direct binary launch via
  /Applications/PwrAgent.app/Contents/MacOS/PwrAgent so the env
  var actually reaches the process — macOS `open` strips most env
  vars).
- A composition table mapping each isolation knob (settings, state,
  messaging, threads, Codex auth) to which profile mechanism owns
  it.
- Verification commands (rg over the PwrAgent config, find over the
  Codex home) so operators can confirm without guessing.

Also adds an 'In-app profile management' item under Coming soon
flagging that the current CLI bootstrap is the manual path until
the in-app flow lands.

* docs(docs-site): fix broken /messaging/ relative links + clickable platform names

Two related fixes to the messaging hub and providers index:

1. **All relative links on /messaging/ were broken.** The page lives
   at /messaging/, so href='providers/' resolved to
   /messaging/providers/ (404) and same for using-codex/, streaming/,
   webhook-dangers/, rate-limits/. Every link on that page needed to
   be ../<page>/ instead. Fixed all 11 references across the
   'Where to start', 'Setting up providers' table, the
   ✅/❌ explainer, the 'Read before you toggle' list, and the
   ❌-Yes paragraph.

2. **Platform names in the providers tables are now clickable.**
   Both /messaging/'s 'Setting up providers' table and the
   /providers/ index table previously rendered the Platform column
   as plain text — only the rightmost Setup column carried a link.
   The first column the eye lands on is now a clickable link
   straight to the platform's setup page. Kept the Setup column
   too so there are two ways to click through the same row.

Verified with grep on the rendered HTML — every href on /messaging/
now starts with ../ and every target returns 200.

* docs(providers): replace [webhook-dangers.md] link labels with prose

Two leftovers from the early provider-page writes — every reference
to webhook-dangers.md and streaming.md was using the literal
filename as the visible link label, even after a previous sed pass
fixed the URL targets. Replaced with the natural-language page
titles:

- '[webhook-dangers.md](/webhook-dangers/)' →
  '[Webhooks — a security note](/webhook-dangers/)'
- '[streaming.md](/streaming/)' →
  '[Streaming responses](/streaming/)'

Also normalized two stragglers that were using a colon instead of
the canonical em-dash ('Webhooks: a security note' → 'Webhooks — a
security note') to match the actual page title in
docs-site/webhook-dangers.md's frontmatter.

Affects all six provider pages plus the See also sections of
streaming.md and webhook-dangers.md themselves.

* docs(docs-site): Environments, in-app profiles, fixture-driven pairing captures

Three feature updates landed on origin/main since the last rebase
that the docs were stale on, plus the screenshot infrastructure
beef-up requested for the desktop app section:

### Codex environments (3294b771, 8e7f3c69)

New /desktop/#codex-environments section. Environments attach
optional setup hooks to a new worktree (install deps, warm caches,
run codegen) that stream live into the transcript, and gate which
commands the agent can invoke during the turn. Usable today, not
yet editable in-app — environments are authored at the Codex level
and PwrAgent picks them up. Roadmap entry under Coming soon noted
in-app editing as the next step.

### In-app profile management (27344389)

The /desktop/#multiple-profiles section was leading with a heavy
CLI bootstrap because in-app management didn't exist when first
written. Rewritten to lead with Settings → Profiles (the new
panel), and demoted the under-the-hood file paths + sqlite mental
model to a brief 'Under the hood' subsection.

The CLI launch path now uses the supported `--profile <name>` flag
(`open -na PwrAgent --args --profile work`) instead of the env-var-
through-Launch-Services workaround. PWRAGENT_PROFILE env var is
documented as the dev-server / shell-script fallback.

Coming soon dropped 'In-app profile management' (shipped) and
'Environment setup on new worktree' (shipped). What stays:
environment cleanup on archive/handoff, in-app environment editing.

/settings/ gains a 'Profiles' section pointing at the in-app surface
and cross-linking the conceptual model on /desktop/.

### Image upload settings UI (b71097b9)

/using-codex/#image-upload-profile rewritten — the previous draft
said 'Settings UI does not expose this control today' and pointed
operators at TOML/env vars only. The control is now in Settings →
Messaging in the desktop UI; TOML/env are documented as headless /
scripted overrides.

/settings/ gains a 'General' section covering the new pasted-image
patch budget control (1024 / 1536 default / 4096 / Actual size)
which lives at the desktop-wide General level.

### Settings nav restructure

Reflects the new 8-section nav: General, Applications, Profiles,
Worktrees, Messaging, Models, Experimental, About. /settings/'s
opening lists all eight with brief descriptions or cross-links.
Adds a brief 'Experimental' section covering the diff-condensation
toggle that's currently the panel's main feature.

### Screenshot captures (six new)

docs-site-screenshots.inspect.spec.ts gained six new tests:

- settings-profiles.png — Settings → Profiles panel.
- settings-general.png — Settings → General panel (pasted image
  patch budget).
- settings-experimental.png — Settings → Experimental panel.
- messaging-pairing-frame-1.png — Pairing token generated, code
  visible in the field.
- messaging-pairing-frame-2.png — Pairing observed, approval prompt
  visible (uses readme-state-seeding's markPairingObserved with the
  same Riley Chen sanitized persona as the README pairing GIF).
- messaging-pairing-frame-3.png — Approved, user (id 5550199999 +
  resolved name 'Riley Chen') in the Telegram authorized user list.

The pairing-frame captures are fixture-driven via direct sqlite
mutation between Generate → Approve, so they reproduce
deterministically without anyone having to manually drive a real
Telegram bot conversation and sanitize the result. Pattern lifted
from the existing readme-screenshots pairing-GIF spec.

All 16 captures pass in ~60 seconds via
`pnpm --filter @pwragent/desktop screenshot:docs-site`.

### Doc wiring

- /desktop/#multiple-profiles  → settings-profiles.png inline.
- /using-codex/#pairing--how-you-populate-the-allowlists → new
  'What each phase looks like' subsection with all three pairing
  frames.
- /settings/#general → settings-general.png inline.
- /settings/#experimental → settings-experimental.png inline.
- /settings/#profiles → settings-profiles.png inline.

Placeholders for desktop-environments.png, desktop-status-card.png,
desktop-resume-browser.png, and desktop-worktree-picker.png remain
as HTML comments — those need additional fixtures to surface the
right state cleanly and can ship as follow-up captures.

* docs: move pairing flow captures from /using-codex/ to providers/telegram/

The three messaging-pairing-frame-{1,2,3}.png captures land more
naturally on the per-provider setup page (operator setup) than on
/using-codex/ (usage guide / conceptual model).

- /using-codex/#pairing--how-you-populate-the-allowlists: replaced
  the captured walkthrough with a one-line cross-link to the
  Telegram setup page. The conceptual two-phase pairing model and
  per-platform space-types description stay where they were.
- /providers/telegram/#pairing-flow-walkthrough: new section
  showing all three captured frames inline with prose explaining
  what's happening at each step. Cross-links back to
  /using-codex/'s conceptual section for readers who want the
  cross-platform model.

Captures themselves are unchanged.

* docs(messaging): centralize Pairing on /messaging/pairing/ + activity troubleshooting screen

The pairing flow is identical across every supported messaging
provider (the screenshots happen to be Telegram, but the mechanic
is the same on Discord, Slack, Mattermost, Feishu, and LINE), so
the captures should live once and be linked from every provider
page rather than duplicated per platform.

### New page: /messaging/pairing/

- Three-step walkthrough using the existing
  messaging-pairing-frame-{1,2,3}.png captures (Generate → send
  code → approve, with the resolved Riley Chen actor name landing
  in the Authorized User IDs list at frame 3).
- Phase 2 space-pairing one-paragraph aside.
- A new troubleshooting section showing how to find blocked
  inbound messages in Settings → Messaging → Activity, plus the
  five common reasons a pairing code doesn't observe (bot not
  receiving, platform requires shared-space membership first,
  token expired, sent from wrong account, autocorrect mangling).
- Cross-links back to the conceptual two-phase model on
  /using-codex/ and to the discovery-mode fallback for operators
  who'd rather copy peer IDs by hand.

### New capture: messaging-activity-blocked.png

New test in docs-site-screenshots.inspect.spec.ts that opens the
Messaging Activity window via the IPC bridge, seeds four
sanitized rejected-inbound entries (two Telegram, one Discord,
one Slack — all with fictional actor IDs and the same Riley Chen
/ Casey Wong / Jordan Lee / Morgan Patel sanitized personas as
the rest of the readme + docs-site captures), and captures the
Attention section showing the rejected entries with their
copyable Peer/Conversation/Supergroup/Topic IDs.

The Bound activity section is intentionally empty in the capture
so the Attention section is the dominant surface — that's the
section operators need to find when troubleshooting "I sent a
message to the bot but nothing happened."

### Cross-link cleanup

Removed the per-platform "Pairing flow walkthrough" section I
added to providers/telegram.md last commit. Replaced with a
one-paragraph cross-link to /messaging/pairing/. Added the
identical cross-link as a "## Pairing" section to discord.md,
slack.md, mattermost.md, feishu.md, line.md so every provider
page surfaces it consistently.

Updated /messaging/ hub to include a "## Pairing" section linking
to /messaging/pairing/. Updated /using-codex/'s pairing section
cross-link to point at /messaging/pairing/ instead of the
provider-specific anchor it used briefly.

* docs(docs-site): dropdown nav for Desktop/Messaging/Settings + brighter inline link underlines

The flat top nav made it hard to discover that each top-level page
had named child sections (Settings → Profiles, Desktop → Codex
environments, etc.), and the inline underline color
(--border-strong = rgba(247,243,235,0.20)) was so dim against the
black canvas that body copy didn't read as containing links at all.

- Top nav: each parent (Desktop / Messaging / Settings) now expands
  on hover/focus-within into a panel of its key in-page anchors.
  Caret indicator on the parent label, keyboard-accessible via
  :focus-within. On <=720px viewports the dropdowns flatten into
  inline indented child lists.
- Body link underline color: rgba(255,138,31,0.55) — tangerine-tinted,
  visible on the black background, matches the accent on hover.

* ci(docs): add Pages workflow to deploy docs-site/ to docs.pwragent.ai

GitHub Pages' built-in "Deploy from a branch" source only supports
`/` or `/docs`. `docs/` is already used for brainstorms / plans /
architecture, so the docs site can't sit there. Use the GitHub
Actions source mode instead — Ruby + Jekyll build inside
`docs-site/`, upload as a Pages artifact, deploy.

One-time setup after this lands: in Settings → Pages, switch Source
to "GitHub Actions" (currently Pages isn't enabled on the repo at
all), then re-run this workflow. CNAME / custom-domain bits are
already in place (docs-site/CNAME + url in _config.yml).

---------

Co-authored-by: Claude Opus 4.7 (200K context, extended thinking) <noreply@anthropic.com>
…resume-replay note (#431)

Three things land together because the nav restructure and the mobile
fixes touch the same layout + stylesheet:

Messaging → Providers becomes a real nested submenu rather than a single
flat link. On desktop, hovering Providers fans a side-panel with all six
platforms (Telegram, Discord, Feishu / Lark, Slack, Mattermost, LINE).
On touch / mobile, an explicit toggle button next to the Providers row
expands the same six links inline under it.

Mobile audit (captured at 393×852 against the live site) caught:
- sticky header + always-expanded dropdowns ate ~60% of the viewport
  before any content showed, and followed users as they scrolled
- the "GitHub →" link flex-wrapped to an awkward position next to
  Settings → Profiles instead of stacking on its own row
- the 4-column "Optional Settings reference" tables on provider pages
  overflowed the article column by ~25px, causing document-level
  horizontal scroll

Fixes:
- header is `position: static` on ≤720px; nav stacks as full-width rows
- dropdowns are now opt-in on mobile via explicit <button> toggles
  (driven by aria-expanded; ≤16-line collapsed nav vs. the old ~700px
  always-expanded wall)
- `.page table { display: block; overflow-x: auto }` on ≤720px keeps
  wide tables self-contained — they get their own scroll instead of
  pushing body width
- `(hover: none) and (pointer: coarse)` branch ensures iPads and large
  touch phones get tap-to-toggle dropdowns regardless of viewport width,
  so the Messaging menu actually opens on touch

The using-codex resume section gets a paragraph for #416 (PwrAgent now
best-effort replays the bound thread's most recent assistant reply when
you resume, with an age label like "from 14 min ago"). Runs through the
Resume browser, direct /resume <id>, and Full Access escalation paths.

Verified locally (Docker rebuild + agent-browser captures at 393, 768,
1440 widths): no document-level horizontal scroll on any page at 393px,
dropdowns hover-open on desktop, nested Providers submenu fans to the
right of the Messaging panel on desktop and inline-indents on mobile.
* fix(desktop): open source links at target lines

* fix(desktop): resolve bundled editor launchers
#432)

* docs(docs-site): value-first landing page + correct sidebar lens names

Two coupled fixes from a user re-read of the live site.

Landing page rewrite. The original opened with "What's in the
desktop today: lenses" — which buries the value pitch behind an
implementation detail nobody outside the codebase cares about. The
new opening leads with what the thing actually IS (an agentic
coding environment in the Codex Desktop family, sharing thread
state by default, with isolated profiles when you want separation),
how it differs (messaging integration on six platforms for driving
threads from your phone), what dogfood tells us about reliability
(author's primary coding environment, hundreds of PRs created or
reviewed through it), and an explicit invite for contributors to
bring their own ideas on top of what's already shipped.

Sidebar lens names + sort semantics. The lens labels in code are
**Updated** / **Created** / **Directories** (apps/desktop/src/
renderer/src/features/navigation/Sidebar.tsx:106-110) — the docs
were still naming them Inbox / Recents / Directories from the
former internal keys. The sort behavior was also off: Updated
sorts by most-recently-updated (active threads bubble up), Created
is a stable sort by creation time so the order doesn't shift while
you work, and Directories groups by repo with per-directory pin
shelves. A new lens table + Pins subsection + Unread marker
subsection describes the actual behavior; the stale "row, not a
punctuation badge" carry-over (a "should be A not B" leak from
the requirements doc, meaningless to readers who never thought
it was a punctuation badge) is dropped in favor of a positive
description of the orange cookie marker.

Two downstream carry-overs in the same page get cleaned up:
- "Pin threads at the top of Recents" was duplicating the new Pins
  subsection and naming the wrong lens — replaced with a pointer
  to the Pins subsection.
- "Threads stay in Recents indefinitely" → "Threads stay in the
  active list indefinitely" (the active-list pool is shared
  across all three lenses; only the sort differs).

* docs(docs-site): "created by PwrDrvr LLC" instead of "owned by"

"Owned by" reads cold next to an MIT license. Same legal reality,
warmer phrasing.

* docs(docs-site): real-product hero screenshot + Desktop page value pitch

Hero. The landing page now opens with an actual screenshot of the
desktop in use — Directories lens grouping threads under PwrAgnt
and PwrSnap, a mid-conversation thread about OCR image tags with
the agent's reply showing passing pnpm lint, four messenger status
icons (Telegram, Discord, Slack, Mattermost) in the title bar,
profile selector reading "profile:default, codex:default", and
per-thread model / Full Access / Fast mode / Worktree controls
above the composer. Replaces the wordmark-hero, which was generic
branding; the brand mark already lives in the site header on every
page.

The screenshot ships with a descriptive alt text (covers every
visible element so screen readers and SEO bots get the same picture
sighted users do) and a short caption beneath.

Desktop page intro. Was "PwrAgent is an Electron desktop app. It
runs locally..." which buried the value pitch under utility info.
The new intro opens with shared thread state vs. Codex Desktop,
then bullets what makes the PwrAgent side worth running: per-thread
settings (vs Codex Desktop's globals), worktree workspaces with
managed handoff, Codex environment setup hooks streaming into the
transcript, the in-place Markdown composer, layered PwrAgent +
Codex profile isolation, and messaging as a first-class concept.
The technical setup info (Electron, ~/.pwragent/, --profile flag,
no cloud relay) drops below the value pitch where it belongs.

The screenshot is 2880×1920 (1440×960 at 2x retina) and ~313 KB.
Sized to render at full content-column width with a thin frame.
iPhone Safari capture showed the mobile parent-row toggles
rendering as dim text characters in --text-secondary against the
black canvas — hard to find, harder to recognize as interactive,
and indistinguishable from the parent label text next to them.

The mobile media query now styles each toggle as an outlined pill:
- 44px min tap target (Apple HIG floor)
- accent-tinted background + border so it pops as an interactive
  affordance, not a glyph buried in text
- 16px bold chevron in --accent for the same visibility reason
- brighter border when aria-expanded="true" so the open ones read
  as "the active one"

Desktop dropdown UX is unchanged — toggles are still display:none
on hover-capable pointers; the @media (hover: none), (pointer:
coarse) branch retains its own minimal styling.

After CF cache purge, no fingerprinting needed on the link.
The CI workflow (Vitest, typecheck, lint, dependency-cruiser, the
full build matrix) runs on every push and PR. Docs-only PRs touching
just docs-site/ don't change any runtime code, so spinning up the
full CI matrix is pure noise (and minutes).

Add paths-ignore on both push and pull_request triggers covering:
- docs-site/** — the operator-facing Jekyll site
- .github/workflows/pages.yml — the Pages deploy workflow itself

If a PR touches docs-site/ AND any other path, paths-ignore lets CI
run (it skips only when EVERY changed file matches the ignore set).
The Pages deploy workflow on pages.yml continues to fire on the same
push because it's a separate workflow with its own trigger.

Branch protection: if "CI" is a required status check, docs-only PRs
would have a pending check forever. Resolve by either marking CI as
not-required, or adding a no-op "CI" placeholder workflow that
always passes for docs-only paths. Not addressing here — defer to
when (if) it actually blocks a merge.
… pitch (#435)

* docs(docs-site): render triple-backticks as inline code in Markdown composer pitch

The composer description listed every other Markdown trigger as a
code-styled token — `>`, `-`, `*`, `1.`, **bold**, *italic*,
~~strikethrough~~, code — but spelled out "triple backticks" /
"Triple backticks" as prose in two places. Visually broke the
pattern; readers had to mentally translate "triple backticks" back
into the literal characters.

Now renders as code-styled ``` consistent with the surrounding
list items in both spots (Desktop page intro bullet + the detailed
Markdown composer section further down).

Kramdown handles this via a four-backtick fence around three
literal backticks: ```` ``` ```` — the longer fence is needed
because the content itself contains backticks. Verified rendered
output via local Docker build.

* docs(docs-site): expand Desktop dropdown to surface every "What's in the desktop today" feature

The Desktop dropdown listed six sections (Sidebar lenses, Workspaces &
worktrees, Codex environments, Per-thread settings, Multiple profiles,
Not yet / Coming soon), which left several features that operators
care about completely invisible from the menu — Markdown composer
(which Codex Desktop doesn't have, May 15 2026), access modes /
approval mirroring, and the "what 'Multiple profiles' actually means"
hook.

The menu is the first thing a curious browser uses to decide whether
this thing is worth their time. Items now read as value hints rather
than bare H3 text, so scanning the menu surfaces the "oh, that's
interesting" moments:

- "Sidebar lenses (Updated / Created / Directories)"
- "Workspaces — Local & Worktree"
- "Codex environments (setup hooks, command gating)"
- "Per-thread model / access / fast / reasoning"
- "Markdown composer (Codex Desktop doesn't have this)"
- "Access modes & approval mirroring"
- "Multiple Codex profiles + isolated PwrAgent state"
- "Not yet / Coming soon"

Each label fits under one line in the dropdown at 1440px viewport.
On mobile (<=720px) the inline-indented child list wraps long items
to two lines cleanly; verified.

Anchor IDs come straight from kramdown's heading slugification — no
H3 renames required, no permalink changes.

* docs(docs-site): rewrite Codex environments dropdown label + bump desktop caret

Two follow-ups from a real-display capture.

The "Codex environments (setup hooks, command gating)" label leaned
on jargon. "Worktree setup, quick commands" maps the same two
concepts to plain-English operator vocabulary — what an environment
actually IS to the operator (run on a worktree to set it up; expose
a small command set the agent can call).

The cosmetic ▾ caret on the desktop top-nav was 9px in
--text-muted, which at retina rendered as a barely-visible
speckle. Now 12px in --text-secondary, with margin-left bumped
4 → 5 to match the heavier glyph weight. Hover still tints to
--accent. Touch / mobile remain unaffected (they use the explicit
outlined toggle button from PR #434).
…Desktop (#438)

* docs(docs-site): document multi-message queueing + skills browser on Desktop

Two desktop features that were undocumented despite being load-bearing
for how the author actually uses the app:

Multi-message queueing. The composer's Send during an active turn
stages the message as a queued turn instead of dropping the click.
Queue is FIFO, multiple deep, and accepts slash commands. The
canonical worked example mirrors how the dogfooding flow looks:
  1. "make a branch and PR for the OAuth refactor" (turn 1)
  2. queue "/review" (turn 2)
  3. queue "now squash and push --force-with-lease" (turn 3)
The queue is the same per-thread queue the messaging side reads
and writes, so a turn queued from your phone shows up immediately
in the desktop composer's chip list.

Skills browser. Type $ in the composer to open an autocomplete
dropdown of every skill the bound Codex profile exposes —
including plugin-exposed skills. Pick one and it lands as an
inline chip; the chip expands into the skill's full $skill-name
mention markdown when the turn submits. The skill registry is
shared with the messaging Skills browser; adding a plugin-exposed
skill makes it available on both surfaces automatically.

Desktop dropdown gets two new entries:
  - "Multi-message queueing (chain turns + slash commands)"
  - "Skills browser ($ autocomplete + plugin skills)"

Both labels are phrased as value hints rather than bare H3 names,
consistent with how the rest of the Desktop dropdown reads.

Note on the skills anchor: kramdown slugified the heading
"Skills browser ($ autocomplete and chips)" to
"skills-browser--autocomplete-and-chips" (double dash from the
open-paren), which is what the dropdown href targets. Verified
via curl against the local Docker build before pushing.

* docs(docs-site): add fixture-driven capture tests for queued turns + skills autocomplete

Two new tests in docs-site-screenshots.inspect.spec.ts plus the
matching image references in desktop.md and SHOT_LIST entries:

desktop-skills-autocomplete — reuses the existing
skill-autocomplete-interactions replay fixture (which already
ships realistic skills: ce:plan, ce:brainstorm, ce:compound,
adversarial-document-reviewer, etc.). Test opens the seeded
thread, types "Let's use $ce" so the composer reads as a real
mid-thought, waits for the Skills listbox, and captures.

desktop-queued-turns — inline fixture (one thread, no git repo —
the queued-review-release spec needs a real repo to test branch
adoption, we just need the visual state). Test sends the first
message ("make a branch and PR for the OAuth refactor"), advances
the replay to turn-started so the composer flips Send → Queue,
queues /review, then queues "now squash and push --force-with-lease"
so the chip stack shows the FIFO-deep-queue capability rather than
a single chip.

Both image references are in place on desktop.md. The PNGs are not
yet committed — they're produced by:

  pnpm --filter @pwragent/desktop screenshot:docs-site

Screen Recording permission is required for whatever terminal /
IDE runs the spec (already granted from prior README captures).

* docs(docs-site): fix queue capture to use /review main + ship skills autocomplete PNG

Bare \`/review\` typed into the composer opens an inline target-picker
panel (the composer's ReviewConfig fieldset at line 1340 of
Composer.tsx — \`isBareReviewCommand && reviewConfig\`) and waits for
you to pick uncommittedChanges / baseBranch / commit / custom before
queueing. The test's \`Queue\` click against a bare \`/review\` therefore
never produced a chip → the assertion timed out.

\`/review main\` parses as a complete \`baseBranch\` review command via
parseReviewCommand and queues directly with the friendly
"Review changes against main" display label (same pattern as
queued-review-release.spec.ts:202). Updated the test fill + assertion
and the desktop.md alt text to match what the screenshot will show.

Side effect of the user running screenshot:docs-site to capture the
new skills autocomplete PNG: every existing capture got re-rendered.
Most diffs are 1-byte (lossless re-encode noise). A few have real
content changes from main-branch UI evolution since the original
captures:
- settings-models.png  637KB → 943KB
- settings-profiles.png 872KB → 805KB
- settings-worktrees.png 769KB → 769KB (similar size, slight diff)

All shipping together so the screenshots stay current with main.

User to re-run \`pnpm --filter @pwragent/desktop screenshot:docs-site\`
after this lands to capture desktop-queued-turns.png with the fix.

* docs(docs-site): ship desktop-queued-turns.png + bring re-captured images forward

The queue capture worked after the /review main fix from the prior
commit. The captured PNG shows the in-flight turn with two stacked
queued chips ("Review changes against main", "now squash and push
--force-with-lease"), the Send button correctly replaced with
Stop/Queue, and the standard profile/model/access controls below.

The user re-ran screenshot:docs-site, which regenerated every PNG
the spec produces — most diffs are 1-byte re-encode noise; the
ones with real content changes from main-branch UI evolution are
already documented in the prior commit's body.
)

Clicking the dropdown anchors (e.g. Desktop → Codex environments,
href="/desktop/#codex-environments") scrolled the target heading
underneath the sticky top header — the heading landed at
viewport y=0, but the header is ~59px tall, so the heading was
completely hidden.

Add `scroll-margin-top: 72px` to all headings inside `.page` when
the viewport is >720px. The browser honors this on anchor-driven
scroll and on `:target` highlighting, so jumps land the heading
~13px below the header's bottom edge instead of behind it.

Scoped to `min-width: 721px` because the mobile media query already
sets `.site-header { position: static }` — on mobile the header
scrolls away normally and a top margin would just add awkward
spacing.

Verified locally at 1440×900: `getBoundingClientRect().top` for
`#codex-environments` reports 72px (below the 59px header) instead
of 0 (behind it).
* ci(pages): bump deprecated actions to node24 versions

actions/checkout, upload-pages-artifact, and deploy-pages were still on
node20-era majors that GitHub now warns about. Their latest releases
ship on node24.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ci(pages): validate Jekyll build on PRs, gate deploy to main

Previously the Pages workflow only ran on push-to-main, and ci.yml
explicitly paths-ignored docs-site/ and pages.yml. So a PR that broke
the Jekyll build (or this very PR's action bumps) got zero validation
until after merge.

Add a pull_request trigger with the same path filter as push, and gate
the deploy job with `if: github.event_name != 'pull_request'` so PRs
exercise the build + artifact upload without touching the production
environment. Concurrency group is split: PRs use a per-ref group with
cancel-in-progress so a force-push supersedes the old build, while
main deploys keep the serialized, no-cancel behavior they had before.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Companion change to the existing .pnpmfile.cjs git-block hook.
Adds `global-pnpmfile=` to .npmrc so pnpm IGNORES any user-level
global pnpmfile when running inside this repo.

Why this matters: pnpm 10 hashes ALL active pnpmfiles (project +
global) into pnpm-lock.yaml's pnpmfileChecksum field. Without this
line, a contributor with a global hook configured (a common
supply-chain pattern — see e.g. the same git-block content this
repo's .pnpmfile.cjs already ships) computes a different
checksum than CI, then commits the new value and breaks the next
`pnpm install --frozen-lockfile` on CI with
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH.

With this line, every machine — local dev with any global pnpmfile
config, CI without one — hashes exactly the project's
.pnpmfile.cjs and nothing else. The lockfile's checksum
(qhUjtdsDx+KID8QibUkWT/nJBDVb/TMK08fsoGdLy78=) matches what every
contributor will compute regardless of their personal setup.

Lockfile is unchanged. Tests would pass. Cross-checked locally:
`pnpm install --frozen-lockfile` succeeds (exit 0). Same fix
PwrSnap landed in #44.
huntharo and others added 27 commits June 5, 2026 12:03
* fix(desktop): improve terminal notifications

* fix(desktop): refine native notification behavior

* test(desktop): align ACP event expectation

* test(desktop): wait for queued stop control
* fix(desktop): label latest request token usage

* fix(desktop): show cumulative output usage in context tooltip

* fix(desktop): finalize turn token usage totals

* fix(desktop): preserve aggregate turn usage on hydration

* fix(desktop): keep hydrated turn activities with usage totals

* fix(desktop): persist finalized token usage rows

* fix(desktop): suppress stale latest usage rows

* feat(desktop): show running turn usage

* fix(desktop): preserve finalized turn usage
* feat(release): add download stats skill

* fix(release): normalize alpha download stat selectors
…ex discovery + JSON-RPC (Wave 1) (#639)

* refactor(desktop): consume login-shell PATH hydration from @pwrdrvr/agent-transport (U2a)

Deletes the in-tree shell-environment.ts (+ its test) and imports
mergeLoginShellEnvIntoEnv from @pwrdrvr/agent-transport — the package was
extracted FROM this file, so it's behavior-identical: same merge shape + the
resolveShellEnv test seam, with PwrAgnt's logger injected (the kit defaults to
no-op) to preserve the diagnostic logs. desktop-settings-service test (the
resolveCodexShellEnv path) stays green.

First step of the documented PwrAgnt → @pwrdrvr/agent-kit migration (plan
docs/plans/2026-06-02-002), Wave 1 / U2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(desktop): consume @pwrdrvr/codex-discovery for Codex discovery + profiles (U3a)

Repoints all consumers (desktop-settings-service, stdio-transport, credential-tester,
transcript-image-protocol, thread-migration-service, ipc/profiles, ipc/settings) from
the in-tree settings/codex-discovery.ts + codex-profiles.ts to @pwrdrvr/codex-discovery,
then deletes the two in-tree files + their tests (the kit carries 36 codex-discovery
tests, seeded from these). MINIMUM_CODEX_CLI_VERSION matches exactly (0.125.0), so
discovery/version-gate behavior is preserved (KTD-P2). command-discovery.ts stays for
git/gh. The Codex login flow (inline in ipc/settings.ts) is unchanged — adopting the
kit's CodexLoginManager is the separate U3b step.

127 consumer tests + desktop typecheck green. Plan docs/plans/2026-06-02-002, Wave 1 / U3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(desktop): consume @pwrdrvr/agent-transport JSON-RPC connection (U2b)

Repoints all consumers (codex + grok app-server clients, the codex + ACP stdio
transports, protocol-capture, protocol-log-observer + their tests) from the
in-tree codex-app-server/json-rpc.ts to @pwrdrvr/agent-transport's
JsonRpcConnection + JsonRpcTransport/JsonRpcObserver interfaces, then deletes the
in-tree json-rpc.ts + its test (the kit carries the json-rpc suite). The kit's
JsonRpcConnectionOptions is a superset ({ logger?, logContext? }); both call
sites now inject getMainLogger('pwragent:json-rpc') so the protocol diagnostics
are preserved (the kit defaults to no-op).

The codex + ACP *stdio* transports stay in-tree — they're app-specific spawn
impls (codex resolves its binary + injects 'app-server'; ACP spawns the agent),
now implementing the kit's JsonRpcTransport interface. Only the generic JSON-RPC
core moves to the package.

105 consumer tests + desktop typecheck green. Plan Wave 1 / U2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(desktop): consume codex-discovery CodexLoginManager (U3b)

Replace PwrAgent's inline Codex profile-login machinery in ipc/settings.ts
(collectCodexStatus / parseCodexLoginPrompt / startCodexProfileLoginProcess /
the activeCodexLoginProcesses Map) with the published
@pwrdrvr/codex-discovery CodexLoginManager. The handler now constructs a
single module-level manager wired to shell.openExternal + the main logger,
delegates to manager.startProfileLogin(), and tears down via
manager.dispose(). checkCodexProfileAuthStatus stays in-tree.

Tests: the kit's bundled dist imports unprefixed `child_process`, so the
existing `vi.mock("node:child_process")` no longer intercepted login spawns
(the kit ran a real `codex login`). Inline @pwrdrvr/codex-discovery in the
desktop-main vitest project so vitest transforms it and the child_process
mock applies; add the matching `vi.mock("child_process")` alias in
settings-ipc.test.ts. Full desktop-main project green (1695 tests),
typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plans): mark agent-kit Wave 1 (U1–U4) complete

Record the shipped state: U1 protocol (PR #638), U2 transport, U3
discovery/profiles/login all consume the published @pwrdrvr/* packages;
U4 cleanup folded into U2/U3 (copies deleted as consumers were repointed,
went straight to published semver pins rather than file:/@next). Captures
the child_process-mock + server.deps.inline test-infra gotcha for future
kit-consumption work. Wave 2 stays staged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(desktop): regenerate THIRD_PARTY_LICENSES for agent-kit deps (Wave 1)

Wave 1 added @pwrdrvr/agent-core, @pwrdrvr/agent-transport, and
@pwrdrvr/codex-discovery (all MIT) but didn't refresh the third-party
notices. The release packaging gate (`pnpm licenses:check`, run by
release.mjs — not by the regular test CI) caught the drift. Regenerated
via `pnpm licenses:generate`; the diff is only the three new MIT entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(desktop): log Codex profile-login failures via our logger (U3b)

The codex-discovery CodexLoginManager rejects when `codex login` exits
without emitting a login link (e.g. it printed "Not logged in"). The
renderer (CodexAuthProfileSelect) already catches that rejection to show
the failure, but the start-codex-auth-profile-login IPC handler didn't
catch it — so the rejection also escaped to Electron's default ipcMain
handler logger ("Error occurred in handler for ...") instead of our
structured settingsIpcLog.

Wrap the startProfileLogin call: log the failure with {profile, error}
via settingsIpcLog.warn, then rethrow so the renderer still receives the
rejection it depends on. No contract change. settings-ipc 14/14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(deps): sync pnpm-lock pnpmfileChecksum with main

Rebasing onto main (now carrying the .pnpmfile.cjs checksum line) dropped
the `pnpmfileChecksum` metadata from this branch's lockfile hunk; `pnpm
install` re-adds it to the same value main records. No dependency graph
change (resolved 872, added 0) — restores frozen-lockfile installs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plans): note Kimi discovery parity check before U6 adopts agent-acp

When Wave 2 / U6 deletes in-tree acp-local-discovery.ts for @pwrdrvr/agent-acp,
the kit must carry both Kimi fixes or the "installed kimi is invisible" bug
returns: the ~/.kimi-code/bin path probe (#641) and exit-code detection of
`kimi acp --help` rather than fragile help-text matching (#645). Flags that
agent-acp@0.3.1's Kimi fix should be confirmed to use the exit-code signal
before adoption, and records that a kit bump is a no-op today since PwrAgnt
doesn't depend on or import agent-acp yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(desktop): bump @pwrdrvr/agent-transport to 0.1.5

Picks up the JsonRpcConnection fixes (restored error.data formatting +
send-failure pending cleanup). API/types unchanged (.d.ts identical to
0.1.4), so this is a pure version bump — no consumer code changes.

Regenerated THIRD_PARTY_LICENSES for the new version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ Wave 2 plan (#657)

* docs(plans): Wave 2 — adopt agent-acp/agent-client + redesign ACP settings

Grounds U5+U6 (and issue #646) in two investigations: PwrSnap's shipped
implementation (the target) and PwrAgnt's current in-tree ACP stack. Captures
the kit architecture (agent-core AgentBackend + Normalized* schema; agent-acp
AcpAgentClient + the extracted AcpSessionNormalizer + discovery strategies;
agent-client ChatThreadController), and phases the work:

- Phase A (~1wk, moderate): adopt the kit's multi-install discovery + redesign
  the settings screen to PwrSnap's card model (instances, Using, manual path),
  keeping the existing turn lifecycle. Subsumes the kimi fixes (#641/#645).
- Phase B (~2-3wk, critical): replace the hand-rolled normalizer/turn machine
  with AcpAgentClient + ChatThreadController, collapse the 40+ isAcpBackendId
  branches onto AgentBackend. Gated on a KTD-P3 lossless-replay proof (B1) done
  before any deletion.

Includes risks, the borrow-from-PwrSnap map, sizing, and three open decisions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(desktop): add @pwrdrvr/agent-acp + agent-core; record Wave 2 decisions

Phase A kickoff: pin agent-acp ^0.9.2 + agent-core ^0.1.3 (latest published).
Plan decisions resolved: keep the durable acp_installed_agents store + #643
freshness gate (intentional Refresh, not poll-every-open); defer the Phase B
Codex-path choice with a head-to-head to write up at Phase B kickoff; pin
latest published kit versions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): multi-install ACP discovery adapter + instance resolver (Phase A)

Foundation for the PwrSnap-style multi-install ACP settings. Adopts the kit's
`discoverLocalAcpAgentInstances` (every installed executable per agent — PATH
matches + well-known fallback bin dirs + a passing override — which generalizes
the in-tree kimi `~/.kimi-code/bin` + exit-code special-cases to all agents):

- shared: AcpAgentInstance / AcpAgentInstanceSource / AcpAgentPreference; extend
  AcpAgentSettingsEntry with instances/activeCommand/enabled/preference (keeps
  the #643 durable/runtime fields — Decision 1).
- acp-instance-resolver.ts: pure precedence override → selectedPath → first
  (borrowed from PwrSnap; single source of truth for badge + spawn).
- acp-instance-discovery.ts: maps kit groups → per-registryId instances + the
  active command; injectable discover fn for tests.
- 9 unit tests (resolver precedence + adapter mapping/override/selected/empty).

Not yet wired into the settings IPC / UI / store — next Phase A steps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): swap ACP discovery to the kit's multi-install path (Phase A 1-3)

The settings/store/registry/#643-capability-probe/chat-launch pipeline now
sources its AcpInstalledAgentRecords from the kit's multi-install discovery
instead of the in-tree per-agent probes.

- acp-instance-discovery.ts: discoverLocalAcpAgentRecords() — kit groups →
  AcpInstalledAgentRecord[] (drop-in for the old discoverLocalAcpAgents),
  resolving the active instance (override → picked → first) into
  launchDescriptor.command so chat spawns the binary shown as "Using", and
  carrying instances/activeCommand.
- acp-registry-types.ts: record gains instances/activeCommand.
- ipc/settings.ts: listInstalledAndLocalAcpAgents calls the kit-backed builder;
  grok/qwen CLI overrides flow through as overridePath preferences; the entry
  builder surfaces instances/activeCommand. The #643 freshness gate + store +
  registry merge are unchanged.
- Tests: settings-ipc retargeted to the new module + the preferences call
  shape; record-builder + resolver unit tests (11).

Single-install in-tree acp-local-discovery.ts is now unused (retired in step 5).
Full per-area suites green; full-suite failures are the known flaky-under-load
contention (backend-registry/messaging pass in isolation — 221/221), not ACP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(desktop): regenerate THIRD_PARTY_LICENSES for agent-acp + agent-core

Phase A added @pwrdrvr/agent-acp@0.9.2 + @pwrdrvr/agent-core@0.1.3 (both MIT)
and their transitives @zed-industries/agent-client-protocol@0.4.5 (Apache-2.0)
and a second zod@3.25.76 (MIT). Regenerated via `pnpm licenses:generate` so
the release/lint license gate (`pnpm licenses:check`) passes. All licenses are
on the always-allowed list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…untime-minor-patch group across 1 directory (#636)

* chore(deps-dev): bump electron

Bumps the desktop-runtime-minor-patch group with 1 update in the / directory: [electron](https://github.com/electron/electron).


Updates `electron` from 41.7.0 to 41.7.1
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](electron/electron@v41.7.0...v41.7.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 41.7.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: desktop-runtime-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps-dev): refresh Electron license notice

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Harold Hunt <harold@pwrdrvr.com>
…into AI Providers (#659)

* feat(desktop): extend acpAgents cliPath override to all four agents (step 4 schema)

Foundation for the multi-install ACP settings card. The per-agent CLI path
override (previously grok + qwen only) now covers gemini + kimi too, so the
"manual path" / pick-install action works for every agent. Reuses the existing
cliPath field as the override (no separate selectedPath needed — picking an
install just writes its command as the override, which the kit probes first).

- desktop-settings-env.ts: ACP_AGENTS_{GEMINI,KIMI}_CLI_PATH_ENV.
- shared settings contracts: acpAgents snapshot + patch gain gemini + kimi.
- desktop-config.ts: raw config type, patch→TOML write, TOML→config read, the
  config-pruning check; new generic resolveAcpCliPathOverride(registryId)
  replacing the grok/qwen-specific resolvers at the call site.
- desktop-settings-service.ts: parse gemini + kimi cliPath via resolveString.
- ipc/settings.ts: build discovery preferences for all four agents.
- Renderer test fixtures updated to the 4-agent acpAgents shape.

typecheck clean; settings-service 45, settings/ipc 18 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): multi-install ACP card + consolidate into "AI Providers" (step 4 UI)

Rebuilds the ACP settings to PwrSnap's multi-install card and merges it into the
model screen as one "AI Providers" pane (no separate "ACP Agents" nav item).

- AcpAgentsSettings.tsx: rewritten card. Per agent: "N installs found · active
  vX · auto/pinned" + a row per discovered install with a "Using" badge (the
  active one) or a "Use" button (pins it by writing its command as the cliPath
  override) + a manual-path input (Save/Clear). Generic onCliPathChange(registryId,
  path) replaces the grok/qwen-specific handlers. Keeps the StrictMode-guarded
  load + Refresh (force re-probe). Reads instances/activeCommand from Phase A.
- SettingsScreen.tsx: drop the `agents` SettingsSection member + nav item + pane;
  rename `models` → "AI Providers"; render the ACP card under that pane; generic
  cliPath patch by registryId.
- Tests updated to the consolidated nav + the multi-install card (install rows,
  Using/Use, pin-writes-cliPath). desktop-renderer settings 93 pass; typecheck clean.

Remaining: card CSS polish; retire in-tree acp-local-discovery.ts (step 5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(desktop): install-row styles for the multi-install ACP card

Token-based styles for the new per-install rows: .settings-acp-instances list,
.settings-acp-instance row (path + meta + action), and the green "Using" badge
(--success-soft/--success-text, matching the active-install indicator). Passes
lint:colors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(desktop): chat-launch discovery via the kit; retire acp-local-discovery (step 5)

The chat/backend path (acp-backend-adapter) had its own in-tree
`discoverLocalAcpAgents` call — the last consumer keeping the hand-rolled
discovery alive. Migrate it to the kit-backed `discoverLocalAcpAgentRecords`
with overrides resolved for all four agents (generic resolveAcpCliPathOverride),
so the binary chat launches is the resolved active install and every agent's
cliPath override is honored — consistent with Settings.

- acp-backend-adapter.ts: default discovery impl now uses the kit builder.
- Delete apps/desktop/src/main/acp/acp-local-discovery.ts + its test (the kit's
  strategies/probes/wellKnownAgentBinDirs fully replace them, incl. #641/#645).
- backend-registry.test.ts: retarget the discovery mock to acp-instance-discovery.
- shared settings.test.ts fixture: 4-agent acpAgents shape (caught by the
  all-package `pnpm lint` typecheck).

backend-registry/adapter/settings-ipc/instance suites 259 pass; `pnpm lint` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): one collapsible section per ACP agent; hide xAI Grok unless experimental

Per review feedback on the AI Providers screen:

- AcpAgentsSettings now renders each discovered agent as its OWN
  SettingsSection (Gemini / Grok / Kimi / Qwen headings) inside a
  SettingsSectionStack, with a status chip in the header — same collapsible
  treatment as the Codex section. This also fixes the missing spacing (the
  previous single bare SettingsSection lived outside any stack, so it had no
  section spacing/registration). Each section body shows the install summary,
  the install rows (Using/Use), and the manual-path override.
- ModelsSettings: the "Grok" (x.ai API key) section — only used by the
  experimental AgentCore-Grok backend — is now hidden unless
  experimental.agentCoreGrok is on, and retitled "Grok (xAI API)" to
  disambiguate from the ACP Grok agent. Normal users see only the ACP Grok.

typecheck clean; desktop-renderer settings 93 pass; lint:colors green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): ACP agents as Codex-styled sibling sections (drop the "ACP" wrapper)

Per review: the ACP agents now look exactly like the Codex section, not a nerdy
"ACP agents" sub-block jammed under it.

- AcpAgentsSettings returns a FRAGMENT of per-agent SettingsSections (Gemini /
  Grok / Kimi / Qwen), each rendered INSIDE ModelsSettings' own
  SettingsSectionStack — so they're siblings of the Codex section with identical
  spacing, eyebrow ("Models"), collapsible header + status chip.
- Each section uses the same primitives as Codex: SettingsField rows + the
  shared SettingsPathRow for the install list (path + source/version chips +
  "Using"/"Use"), a Manual-path field, and a Re-probe field. No bespoke
  card/instance markup; no "ACP" eyebrow or group header.
- ModelsSettings renders <AcpAgentsSettings> after Codex/Grok and takes
  onAcpCliPathChange; SettingsScreen passes it through (no separate ACP render).
- Tests updated to the section/path-row structure. desktop-renderer settings 93
  pass; typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): per-agent enabled toggle for ACP providers

Add an on-by-default Enable/disable switch to each ACP provider
(Gemini/Grok/Kimi/Qwen) on the AI Providers settings page. Disabling
hides the agent from the chat-launch discovery path so it never
surfaces as a launchable backend.

- Config: acp_agents.<id>.enabled (TOML), parsed to snapshot
  acpAgents.<id>.enabled (plain boolean, defaults true).
- Backend: acp-backend-adapter filters disabled agents out of the
  chat-launch discovery default impl via resolveAcpAgentEnabled.
- UI: SettingsSwitch 'Enabled' field per agent section; section chip
  reads 'Disabled' when off. Threaded onAcpEnabledChange through
  SettingsScreen -> ModelsSettings -> AcpAgentsSettings.
- Tests: service round-trip (default-enabled + disable persists);
  snapshot fixtures updated with enabled: true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): always show the 4 ACP providers; single config read

Address review feedback on the AI Providers screen:

- Always render every supported provider (Gemini/Kimi/Grok/Qwen) as its
  own section, even when undiscovered. The list is now seeded from the
  kit's BUILT_IN_ACP_STRATEGIES catalog in listAcpAgentSettingsImpl, so
  an undiscovered provider shows a 'Not installed' status instead of
  vanishing — independent of registry availability (offline/cold start).
  Entries are ordered by the catalog (gemini, kimi, grok, qwen).
- acpStatusLabel returns 'Not installed' for not-installed entries; the
  renderer's empty/error fallback no longer says 'ACP agents'.
- Single config read: resolution of cliPath overrides + enabled now
  reads/parses the config TOML once per discovery pass via
  readDesktopSettingsConfigSafe + acpCliPathOverrideFor /
  acpAgentEnabledFor, instead of one disk read per agent lookup. The
  single-shot resolveAcp* helpers delegate to these.
- Tests: settings-ipc asserts the always-4 placeholder list on a
  cache-only read; new renderer test for a 'Not installed' provider
  section; updated fallback-string assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…v-safe exit (#661)

* fix(desktop): provision boot-requested profile name in onboarding

Booting with PWRAGENT_PROFILE=test (or --profile test), completing the
onboarding wizard in Shared mode, then relaunching re-fired the wizard
forever.

Root cause: the single-profile bootstrap finish paths (Shared mode and
'Skip and use default') hardcoded the provisioned profile name to
'default', ignoring the env/CLI-pinned name. Boot resolves the pinned
env/CLI name FIRST (before the registry default), so the next launch
looked for a 'test' profile dir, found only 'default', and fell back to
missing-named-profile -> wizard, every time.

Fix: name the provisioned profile after the boot-requested name when one
was pinned, via a new exported bootstrapProvisionProfileName(bootInfo)
helper used by both paths. Falls back to 'default' only for the unnamed
first-run (no-profile-configured) case. The Isolated/Multiple paths
already seeded the requested name as their naming default; only
Shared/skip had the gap.

Adds bootstrapProvisionProfileName.test.ts (5 cases) guarding the rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(desktop): exit with a clear hint after onboarding graduation in dev

Under `pnpm dev`, the post-graduation auto-switch spawns a second
Electron instance that can't re-attach to electron-vite's single Vite
dev server, so it never reports alive — `waitForProfileAlive` times out
and the bootstrap window is left open (looks broken).

In dev + bootstrap mode, skip the doomed spawn-and-wait entirely: log a
clear message telling the operator the profile is ready and how to open
it (relaunch into it — it's now the registry default), then quit. The
profile + secrets + config are already graduated by this point, so the
next launch boots straight in. Production builds relaunch reliably and
keep the spawn-and-switch path.

Adds minimal `import.meta.env` typing to the renderer's vite-env.d.ts
(it doesn't pull in the full vite/client types).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(desktop): restore Gemini thread history on reload (use rollout, not session/load)

Gemini (and any ACP agent without sessionHistoryReplay) lost its entire
conversation on a process restart — the reopened thread showed only
Gemini's <session_context> boilerplate as a lone user message.

Root cause: a read/write capability mismatch in the ACP replay path.
The WRITE path (acp-client.appendHistoryUpdate) persists our durable
rollout.jsonl only when the agent does NOT support sessionHistoryReplay
— so Gemini/Grok/Qwen rollouts capture every turn. But the READ path
(AcpBackendAdapter.readReplay) decided whether to trust the agent's
session/load replay using acpRuntimeSupportsSessionLoad instead. Gemini
advertises loadSession=true (it can resume) but does NOT replay history
on session/load — it only re-emits <session_context> as a single user
message. readReplay trusted that 1-entry provider replay (entries>0, so
no fallback) and dropped the real conversation living in our rollout.
Grok worked only because it lacks loadSession and already used rollout.

Fix: gate readReplay's provider-replay-as-history on
acpRuntimeSupportsSessionHistoryReplay (matching the write path). Agents
that don't replay history (Gemini/Grok/Qwen) now read history from our
rollout; only agents that do (Kimi) use the provider replay. Session
resume for *continuing* the chat is unaffected — that happens separately
at turn time via client.ensureSession() in backend-registry.

Tests: new adapter test proving a Gemini-shaped agent (loadSession=true,
no sessionHistoryReplay) reads the full rollout history and never calls
session/load for history; verified it fails without the fix. Updated the
Kimi fallback test to set sessionHistoryReplay=true so it genuinely
exercises the provider-load -> empty -> rollout-fallback path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(desktop): prefer rollout over bogus provider replay (not gate session/load)

Refine the prior approach. Instead of skipping session/load entirely for
non-history-replay agents (which broke agents/E2E that legitimately source
history from the provider session/load replay when no rollout exists),
keep calling session/load (it also resumes the agent session) and make
providerReplayOrRolloutFallback prefer our durable rollout ONLY when it
actually has entries.

Result:
- Real Gemini reload: provider replay = bogus <session_context> (1 entry),
  rollout = full transcript -> return rollout. Fixed.
- Empty rollout (brand-new session, or E2E fake agent that replays via
  session/load): return the provider replay -> unchanged behavior.
- Kimi (sessionHistoryReplay=true): provider-first, unchanged.
- Grok/Qwen (no loadSession): rollout path, unchanged.

This keeps the 4 backend-registry ACP-replay tests and the
acp-runtime-modes E2E specs (empty-rollout, provider-sourced) green while
fixing the real Gemini-reload data loss. Updated the new adapter test:
session/load is still invoked (to resume) but its replay is discarded in
favor of the rollout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(desktop): filter Gemini <session_context> boilerplate from history

The prefer-rollout fix alone wasn't enough — two further problems made
Gemini reloads show only the <session_context> block (and would have
polluted the rollout over time):

1. session/load REPLAY pollutes the rollout. On reload Gemini re-emits
   its <session_context> environment block as a user_message_chunk, which
   flowed through applySessionUpdate -> appendHistoryUpdate and got
   appended to our durable rollout.jsonl. Every reload added another copy
   (confirmed on disk: a session_context record written 52s after the
   turn finished).
2. <session_context> was surfaced as a real user message by the
   normalizer, so even a clean rollout would render the environment block
   as if the human had pasted a directory tree.

Fix: treat the Gemini <session_context> block as agent boilerplate
everywhere.
- New isAcpSessionContextBoilerplate() helper.
- AcpSessionReplayNormalizer drops user_message_chunks that are the
  boilerplate (fixes display, including ALREADY-polluted rollouts on read
  — no need to scrub existing data).
- AcpRolloutStore.shouldPersistUpdate skips them (stops future pollution).

Validated against a real on-disk rollout: the reloaded thread now renders
[user prompt, assistant answer] with the full answer preserved and no
session_context. Confirmed live in the app on a previously-created thread.

Tests: normalizer drops the boilerplate; rollout store neither persists
nor replays it (reload-pollution guard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kimi surfaced TWO overlapping access-mode dropdowns: the hardcoded
Default/Full Access execution modes (kimi+grok only) AND its own advertised
runtime modes (Default/Plan/Auto/Yolo). They mirrored each other (picking
Yolo flipped the other to Full Access), and the legacy /yolo slash command
the hardcoded modes drive is rejected by current kimi.

Fix: when an agent advertises its OWN runtime mode selector, defer to it as
the single source of truth and suppress the hardcoded execution modes —
matching how Gemini already works (one dropdown).

- New acpAdvertisesRuntimeModeSelector() mirrors the renderer's
  getAcpRuntimeModeControl gating EXACTLY (configOptions category 'mode'
  with >=2 values, OR ACP SessionModeState modes.availableModes with >=2).
  Kimi's modes arrive via the latter, so a configOptions-only check would
  have missed them and left the duplicate.
- buildAcpExecutionModes() returns [] when a runtime mode selector exists.
- usesKimiSlashExecutionModes() returns false in that case, so kimi never
  sends the rejected /yolo; approval policy is the runtime 'yolo' mode set
  over the standard session/set_mode protocol method.
- grok (no runtime mode selector) keeps its /always-approve execution modes.

Tests: execution modes suppressed for kimi advertising runtime modes via
either configOptions or SessionModeState; kept for single-mode/no-mode kimi
and for grok; registry no longer sends /yolo once kimi advertises modes.

Note on Kimi yolo support: mainline kimi-cli advertises only the 'default'
ACP mode and its set_session_mode asserts mode_id=='default' (rejects
yolo). MoonshotAI/kimi-cli#2364 (open) adds yolo advertisement + a real
session/set_mode — which is the mechanism this fix routes through. So a
kimi build containing that PR is required for yolo-via-ACP; PwrAgent is now
ready for it with no further changes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(eval): live smoke harness for real-agent end-to-end checks

Add `pnpm eval:smoke` — a local, hands-off end-to-end check that drives
the REAL app (no mocked app-server) with your installed agents and prints
a pass/fail grid for Codex + every installed ACP agent.

Per backend it runs a small matrix via the real preload IPC:
  1. 'What is this project?'  (Default Access) → expect a non-empty answer
  2. 'Build the project'      (Default Access) → expect >=1 approval request
  3. 'Run one unit test'      (Full Access)    → expect the turn to complete

Design:
- Isolation via PWRAGENT_HOME (throwaway profile root), NOT HOME — so
  Codex (~/.codex) and the ACP agents (~/.gemini, ~/.qwen, ~/.kimi-code,
  ~/.grok) keep their real auth and your real thread list is untouched.
- Launches with no PWRAGENT_REPLAY_FIXTURE_PATH so the real app-server
  runs (the e2e mock is gated on that env var).
- Clones the repo at a pinned SHA into a throwaway PwrAgent-Test dir so
  full-access command runs never touch the real checkout.
- Drives window.pwragent.* via page.evaluate (startThread/setExecutionMode/
  startTurn/readThread/onAgentEvent/submitServerRequest) instead of fragile
  DOM selectors; auto-approves permission requests with the shipping
  buildPendingRequestResponse so the decision shape never drifts.
- Turns on protocol capture (PWRAGENT_PROTOCOL_CAPTURE) so every run banks
  raw ACP/Codex transcripts for the Phase B / KTD-P3 replay harness.

Local-only and deliberately OUT of CI: eval/ sits outside the app's
typecheck include + the depcruise boundary scan, so it never affects the
gate. Typecheck it with `pnpm --filter @pwragent/desktop eval:typecheck`.

See apps/desktop/eval/README.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(eval): focus each thread as it runs so it renders live

The eval creates threads via IPC (startThread), which doesn't auto-
navigate the renderer the way clicking 'Start thread' does — so threads
stacked up unfocused and never rendered their transcript / approval UI.

Focus each thread right after creation by pushing the existing
window:show-thread channel from the main context (Playwright
electronApp.evaluate) — the same path the app's menu / deep-link
navigation uses (onShowThreadRequested -> navigation.showThread). Now
each thread switches into view and streams live, and approval prompts
render, while the backend assertions are unchanged.

Documents in the README that the pre-thread launchpad/settings UI isn't
animated (threads are created via IPC, not the composer) and that a
heavier DOM-driven composer mode could be added if desired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(eval): full failure diagnostics (named step + untruncated error)

The grid clipped errors at 48 chars, so the kimi full-access failure
showed only 'Error invoking remote met…' — undiagnosable.

Now each cell tracks which IPC step it's on (startThread / startTurn /
waitForTurn …) so a thrown error names the failing call, prints the full
untruncated error (stack) to stderr, and any non-pass cell dumps the turn
status + the distinct notification methods seen (so e.g. codex's
'no approval' is explained by what it actually did).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(eval): UI-drive mode (EVAL_DRIVE_UI) + eval:smoke:build:ui

Add an optional mode that drives the real composer instead of the IPC,
so the eval validates what a user actually sees: it opens the new-thread
launchpad for the clone, asserts the Provider dropdown offers the agent
and selects it, asserts the Access-mode dropdown offers both Default +
Full Access and selects the scenario's mode (accepting the 'Enable Full
Access?' dialog), types the prompt, and clicks Start thread — then
discovers the created threadId via listThreads and observes the turn as
usual.

Selectors are role/name based and mirror the passing e2e specs. Every
step is labeled; on any UI failure it screenshots to the captures dir and
falls back to IPC creation so the grid always completes. Grid notes are
tagged [UI] / [IPC] / [IPC(fallback)].

Scripts: pnpm eval:smoke:ui (no rebuild) and pnpm eval:smoke:build:ui
(build + UI-drive). Default eval:smoke stays IPC-drive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(eval): UI-drive hard-fails (no IPC fallback) + correct Directories tab

Two fixes from the first UI-drive run:

- The sidebar tabs are role=tab, not button — openLaunchpad clicked the
  wrong role, so the Directories tab never activated and the launchpad
  button never rendered (15s timeout). Use getByRole('tab', {name:
  'Directories'}).
- A UI-drive failure now FAILS the cell (screenshot + rethrow) instead of
  silently falling back to IPC — the fallback masked exactly the 'control
  not present/reachable' problems UI-drive exists to catch. Grid tags are
  now just [UI] or [IPC].
- Key the launchpad button on the registered directoryLabel (returned from
  registerDirectoryFromDisk) rather than assuming the clone's basename.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(eval): UI-drive handles both Access-mode and Agent-mode controls

The full UI-drive run failed for gemini/kimi/qwen at selectAccessMode:
those ACP agents don't render the generic 'Access mode' dropdown. The
composer shows 'Access mode' only when the backend summary exposes
executionModes (codex, grok); ACP agents whose runtime exposes a mode
config option instead render an 'Agent mode' dropdown (the permission/
approval-mode control), where full-access is implied by an elevated agent
mode rather than a literal 'Full Access' entry. (Kimi worked solo because
its runtime wasn't probed yet at launch, so it fell back to 'Access
mode'; switching to it in the full run surfaced 'Agent mode'.)

selectAccessMode now waits for EITHER control, drives whichever is
present, asserts its options are offered, and for full-access on an
'Agent mode' control picks the most-elevated option (by keyword, else the
last/most-permissive) — accepting the Full Access dialog if it appears.
Logs which control + the offered/selected modes. Adds a settle after the
provider switch so the re-render lands before we read the control.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(eval): optional clone setup (EVAL_SETUP_ENV/EVAL_SETUP_CMD)

Install deps once in the clone before any cell so agents don't each burn
tokens running pnpm install. Two knobs:
- EVAL_SETUP_ENV=<name>: run a repo Codex environment's setup.script,
  read from <clone>/.codex/environments/*.toml via the app's own
  listCodexEnvironmentOptions (the env ships in the repo, so it's in the
  clone). e.g. EVAL_SETUP_ENV=PwrAgnt.
- EVAL_SETUP_CMD="<cmd>": run an explicit command (e.g. pnpm install).

Runs in a login shell in the clone cwd (PATH/pnpm resolve), streamed,
EVAL_SETUP_TIMEOUT_MS-bounded (default 15m). Codex environments are
Codex-only at runtime, but running the setup here benefits ALL backends
since they share the clone cwd. Default off (fast runs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ents 1-2) (#665)

* feat(desktop): KTD-P3 normalizer parity harness (Phase B / B1, increment 1)

The lossless-replay proof that gates the Phase B turn-lifecycle
migration: replay the same ACP session/update transcript through both
PwrAgent's in-tree AcpSessionReplayNormalizer and the kit's
AcpSessionNormalizer, and assert they render the same transcript. Must
stay green before any in-tree normalizer is deleted.

The two normalizers have different output models (in-tree returns a
coalesced AppServerThreadReplay; the kit emits NormalizedThreadEvent
deltas the caller must fold). agent-core ships primitives but no reducer,
so this adds the two reusable Phase B artifacts:

- normalized-thread-reducer.ts: reduceNormalizedThread(events) ->
  NormalizedThread (folds kit deltas: messages, tool-call activities via
  mergeToolCall, plans, status).
- normalized-thread-to-replay.ts: normalizedThreadToReplay(thread) ->
  AppServerThreadReplay (bridge to today's renderer/persistence).

Parity test (desktop-main, runs in CI) feeds BOTH real normalizers and
compares a canonicalized transcript (ids/timestamps stripped). Increment
1 covers assistant message streaming + turn lifecycle — both agree.
Roadmap (tools, plans, files, reasoning, real captured fixtures per agent)
in docs/plans/2026-06-07-001-feat-ktdp3-normalizer-parity-harness-plan.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): KTD-P3 parity increment 2 — tool/plan structural parity + cataloged divergences

Expand the harness: ACP content-block chunks, a read tool call, and a
plan-then-tool-call. STRUCTURAL parity holds (entry types + order,
activity summary + status, plan steps, per-detail label + count).

The harness immediately surfaced real field-level divergences in the
kit's tool-call normalization — the first concrete KTD-P3 findings:
  1. read tool location 'path' is dropped (kit has no locations/path on
     NormalizedToolCall; folds the title into command.displayCommand).
  2. command conflation: kit sets command.displayCommand for non-command
     tools; in-tree only emits a command detail with real command/output.
  3. tool-kind inference differs (in-tree toolDetailKind vs kit
     inferToolKind) for updates without an explicit kind.
These are cataloged in the plan doc as B2-gating reconciliations (kit fix
or documented accepted-loss before deleting acp-session-normalizer.ts);
the detail comparison is scoped to structural (label) parity until then.

Also fixed one adapter bug found by the harness: the in-tree keeps tool
status on the activity, not the detail, so the adapter no longer sets
detail.status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): KTD-P3 parity on a real captured transcript + full-corpus finding

Wire a real ACP transcript from the smoke eval into the parity suite: a
redacted Gemini 'build' turn (available-commands, a thought, 4 tool calls
+ 7 updates, 8 message chunks) in
fixtures/acp-transcripts/gemini-build.json. Structural parity holds.

The big result, recorded in the plan doc: a full-fidelity parity sweep
over ALL four ACP agents' real captures (Gemini/Grok/Kimi/Qwen, 11
diverging sessions, 90 differing entries) found:
  - ZERO entry-count mismatches (identical structure + order everywhere),
  - ZERO message divergences (all assistant/user text byte-identical),
  - every divergence is an activity detail field — i.e. ONLY the three
    already-cataloged tool-detail findings (read path, command conflation,
    tool-kind inference). No new divergence classes, no structural drift,
    no content loss.

So on real transcripts the kit reproduces the in-tree transcript
losslessly except for three bounded, known fields — the core KTD-P3
reassurance that the Phase B swap is safe once those are reconciled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): KTD-P3 — all-agent real fixtures, command shim, upstream issue, honest catalog

Reconcile the tool-detail findings and broaden real-data coverage:

- Real captured transcripts for ALL four ACP agents (redacted) wired into
  the parity suite: fixtures/acp-transcripts/{gemini,grok,kimi,qwen}-build.json.
  Transcript-level parity holds for every agent.
- CORRECTION: gemini's simple reads made the per-detail divergence look like
  ~2 clean fields; the richer agents (grok/kimi/qwen) show it spans the whole
  tool-detail object — kind inference, command-display formatting, AND the
  dropped path. The parity tests now assert transcript-level parity (messages
  byte-identical, entry structure/order, activity summary + status + detail
  count) and scope out the per-detail fields, which are the reconciliation
  surface. Plan doc corrected accordingly.
- Adapter shim (reconciles the command-conflation finding): emit a command
  detail only with real command evidence (rawCommand/output/exitCode),
  mirroring the in-tree rule, so a conflated read title no longer looks like a
  command.
- Filed the genuine information-loss finding upstream:
  pwrdrvr/agent-kit#1 (AcpSessionNormalizer drops tool-call file locations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (B1) (#668)

A sibling change fixed agent-kit#1 (AcpSessionNormalizer dropped tool-call
file locations). Consume it:

- Bump @pwrdrvr/agent-acp ^0.9.2 -> ^0.11.0 and @pwrdrvr/agent-core
  ^0.1.3 -> ^0.2.0 (adds NormalizedToolCall.locations). Regenerated
  THIRD_PARTY_LICENSES.
- normalized-thread-to-replay.ts reads the file path from
  tool.locations[0].path. agent-acp 0.11.0 also stopped conflating a
  read's title into command, so the adapter surfaces command faithfully
  (the earlier shim is gone).
- The parity canon is STRENGTHENED to assert per-detail label + path
  (previously scoped out). path now matches on real data for all four
  agents — the path-loss finding is verified resolved end-to-end.

Remaining scoped-out tool-detail fields (cataloged, OPEN, not
information-losing): tool-kind inference differences and
command.displayCommand derivation — including the in-tree's spurious
"tool call update" command for Qwen's tool_call_update, which the kit
correctly omits. Plan doc updated; findings #1/#2 marked resolved.

All 8 parity cases green; pnpm lint clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

* docs(plan): Decision 2 — Codex path for Phase B (B-full vs B-wrap)

Resolve the deferred Decision 2 with a quantified head-to-head, grounded
in a read of both PwrAgent's in-tree Codex client (5,918-line client.ts +
1,685 LOC of Codex-only environments) and the kit's agent-client
(CodexThreadClient 523 / ChatThreadController 965 / normalize 416).

Key findings:
- Renderer contract is already identical for Codex + ACP (AgentBackend
  abstraction is natural), and the codex-app-server-protocol version
  matches exactly (0.133.0) — no migration risk there.
- BUT the kit does NOT cover PwrAgent's rich Codex features (steering,
  compaction, review, fastMode, model/skills/account queries). PwrSnap —
  the reference consumer — is a simple chat surface that exercises none of
  them, so it is not proof the rich path survives a kit swap.
- The two goals (delete a ton / don't break) therefore pull opposite ways
  for the one big deletable artifact (client.ts).

Recommendation: staged B-full (hybrid). Adopt the kit for the common path
+ unify Codex/ACP behind AgentBackend, GATED by extending B1's parity
harness to Codex; keep the rich orchestration as thin seams and upstream
the missing primitives to agent-kit (the agent-kit#1 loop that just
worked). Pure B-full = breakage; pure B-wrap = adds code + forks the kit.

Includes blast-radius/LOC/risk tables, the coverage-gap matrix, and three
open questions for sign-off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(plan): record Decision 2 outcome — B-full via agent-kit-first + parity gates

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Version-align the kit deps to the latest published:
- @pwrdrvr/agent-acp        ^0.11.0 -> ^0.12.0  (^0.11.0 didn't allow 0.12.0)
- @pwrdrvr/agent-transport  ^0.1.5  -> ^0.1.6
(agent-core is already ^0.2.0.)

Also drop the unused @pwrdrvr/agent-client dependency that had been added
here -- nothing on this branch imports it (the AcpAgentClient class is
local to apps/desktop), and it only dragged codex-discovery@0.1.3 into the
dependency tree and the license file.

Deliberately NOT deduping zod. The lingering zod@3.25.76 comes from
@zed-industries/agent-client-protocol@0.4.5 (Zed's ACP lib, pulled
transitively by agent-acp). Forcing it to 4.3.6 via pnpm.overrides is a
breaking major (zod 3 -> 4) on a third-party lib and looks incompatible,
so accept the isolated dual-zod (pnpm keeps 3.x for Zed's lib, 4.x
everywhere else) until Zed/agent-acp move to zod 4 upstream.

Regenerated THIRD_PARTY_LICENSES; licenses + boundaries gates green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_KIT

First launchable slice of the Phase-B Codex swap. When PWRAGENT_CODEX_KIT=1,
PwrAgent drives the live Codex thread through the kit's CodexThreadClient
(@pwrdrvr/agent-client) instead of the in-tree CodexAppServerClient.

- New CodexKitBackendClient (codex-kit-adapter.ts) implements the registry's
  BackendClient surface over CodexThreadClient: startThread, startTurn, stream,
  readThread (via the B1 reduceNormalizedThread + normalizedThreadToReplay
  adapter), interruptTurn, close.
- codexEventToNotifications maps the kit's neutral NormalizedThreadEvents to the
  AppServerNotification shapes the registry + renderer already consume live
  (turn lifecycle, agent-message deltas/completion, tool-call items, token
  usage, errors). Pure + unit-tested.
- Gated at one seam (backend-registry.ts construction); flag-off keeps the full
  in-tree client byte-for-byte unchanged. Rich Codex features (steer/compact/
  review/model+skill+account queries) are intentionally unavailable under the
  flag in v1 — they're optional on BackendClient, so omitting them is clean.

Scope is the core path you can actually launch and run a Codex turn on. The
feature port + full client deletion follow. Built green: typecheck, desktop
build, 6 new adapter tests, 230 backend-registry tests, boundary lint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that agent-client's CodexThreadClient has steer/compact/review/trust/
permissions (agent-kit branch claude/codex-feature-port), expose them through
CodexKitBackendClient so the PWRAGENT_CODEX_KIT path is no longer core-chat-only:

- steerTurn → kit turn/steer (maps AppServerTurnInputItem[] → UserInput[])
- compactThread → kit thread/compact/start
- startReview → kit review/start
- setThreadPermissions → kit thread/resume overlay
- trustProject → kit config/value/write

LOCAL BRIDGE (pending publish): the root pnpm override points @pwrdrvr/agent-client
at a local tarball built from the agent-kit feature-port branch
(file:/Users/huntharo/.../agent-kit/.worktrees/codex-port/.pack/...0.6.1.tgz),
because the ported methods aren't published yet. To run:
  PWRAGENT_CODEX_KIT=1 pnpm --filter @pwragent/desktop dev
Before merge: publish agent-client with these methods, bump the apps/desktop pin,
and drop the override — CI stays red until then (expected for this draft).

Built green locally against the linked build: desktop typecheck, build, 6 adapter
tests, 230 backend-registry tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants