feat(desktop): runnable Codex-on-agent-kit swap behind PWRAGENT_CODEX_KIT#679
Closed
huntharo wants to merge 685 commits into
Closed
feat(desktop): runnable Codex-on-agent-kit swap behind PWRAGENT_CODEX_KIT#679huntharo wants to merge 685 commits into
huntharo wants to merge 685 commits into
Conversation
* 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.
* 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>
huntharo
force-pushed
the
claude/codex-kit-swap
branch
from
June 8, 2026 19:51
6a28eed to
a9837de
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft / WIP tracker — the Phase-B Codex swap: PwrAgent's live Codex thread driven through the kit's
CodexThreadClient(@pwrdrvr/agent-client) behindPWRAGENT_CODEX_KIT=1, instead of the in-treeCodexAppServerClient. Flag-off keeps the in-tree client byte-for-byte unchanged (A/B-able).What runs through the kit now
CodexThreadClient(agent-kit PR feat(agent-client): port Codex steer/compact/review/trust/permissions to CodexThreadClient agent-kit#7).CodexKitBackendClient(codex-kit-adapter.ts) implements the registry'sBackendClientsurface overCodexThreadClient;codexEventToNotificationsmaps the kit's neutralNormalizedThreadEvents → theAppServerNotificationshapes the registry + renderer consume live. Gated at one seam (thecodexClientconstruction inbackend-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
pnpmoverride points@pwrdrvr/agent-clientat a tarball built from the agent-kit feature-port branch. CI is red until agent-kit#7 lands, a version is published, theapps/desktoppin is bumped, and the override is dropped. Expected for the draft.Run it (local bridge in place)
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