Skip to content

feat: provider extension surface — dialogs, dynamic commands, rich parts, provider registry - #131

Merged
saucam merged 2 commits into
mainfrom
feat/provider-extension-surface
Jul 9, 2026
Merged

feat: provider extension surface — dialogs, dynamic commands, rich parts, provider registry#131
saucam merged 2 commits into
mainfrom
feat/provider-extension-surface

Conversation

@saucam

@saucam saucam commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What

Wire-additive groundwork for supporting additional agent backends (the pi harness is next) at full fidelity. Four gaps closed, one refactor:

1. Provider-initiated dialogs (session.ui_request / session.ui_response / session.ui_resolved)

A provider (or one of its extensions) can now ask the user something that is not a tool approval — confirm gates, pick-one lists, free text, multi-line editors. Semantics:

  • Daemon-enforced timeoutMs (auto-cancel); clients only display the countdown
  • Pending requests re-delivered on session.attach (a dialog raised while nobody watched still gets answered)
  • First answer wins across clients; session.ui_resolved dismisses every other copy
  • interrupt() / destroy() cancel pending dialogs; the turn-stall watchdog pauses while a dialog is pending (same rule as tool approvals)
  • Gated on a new ui.dialogs capability — the daemon only targets clients that declared it
  • Web UI: new UiRequestBar above the prompt (same slot family as ApprovalBar)
  • Provider seam: TurnOpts.requestUserInput (optional, so existing providers/tests compile unchanged)

2. Dynamic provider commands (session.commands)

Providers can expose their slash-command catalog (extension commands, prompt templates, skills) via a new optional AgentProvider.listCommands(). Invocation needs no new verb: clients send "/name args" as plain prompt text and the provider expands it. parseSlash gains an isProviderCommand passthrough option (built-ins always win); the web UI prefetches the catalog on session focus. Gated on the commands.dynamic capability.

3. Rich parts, actually rendered + ButtonPart return path

  • New custom_message provider event: standalone provider-authored messages with ContentPart[] (plain-text fallback in content), persisted + replayed like any message
  • The web UI finally renders parts[] (PartsView: code, diff, table, tree, progress, image, anchor, button) — previously the parts system was transported but never rendered anywhere
  • New session.part_action verb activates a ButtonPart: the daemon validates the button exists on the real message (clients can't mint arbitrary provider calls), then forwards to the provider's optional handlePartAction

4. Provider-declared approval forms (tool_start.patchableKeys)

Generalizes the hardcoded AskUserQuestion updatedInput whitelist: any backend can declare which input keys a client may patch on approval. The built-in AskUserQuestion rule remains the fallback; undeclared keys are still dropped.

5. ProviderRegistry wired in

Session backends now come from a factory registry built once at daemon startup — the previous ProviderRegistry was dead code next to a hardcoded switch in Session. Adding a backend is one register() call. Unknown provider ids keep the warn-and-fall-back behavior so resume survives metas written by newer codeoids.

Compatibility

Everything is additive: PROTOCOL_VERSION unchanged, new message kinds/fields follow the ignore-unknown discipline, new behaviors are capability-gated (ui.dialogs, commands.dynamic), and legacy clients (Telegram, older TUIs) are untouched. The companion codeoid-ui (Rust TUI) PR consumes the same surface.

Tests

  • session-extension-surface.test.ts — dialog lifecycle (broadcast gating, answer, timeout, interrupt, attach re-delivery), custom_message persistence, patchableKeys sanitization (declared keys pass, foreign keys dropped, legacy fallback intact), part-action validation, command listing
  • session-manager-extension-verbs.test.ts — scope enforcement + wire shapes for the three new verbs
  • provider-registry.test.ts — registration invariants, resolve fallback, default catalog
  • Protocol schema fidelity/coverage tests extended (compile-time exhaustiveness held the line)
  • Web: UiRequestBar, PartsView (incl. javascript: anchor sanitization and button dispatch), ui-requests + commands stores
  • Full suites green: 1146 daemon/core/protocol tests, 126 web tests, tsc + biome + eslint clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added provider-driven UI dialogs, richer message parts, and button actions in the session view.
    • Added support for provider commands, including command lookup and slash-command passthrough.
    • Expanded provider capabilities handling, with fallback behavior for unknown provider IDs.
  • Bug Fixes

    • Improved approval patch handling so only allowed fields can be updated.
    • Added safer handling for rich content, links, and image rendering.
    • UI dialogs now persist, re-deliver, and time out more reliably across session changes.

…rts, provider registry

Wire-additive groundwork for non-Claude backends to expose their full
feature set through codeoid (pi harness lands on top of this):

- session.ui_request/ui_response/ui_resolved: generic provider-initiated
  dialogs (select/confirm/input/editor) with daemon-enforced timeouts,
  attach re-delivery, first-answer-wins, interrupt cancellation, and
  stall-watchdog integration; ui.dialogs capability + web UiRequestBar
- session.commands: provider slash-command catalogs; clients pass
  catalogued verbs through as prompt text (parseSlash isProviderCommand)
- custom_message provider events + PartsView: ContentPart[] finally
  rendered in the web UI; session.part_action gives ButtonPart its
  return path (validated against the real message parts)
- tool_start.patchableKeys: provider-declared approval-form whitelist
  generalizing the hardcoded AskUserQuestion case
- ProviderRegistry (factories) wired into session creation, replacing
  the hardcoded provider switch; unknown ids still fall back for resume

All additive: PROTOCOL_VERSION unchanged, legacy clients unaffected.
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@saucam, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d1328fd6-462a-4180-b696-e9aa2814ba5c

📥 Commits

Reviewing files that changed from the base of the PR and between 429a471 and 1c83c9c.

📒 Files selected for processing (7)
  • packages/protocol/src/schemas.test.ts
  • packages/protocol/src/schemas.ts
  • src/daemon/providers/registry.ts
  • src/daemon/session.ts
  • src/tests/provider-registry.test.ts
  • web/src/state/commands.test.ts
  • web/src/state/commands.ts
📝 Walkthrough

Walkthrough

This PR adds a provider extension surface: provider-initiated UI dialogs, dynamic slash-command discovery, rich renderable content parts with button actions, and provider-declared patchable approval keys. A new factory-based ProviderRegistry replaces hardcoded backend construction, wired through SessionManager/Session, with matching web-side state and components.

Changes

Provider Extension Surface

Layer / File(s) Summary
Protocol messages and schemas
packages/protocol/src/types.ts, packages/protocol/src/schemas.ts, packages/protocol/src/schemas.test.ts, src/tests/protocol.test.ts, CHANGELOG.md
Adds UI_DIALOGS/DYNAMIC_COMMANDS capabilities, UI text/option limits, new ClientMessage/DaemonMessage variants for UI request/response, part actions, and command catalogs, plus corresponding Zod schemas and test coverage.
Provider interface and registry
src/daemon/providers/interface.ts, src/daemon/providers/registry.ts, src/daemon/providers/mock/session-provider.ts, src/tests/provider-registry.test.ts
Extends AgentProvider with requestUserInput, listCommands, handlePartAction, patchableKeys, and custom_message; replaces the instance-based registry with a factory-based ProviderRegistry and createDefaultProviderRegistry() with fallback resolution.
Session manager wiring and capability negotiation
src/daemon/server.ts, src/daemon/session-manager.ts, src/daemon/scrollback.ts, src/tests/session-manager-extension-verbs.test.ts
Advertises new capabilities during handshake, propagates client capabilities, shares the registry across session creation paths, and routes session.ui_response/session.part_action/session.commands.
Session UI dialog, custom message, and patchable approval logic
src/daemon/session.ts, src/tests/session-extension-surface.test.ts
Implements dialog lifecycle (requestUserInput, resolveUiRequestFromClient), custom_message handling, patchableKeys-based approval sanitization, and watchdog/cleanup integration.
Core slash-command provider passthrough
packages/core/src/slash.ts, packages/core/src/slash.test.ts
Adds ParseSlashOptions.isProviderCommand so unmatched verbs return null instead of throwing when a provider predicate matches, preserving built-in precedence.
Web provider command catalog and slash passthrough
web/src/state/commands.ts, web/src/state/commands.test.ts, web/src/components/prompt/PromptBox.tsx, web/src/state/connection.ts
Adds a lazily-fetched, per-session command catalog store and wires it into prompt slash parsing and connection routing.
Web UI request bar and dialog state
web/src/state/ui-requests.ts, web/src/state/ui-requests.test.ts, web/src/components/transcript/UiRequestBar.tsx, web/src/components/transcript/UiRequestBar.test.tsx, web/src/components/CenterPane.tsx
Adds a pending-dialog store and a UiRequestBar component supporting confirm/select/input/editor methods with countdown, rendered in CenterPane.
Web rich parts rendering and button actions
web/src/components/transcript/PartsView.tsx, web/src/components/transcript/PartsView.test.tsx, web/src/components/transcript/MessageRow.tsx
Adds PartsView rendering of text/code/diff/tree/button/progress/image/anchor/table parts with sanitized links and a session.part_action dispatch on button click, wired into MessageRow.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Daemon
  participant Session
  participant Provider
  Provider->>Session: requestUserInput(UiRequest)
  Session->>Client: session.ui_request (capable clients only)
  Client->>Daemon: session.ui_response
  Daemon->>Session: resolveUiRequestFromClient
  Session->>Provider: settle UiResponse
  Session->>Client: session.ui_resolved
Loading
sequenceDiagram
  participant Client
  participant Daemon
  participant Session
  participant Provider
  Client->>Daemon: session.part_action(messageId, action, data)
  Daemon->>Session: dispatchPartAction
  Session->>Provider: handlePartAction(action, data)
  Provider-->>Session: result
  Session-->>Daemon: ok / not_found / invalid_request
  Daemon-->>Client: response
Loading

Possibly related issues

Possibly related PRs

  • saucam/codeoid#38: Builds directly on the multi-provider plumbing (provider interface/registry/session integration) introduced there, extending it with provider commands, part actions, and UI/dialog and patchable approval handling.
  • saucam/codeoid#102: Implements the typed auth handshake and capability negotiation that this PR extends with new capability identifiers and additional session/UI/command message handling.
  • saucam/codeoid#115: Introduces the packages/core/src/slash.ts parseSlash implementation that this PR extends with isProviderCommand passthrough support.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main additive provider-extension changes: dialogs, dynamic commands, rich parts, and the provider registry.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/provider-extension-surface

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.15%. Comparing base (8ac9c11) to head (1c83c9c).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #131      +/-   ##
==========================================
+ Coverage   79.49%   80.15%   +0.66%     
==========================================
  Files          90       90              
  Lines       15251    15587     +336     
==========================================
+ Hits        12124    12494     +370     
+ Misses       3127     3093      -34     
Flag Coverage Δ
daemon 80.15% <100.00%> (+0.66%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/core/src/slash.ts 97.61% <100.00%> (+0.01%) ⬆️
packages/protocol/src/schemas.ts 100.00% <100.00%> (ø)
packages/protocol/src/types.ts 100.00% <100.00%> (ø)
src/daemon/providers/interface.ts 100.00% <ø> (ø)
src/daemon/providers/mock/session-provider.ts 100.00% <100.00%> (ø)
src/daemon/providers/registry.ts 100.00% <100.00%> (+56.25%) ⬆️
src/daemon/scrollback.ts 100.00% <100.00%> (ø)
src/daemon/session-manager.ts 64.53% <100.00%> (+2.28%) ⬆️
src/daemon/session.ts 88.03% <100.00%> (+2.30%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/tests/provider-registry.test.ts (1)

31-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the fixture to actually prove which field feeds backingSessionId.

makeInit() uses the same value ("s1") for both sessionId and initialBackingId, so this test can't distinguish whether the gemini/openai factories wire backingSessionId from init.sessionId or init.initialBackingId — a future refactor could swap the field silently and this test would still pass.

♻️ Proposed fix
 function makeInit(store: Store): ProviderSessionInit {
   return {
     sessionId: "s1",
     workspaceId: "ws1",
     model: null,
-    initialBackingId: "s1",
+    initialBackingId: "backing-s1",
     store,
   };
 }

Also applies to: 83-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tests/provider-registry.test.ts` around lines 31 - 39, The `makeInit`
fixture in `provider-registry.test.ts` currently uses the same value for
`sessionId` and `initialBackingId`, so the gemini/openai factory assertions
can’t verify which field actually populates `backingSessionId`. Update
`makeInit()` to use distinct values for `sessionId` and `initialBackingId`, then
adjust the related expectations in the provider factory tests so they explicitly
prove `backingSessionId` comes from the intended field in the `gemini` and
`openai` factory paths.
src/daemon/providers/registry.ts (1)

102-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log level mismatch: resolve() uses console.error for an expected fallback, but the docstring calls it a "warn".

The comment states unknown provider ids "warn and fall back to the default rather than throw" — this is normal forward-compat behavior (e.g. resume against an older defaultId), not a real error. Using console.error here risks false-positive alerting in production log-monitoring pipelines that key off stderr/error-level output for real incidents.

♻️ Proposed fix
   resolve(id: string | undefined, logContext: string): ProviderFactory {
     const requested = id ?? this.defaultId;
     const factory = this.#factories.get(requested);
     if (factory) return factory;
-    console.error(
+    console.warn(
       `[codeoid/${logContext}] unknown provider "${requested}" — falling back to ${this.defaultId}`,
     );
     return this.getOrThrow(this.defaultId);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/daemon/providers/registry.ts` around lines 102 - 115, `resolve()` in
`Registry` is treating the expected unknown-provider fallback as an error by
logging with `console.error`, which conflicts with the docstring’s
warn-and-fallback behavior. Change the logging in `resolve(id, logContext)` to a
warning-level log for the unknown provider case, keeping the same fallback to
`this.getOrThrow(this.defaultId)`, and preserve the existing context/message
format so forward-compat resume behavior still works without triggering error
monitoring.
web/src/state/commands.ts (1)

27-59: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Transient failures are cached as permanently-empty, same as "unsupported".

.catch() caches [] unconditionally on any rejection — network blip, WS timeout (timeoutMs: 8_000), or an older daemon rejecting the verb are all treated identically. Since nothing besides invalidateCommands() (only called today after rotate) clears the cache, a transient failure on the first fetch permanently disables provider-command passthrough for that session until the user rotates.

Consider not caching on transient/timeout-classified failures so the next ensureCommands call (e.g. next focus) retries, while still caching [] for a definitive "unsupported verb" rejection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/state/commands.ts` around lines 27 - 59, The ensureCommands flow is
caching every rejection as an empty command list, which makes transient failures
look like unsupported sessions. Update the catch handling in ensureCommands so
only a definitive unsupported-verb rejection stores [] in state.bySession, while
timeouts/network/transient errors simply clear inflight and allow a later
ensureCommands call to retry. Use the existing ensureCommands, getClient, and
invalidateCommands paths to keep the retry behavior aligned with session focus
changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/protocol/src/schemas.ts`:
- Around line 120-128: The session.ui_response schema currently allows value,
confirmed, and cancelled to appear together or all be missing, which breaks the
“exactly one payload field” contract. Update sessionUiResponseSchema in
schemas.ts by adding a refine/superRefine that enforces exactly one of those
three optional fields is present. Keep the validation tied to
sessionUiResponseSchema so SessionUiResponseMsg consumers get the
mutual-exclusivity check automatically.

In `@src/daemon/session.ts`:
- Around line 2683-2703: The custom_message handler is letting provider metadata
override the fixed provider message event tag. In session.ts within the switch
case for custom_message, update the object passed to this.#makeMessage so
event.metadata is spread before the fixed event: "provider.message" field,
keeping that tag authoritative regardless of metadata contents.

---

Nitpick comments:
In `@src/daemon/providers/registry.ts`:
- Around line 102-115: `resolve()` in `Registry` is treating the expected
unknown-provider fallback as an error by logging with `console.error`, which
conflicts with the docstring’s warn-and-fallback behavior. Change the logging in
`resolve(id, logContext)` to a warning-level log for the unknown provider case,
keeping the same fallback to `this.getOrThrow(this.defaultId)`, and preserve the
existing context/message format so forward-compat resume behavior still works
without triggering error monitoring.

In `@src/tests/provider-registry.test.ts`:
- Around line 31-39: The `makeInit` fixture in `provider-registry.test.ts`
currently uses the same value for `sessionId` and `initialBackingId`, so the
gemini/openai factory assertions can’t verify which field actually populates
`backingSessionId`. Update `makeInit()` to use distinct values for `sessionId`
and `initialBackingId`, then adjust the related expectations in the provider
factory tests so they explicitly prove `backingSessionId` comes from the
intended field in the `gemini` and `openai` factory paths.

In `@web/src/state/commands.ts`:
- Around line 27-59: The ensureCommands flow is caching every rejection as an
empty command list, which makes transient failures look like unsupported
sessions. Update the catch handling in ensureCommands so only a definitive
unsupported-verb rejection stores [] in state.bySession, while
timeouts/network/transient errors simply clear inflight and allow a later
ensureCommands call to retry. Use the existing ensureCommands, getClient, and
invalidateCommands paths to keep the retry behavior aligned with session focus
changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9da57bfa-90c9-4b77-9097-23bb4b91aa4f

📥 Commits

Reviewing files that changed from the base of the PR and between 8ac9c11 and 429a471.

📒 Files selected for processing (29)
  • CHANGELOG.md
  • packages/core/src/slash.test.ts
  • packages/core/src/slash.ts
  • packages/protocol/src/schemas.test.ts
  • packages/protocol/src/schemas.ts
  • packages/protocol/src/types.ts
  • src/daemon/providers/interface.ts
  • src/daemon/providers/mock/session-provider.ts
  • src/daemon/providers/registry.ts
  • src/daemon/scrollback.ts
  • src/daemon/server.ts
  • src/daemon/session-manager.ts
  • src/daemon/session.ts
  • src/tests/protocol.test.ts
  • src/tests/provider-registry.test.ts
  • src/tests/session-extension-surface.test.ts
  • src/tests/session-manager-extension-verbs.test.ts
  • web/src/components/CenterPane.tsx
  • web/src/components/prompt/PromptBox.tsx
  • web/src/components/transcript/MessageRow.tsx
  • web/src/components/transcript/PartsView.test.tsx
  • web/src/components/transcript/PartsView.tsx
  • web/src/components/transcript/UiRequestBar.test.tsx
  • web/src/components/transcript/UiRequestBar.tsx
  • web/src/state/commands.test.ts
  • web/src/state/commands.ts
  • web/src/state/connection.ts
  • web/src/state/ui-requests.test.ts
  • web/src/state/ui-requests.ts

Comment thread packages/protocol/src/schemas.ts Outdated
Comment thread src/daemon/session.ts
…horitative event tag, transient command-fetch retry

- session.ui_response schema now enforces exactly one of value/confirmed/
  cancelled (ambiguous payloads rejected with invalid_request)
- custom_message: spread provider metadata BEFORE the fixed
  event=provider.message tag so it can't be overridden
- web commands store: only permanent rejections (unknown verb / missing
  scope) cache as empty; transient failures retry on next focus
- registry.resolve logs the expected fallback at warn, not error
- provider-registry test fixture differentiates sessionId from
  initialBackingId to prove which feeds backingSessionId
@saucam
saucam merged commit 1e9c1fc into main Jul 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant