Skip to content

feat(browser): PR 7 — co-pilot mode (driving, annotations, capability prompts)#307

Merged
jaylfc merged 18 commits into
masterfrom
feat/browser-pr-7-copilot-driving
May 5, 2026
Merged

feat(browser): PR 7 — co-pilot mode (driving, annotations, capability prompts)#307
jaylfc merged 18 commits into
masterfrom
feat/browser-pr-7-copilot-driving

Conversation

@jaylfc
Copy link
Copy Markdown
Owner

@jaylfc jaylfc commented May 5, 2026

Summary

The "write half" of agent integration on top of PR 6's read half. Agents can now drive (scrollTo / click / type / focus / navigate) and annotate (highlight, sticky, parent-overlay cursor + arrows). Privileged ops (drive / navigate / see_cookies) require explicit user grant via a modal that pops on first use; grants are revocable in Settings. The driving state has obvious chrome — green banner with Pause/Take back, tinted iframe, tab-strip wash, brighter pulsing presence pill.

Architecture

  • Agent-side WS at /api/desktop/browser/copilot-agent?ticket=… — agent runtime connects, sends ops, receives acks. Same ticket store as the iframe side.
  • CopilotHub extended with _agent_conns registry + route_op_to_iframe (server→iframe) + route_ack_to_agent (iframe→agent) + notify_capability_needed (server→iframe→parent).
  • Capability enforcement in the agent-side receive loop: privileged ops checked against check_capability(host, permission); on miss → deny agent + notify iframe so the modal pops.
  • drive_sessions table tracks active driving with last_op_at for the 30s idle timeout. is_driving is the source of truth for "is this agent currently driving on this tab".
  • Frontend driving state: copilot.js forwards driving-state via postMessage; agent-ws-bridge sets local drivingState + arms a 30s decay timer. Chrome reads from store: CoPilotBanner, iframe tint, tab-strip wash, pill pulse-faster.
  • Annotations: in-iframe (highlight, sticky) drawn by copilot.js with data-taos-annotation-id for clear to find/remove. Parent-overlay (cursor, arrows) drawn by <AnnotationLayer /> SVG with pointer-events: none.

Files (24 changed, ~3500 lines)

Backend: drive_sessions schema + 5 store methods + 3 capability wrappers, copilot_agent_ws.py, capability_routes.py (GET/POST/DELETE), CopilotHub extensions, copilot.js (5 drive ops + 5 annotation ops + driving-state forwarding).

Frontend: CapabilityPromptModal, AgentCapabilitiesPanel, AnnotationLayer, CoPilotBanner, browser-capability-api.ts. Agent store extensions: drivingState + isAnyDriving + annotation actions. agent-ws-bridge handles driving-state and capability-needed events with 30s decay. Chrome / TabRenderer / TabStrip / AgentPresencePill render the new chrome states.

Test plan

  • Backend: pytest tests/routes/desktop_browser/ -q → 346 passed
  • Frontend: cd desktop && npx vitest run → 692 passed across 102 files
  • TypeScript: cd desktop && npx tsc --noEmit → zero errors
  • Manual: pin agent → simulate drive op (via WS test client) → modal pops → grant "this site (always)" → driving banner shows, iframe tints, tab-strip green wash, pill pulses faster → Pause flips to idle → 30s decay flips back to idle from server side → Take back revokes grant + unpins
  • Manual: Settings → Agent capabilities → revoke entry → next drive attempt re-prompts

Atomic invariants

  • Capability check is server-side — copilot.js executes ops unconditionally; the server gates which ops reach the iframe.
  • 30s idle decay enforced both server-side (drive_sessions.last_op_at + is_driving) and client-side (agent-ws-bridge setTimeout). Server is source of truth.
  • Agent ↔ iframe routing via (user, agent_id) agent connections + (user, profile, tab, agent) iframe connections in the same hub. PR 6's no-clobber design preserved.

Deferred to follow-up brainstorms

  • AgentsApp aggregated capabilities tab (cross-app — separate brainstorm per spec §12)
  • Real chat persistence in AgentPanel (still local-only — small follow-up PR)
  • Cursor follows user mouse during co-pilot (UX brainstorm)
  • Multi-step plan replay (agent runtime concern)

Spec / plan references

  • Spec: `docs/superpowers/specs/2026-05-03-browser-app-v2-design.md` §6.3 / §6.4 / §6.5 / §6.6
  • Plan: `docs/superpowers/plans/2026-05-05-browser-app-v2-pr-7-copilot-driving.md`

Summary by CodeRabbit

  • New Features
    • Agent capability management: view and revoke agent permissions in Settings
    • Co-pilot driving mode with visual indicators on tabs and browser area
    • Capability permission prompts allowing users to grant agent access with scope options (page, site session, always)
    • Annotation overlay system displaying agent cursors and arrows in real-time
    • Co-pilot banner showing active driving status with Pause and Take Back actions

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Warning

Rate limit exceeded

@jaylfc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 38 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d315f06-2955-48d6-a7f4-c1882199c40b

📥 Commits

Reviewing files that changed from the base of the PR and between 96b47de and 32c32d1.

📒 Files selected for processing (16)
  • desktop/src/apps/BrowserApp/AgentCapabilitiesPanel.tsx
  • desktop/src/apps/BrowserApp/AnnotationLayer.tsx
  • desktop/src/apps/BrowserApp/BrowserApp.tsx
  • desktop/src/apps/BrowserApp/CoPilotBanner.test.tsx
  • desktop/src/apps/BrowserApp/CoPilotBanner.tsx
  • desktop/src/apps/BrowserApp/SettingsPanel.tsx
  • desktop/src/apps/BrowserApp/TabStrip.tsx
  • desktop/src/stores/browser-agent-store.test.ts
  • tests/routes/desktop_browser/test_copilot_agent_ws.py
  • tests/routes/desktop_browser/test_drive_sessions.py
  • tinyagentos/routes/desktop_browser/capability_routes.py
  • tinyagentos/routes/desktop_browser/copilot.js
  • tinyagentos/routes/desktop_browser/copilot_agent_ws.py
  • tinyagentos/routes/desktop_browser/copilot_ws.py
  • tinyagentos/routes/desktop_browser/proxy.py
  • tinyagentos/routes/desktop_browser/store.py
📝 Walkthrough

Walkthrough

This PR implements an end-to-end agent capability and browser driving system. It adds permission grants for agents to control browser tabs, a capability request modal, a management panel, visual indicators (annotations, banners, tints), server-side drive-session tracking, and WebSocket infrastructure to bridge agent operations with iframe event handling.

Changes

Agent Capability & Driving System

Layer / File(s) Summary
Data Models & Types
desktop/src/stores/browser-agent-store.ts, desktop/src/lib/browser-capability-api.ts, tinyagentos/routes/desktop_browser/schema.py
Zustand store gains annotations and drivingState state with new types (Annotation, AnnotationCursor, AnnotationArrow); capability API exports CapabilityGrant interface; database schema adds drive_sessions table.
Store Actions & Backend Methods
desktop/src/stores/browser-agent-store.ts, tinyagentos/routes/desktop_browser/store.py
Store gets annotation/driving-state accessors (addAnnotation, clearAnnotations, setDrivingState, isAnyDriving); backend BrowserStore adds drive-session lifecycle (start_drive_session, bump_drive_session, end_drive_session, is_driving, prune_expired_drive_sessions) and capability checks (check_drive_capability, check_navigate_capability, check_see_cookies_capability).
REST & WebSocket Endpoints
tinyagentos/routes/desktop_browser/capability_routes.py, tinyagentos/routes/desktop_browser/copilot_agent_ws.py, tinyagentos/routes/desktop_browser/copilot_ws.py
New GET/POST/DELETE /api/desktop/browser/capabilities routes for listing, granting, and revoking permissions; new WebSocket agent handler that validates tickets, checks capabilities, routes ops to iframe, and manages drive sessions; CopilotHub gains agent connection tracking and op/ack routing.
Client-Side Bridge & JavaScript
desktop/src/apps/BrowserApp/agent-ws-bridge.ts, tinyagentos/routes/desktop_browser/copilot.js
Bridge recognizes and routes driving-state and capability-needed server events, triggering 30s auto-decay of driving state; copilot.js adds drive ops (scrollTo, click, type, navigate, focus), annotation ops (highlight, sticky, arrow, cursor, clear), and posts driving-state events on drive op execution.
Capability Request Modal
desktop/src/apps/BrowserApp/CapabilityPromptModal.tsx, desktop/src/apps/BrowserApp/CapabilityPromptModal.test.tsx
Listens for taos-browser:capability-prompt window event, renders permission request dialog, computes hostPattern and expiry based on user scope ("page", "session", "always"), calls grantCapability, handles errors and success dismissal.
Capability Management Panel
desktop/src/apps/BrowserApp/AgentCapabilitiesPanel.tsx, desktop/src/apps/BrowserApp/AgentCapabilitiesPanel.test.tsx, desktop/src/apps/BrowserApp/SettingsPanel.tsx
Nested panel within settings that lists grants in a table (agent name, host, permissions, expiry), resolves agent names from store, formats expires_at as human-readable strings, and provides revoke button with error handling.
Driving Status UI & Indicators
desktop/src/apps/BrowserApp/CoPilotBanner.tsx, desktop/src/apps/BrowserApp/CoPilotBanner.test.tsx, desktop/src/apps/BrowserApp/AgentPresencePill.tsx, desktop/src/apps/BrowserApp/TabRenderer.tsx, desktop/src/apps/BrowserApp/TabStrip.tsx
CoPilotBanner displays persistent status when agent is driving, with Pause and Take Back actions; AgentPresencePill shows three-state indicator (driving → bright green ping, watching → standard green pulse, idle → dark green); TabRenderer adds semi-transparent green overlay when driving; TabStrip marks tabs with green bottom border when driving.
Component Integration & Wiring
desktop/src/apps/BrowserApp/Chrome.tsx, desktop/src/apps/BrowserApp/BrowserApp.tsx, desktop/src/apps/BrowserApp/AnnotationLayer.tsx, desktop/src/apps/BrowserApp/AnnotationLayer.test.tsx
Chrome passes profileId to SettingsPanel and conditionally renders CoPilotBanner above nav when driving; BrowserApp mounts global CapabilityPromptModal; AnnotationLayer component renders SVG overlay for cursor/arrow annotations scoped per window+tab.
Tests & API Tests
desktop/src/apps/BrowserApp/*.test.tsx, desktop/src/lib/browser-capability-api.test.ts, desktop/src/stores/browser-agent-store.test.ts, tests/routes/desktop_browser/test_capability_routes.py, tests/routes/desktop_browser/test_copilot_agent_ws.py, tests/routes/desktop_browser/test_drive_sessions.py, tests/routes/desktop_browser/test_copilot_js.py
Comprehensive test coverage for capability flow (grant/revoke/list), driving state transitions with auto-decay, agent registration and cleanup, op routing and ack forwarding, drive-session CRUD and isolation, and JavaScript drive/annotation op presence; mocks and spies validate side effects.
Router Registration
tinyagentos/routes/desktop_browser/__init__.py
Side-effect imports register capability and copilot-agent-ws routes into the shared router.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser as Browser UI
    participant BrowserStore as Zustand Store
    participant Server as Backend
    participant Iframe as iFrame

    User->>Browser: Agent requests drive permission
    Browser->>Browser: Dispatch CustomEvent<br/>(taos-browser:capability-prompt)
    Browser->>+Browser: CapabilityPromptModal listens
    Browser-->>User: Show permission dialog<br/>(page/session/always)
    User->>Browser: Select "This site (always)"
    Browser->>+Server: POST /api/desktop/browser/capabilities<br/>(grant with null expiry)
    Server->>Server: Validate permissions<br/>Store grant
    Server-->>-Browser: 200 {"granted": true}
    Browser->>-Browser: Close modal, refresh grants
    
    Note over User,Iframe: Agent begins driving

    Iframe->>Server: WebSocket: Send drive op<br/>(click, type, etc.)
    Server->>Server: Validate capability grant<br/>(host_pattern matches)
    Server->>Server: Bump drive_session<br/>Set drivingState="driving"
    Server->>Browser: WebSocket event:<br/>driving-state (driving)
    Browser->>BrowserStore: Update drivingState<br/>Start 30s decay timer
    Browser->>Browser: Render CoPilotBanner<br/>Green tint overlay
    Server->>Iframe: Forward op to iframe
    Iframe->>Iframe: Execute DOM action<br/>(click, type, scroll)
    Iframe->>Browser: Post driving-state event<br/>(iframe-side confirmation)
    
    User->>Browser: Click "Take back" button
    Browser->>Server: POST revoke + DELETE session
    Server->>Server: Remove grant & session
    Browser->>BrowserStore: setDrivingState="idle"<br/>Cancel decay timer
    Browser->>Browser: Hide banner & tint
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • jaylfc/tinyagentos#303: Extends same BrowserApp components (Chrome, TabRenderer, SettingsPanel, TabStrip) to integrate driving state, co-pilot banner, and capability features.
  • jaylfc/tinyagentos#302: Modifies browser proxy and copilot surface (copilot.js, WebSocket logic, capability events, drive-session tracking) alongside this PR's backend changes.
  • jaylfc/tinyagentos#300: Adds foundational desktop_browser module structure (routes, schema, store patterns) that this PR builds upon with capability and drive-session features.

Poem

🐰 A warren of whiskers twitching with glee,
Grant agents a license to drive wild and free—
With capability clicks and permission sublime,
We've dotted each i and crossed every line.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.63% 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 and concisely describes the main feature addition: co-pilot mode with driving, annotations, and capability prompts. It maps directly to the core functionality implemented across the changeset.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/browser-pr-7-copilot-driving

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 and usage tips.

if (!iso) return "Never";
const ms = new Date(iso).getTime() - Date.now();
if (ms < 0) return "Expired";
const hours = Math.floor(ms / (60 * 60 * 1000));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Potential runtime error if iso is an invalid date string, new Date(iso).getTime() returns NaN, leading to NaN calculations and malformed expiry strings.

}

useEffect(() => {
load();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Missing error handling; if listCapabilities or listAgents reject, the promise rejection is unhandled, potentially leaving the UI in loading state.

return;
}
// Refresh list
const fresh = await listCapabilities(profileId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Missing error handling; if listCapabilities rejects, the promise is unhandled, potentially causing unhandled rejection.

useBrowserAgentStore.getState().setDrivingState(windowId, tabId, agentId, "idle");
};

const handleTakeBack = async () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Missing error handling; if revokeCapability or unpinAgent reject, the promises are unhandled, potentially leaving inconsistent state.


useEffect(() => {
function handler(e: Event) {
const ce = e as CustomEvent<CapabilityPromptDetail>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Unsafe type assertion; if the event is not a CustomEvent with the expected detail type, accessing properties later could cause runtime errors.

@@ -180,7 +198,7 @@ export function Chrome({ windowId }: ChromeProps) {
<Settings size={16} />
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Breaking API change; SettingsPanel now requires a profileId prop, which could cause runtime errors if this component is used elsewhere without it.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented May 5, 2026

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 3
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

WARNING

File Line Issue
desktop/src/apps/BrowserApp/CapabilityPromptModal.tsx 37 Unsafe type assertion; if the event is not a CustomEvent with the expected detail type, accessing properties later could cause runtime errors.
desktop/src/apps/BrowserApp/Chrome.tsx 199 Breaking API change; SettingsPanel now requires a profileId prop, which could cause runtime errors if this component is used elsewhere without it.
tinyagentos/routes/desktop_browser/copilot_agent_ws.py 85 Missing input validation for target_profile and target_tab; agent could supply None or empty strings, potentially causing runtime errors in store operations.

SUGGESTION

File Line Issue
tests/routes/desktop_browser/test_drive_sessions.py 194 Edge case not handled; if start_drive_session fails for the fresh session, the test may pass incorrectly assuming prune removes only one, but missing error handling could mask issues.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
tinyagentos/routes/desktop_browser/proxy.py 248 Logic bug - page-changed event broadcasts the initial request URL instead of the final URL after redirects, misleading agents about the current page.
Files Reviewed (35 files)
  • desktop/src/apps/BrowserApp/AgentCapabilitiesPanel.test.tsx - 0 issues
  • desktop/src/apps/BrowserApp/AgentCapabilitiesPanel.tsx - 0 issues
  • desktop/src/apps/BrowserApp/AgentPresencePill.tsx - 0 issues
  • desktop/src/apps/BrowserApp/AnnotationLayer.test.tsx - 0 issues
  • desktop/src/apps/BrowserApp/AnnotationLayer.tsx - 0 issues
  • desktop/src/apps/BrowserApp/BrowserApp.tsx - 0 issues
  • desktop/src/apps/BrowserApp/CapabilityPromptModal.test.tsx - 0 issues
  • desktop/src/apps/BrowserApp/CapabilityPromptModal.tsx - 1 issues
  • desktop/src/apps/BrowserApp/Chrome.tsx - 1 issues
  • desktop/src/apps/BrowserApp/CoPilotBanner.test.tsx - 0 issues
  • desktop/src/apps/BrowserApp/CoPilotBanner.tsx - 0 issues
  • desktop/src/apps/BrowserApp/SettingsPanel.test.tsx - 0 issues
  • desktop/src/apps/BrowserApp/SettingsPanel.tsx - 0 issues
  • desktop/src/apps/BrowserApp/TabRenderer.tsx - 0 issues
  • desktop/src/apps/BrowserApp/TabStrip.tsx - 0 issues
  • desktop/src/apps/BrowserApp/agent-ws-bridge.test.ts - 0 issues
  • desktop/src/apps/BrowserApp/agent-ws-bridge.ts - 0 issues
  • desktop/src/lib/browser-capability-api.test.ts - 0 issues
  • desktop/src/lib/browser-capability-api.ts - 0 issues
  • desktop/src/stores/browser-agent-store.test.ts - 0 issues
  • desktop/src/stores/browser-agent-store.ts - 0 issues
  • tests/routes/desktop_browser/test_capability_routes.py - 0 issues
  • tests/routes/desktop_browser/test_copilot_agent_ws.py - 1 issues (incremental)
  • tests/routes/desktop_browser/test_copilot_agent_ws.py - 0 issues
  • tests/routes/desktop_browser/test_drive_sessions.py - 1 issues
  • tinyagentos/routes/desktop_browser/__init__.py - 0 issues
  • tinyagentos/routes/desktop_browser/capability_routes.py - 0 issues
  • tinyagentos/routes/desktop_browser/copilot.js - 0 issues
  • tinyagentos/routes/desktop_browser/copilot_agent_ws.py - 1 issues
  • tinyagentos/routes/desktop_browser/copilot_ws.py - 0 issues
  • tinyagentos/routes/desktop_browser/proxy.py - 1 issues
  • tinyagentos/routes/desktop_browser/schema.py - 0 issues
  • tinyagentos/routes/desktop_browser/store.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by grok-code-fast-1:optimized:free · 494,357 tokens

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
desktop/src/apps/BrowserApp/BrowserApp.tsx (1)

78-126: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Render CapabilityPromptModal in the mobile branch as well.

The modal is currently mounted only in the non-mobile path (Line 145). In mobile layout, capability-needed events have no prompt UI, so users cannot grant permissions there.

Proposed fix
   if (isMobile) {
     return (
       <div className="flex flex-col h-full bg-shell-bg overflow-hidden relative">
@@
         <div className="flex items-center gap-1 px-2 py-1 bg-shell-surface border-t border-shell-border-subtle">
@@
         </div>
+        <CapabilityPromptModal />
       </div>
     );
   }
🤖 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 `@desktop/src/apps/BrowserApp/BrowserApp.tsx` around lines 78 - 126, The mobile
branch in BrowserApp (when isMobile is true) does not render the
CapabilityPromptModal, so capability-needed events have no UI; update the mobile
JSX path to also mount <CapabilityPromptModal ... /> (same props as used in the
non-mobile branch) so the modal appears on mobile—locate the mobile return block
in BrowserApp.tsx and add the CapabilityPromptModal component alongside
WindowChooser/TabRenderer so it uses the same handlers/props as the desktop
branch.
desktop/src/apps/BrowserApp/SettingsPanel.tsx (1)

51-57: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent Escape from closing both dialogs when the capabilities modal is open.

Line 53 always closes SettingsPanel. When capsOpen is true, Escape also triggers the child modal close, so both layers dismiss at once.

Proposed fix
   useEffect(() => {
     const handler = (e: KeyboardEvent) => {
-      if (e.key === "Escape") onClose();
+      if (e.key !== "Escape") return;
+      if (capsOpen) {
+        setCapsOpen(false);
+        return;
+      }
+      onClose();
     };
     window.addEventListener("keydown", handler);
     return () => window.removeEventListener("keydown", handler);
-  }, [onClose]);
+  }, [onClose, capsOpen]);
🤖 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 `@desktop/src/apps/BrowserApp/SettingsPanel.tsx` around lines 51 - 57, The
Escape key handler in SettingsPanel's useEffect always calls onClose, which lets
Escape dismiss both the panel and a child capabilities modal; change the handler
so it only calls onClose when capsOpen is false (i.e., if (e.key === "Escape" &&
!capsOpen) onClose()). Also add capsOpen to the useEffect dependency array so
the handler sees updates to that flag; reference the useEffect block, the
handler function, the capsOpen boolean, and onClose callback when making the
change.
🧹 Nitpick comments (4)
desktop/src/apps/BrowserApp/AgentPresencePill.tsx (1)

152-167: 💤 Low value

Consider whether animate-ping reads as "driving" or just looks busy.

animate-ping produces a single outward-expanding fade, not a faster pulse — visually it can look like a notification ping rather than an intensified watching state. If the goal is "more emphatic than watching", a brighter colour with the same animate-pulse (or a slightly larger dot) usually communicates that more clearly. Purely cosmetic; safe to defer to design review.

🤖 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 `@desktop/src/apps/BrowserApp/AgentPresencePill.tsx` around lines 152 - 167,
The presence-dot span currently uses animate-ping for isDriving which reads like
a notification ping; change it to use the same pulse animation with a brighter
color and optionally a slightly larger size to convey "more emphatic than
watching." Specifically, update the class array in the span
(data-testid="presence-dot") so the isDriving branch uses "bg-green-300
animate-pulse" (or similar brighter bg class) and optionally "w-2.5 h-2.5"
instead of animate-ping; leave the anyWatching branch as "bg-green-400
animate-pulse" and the idle branch unchanged.
desktop/src/stores/browser-agent-store.test.ts (1)

408-415: 💤 Low value

Test name vs. scenario mismatch.

The case name says "multiple driving agents" but only agent-b is driving (agent-a is set to idle). To genuinely exercise multi-driver behaviour, set both agents to "driving" and assert the result is one of the two:

-    s.setDrivingState("win-1", "tab-1", "agent-a", "idle");
+    s.setDrivingState("win-1", "tab-1", "agent-a", "driving");
     s.setDrivingState("win-1", "tab-1", "agent-b", "driving");

     const result = useBrowserAgentStore.getState().isAnyDriving("win-1", "tab-1");
-    expect(result).toBe("agent-b");
+    expect(["agent-a", "agent-b"]).toContain(result);

Otherwise the name is misleading — consider renaming to "isAnyDriving picks the driving agent when others are idle".

🤖 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 `@desktop/src/stores/browser-agent-store.test.ts` around lines 408 - 415, The
test title and scenario disagree: update the test around useBrowserAgentStore to
match intent — either rename the it() description from "isAnyDriving returns one
of multiple driving agents" to "isAnyDriving picks the driving agent when others
are idle" if you want to keep agent-a idle, or make both agents driving by
calling s.setDrivingState("win-1","tab-1","agent-a","driving") and
s.setDrivingState("win-1","tab-1","agent-b","driving") and change the assertion
to accept either agent (e.g.,
expect(["agent-a","agent-b"]).toContain(useBrowserAgentStore.getState().isAnyDriving("win-1","tab-1")));
ensure you modify the test that uses useBrowserAgentStore, setDrivingState and
isAnyDriving accordingly.
tinyagentos/routes/desktop_browser/copilot_ws.py (1)

281-285: 💤 Low value

Stale comment — ack handling is now in this PR.

The # Drive ack events come in PR 7. comment predates this change; ack routing is wired below at lines 329–334. Worth dropping the parenthetical to avoid future-reader confusion.

-# Allowed event kinds from iframe → server. Drive ack events come in PR 7.
+# Allowed event kinds from iframe → server. 'ack' is routed back to the agent runtime.
🤖 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 `@tinyagentos/routes/desktop_browser/copilot_ws.py` around lines 281 - 285, The
inline comment above _ALLOWED_EVENT_KINDS is stale; remove the parenthetical
"Drive ack events come in PR 7." and update the comment to simply state that
these are the allowed event kinds from iframe→server (e.g., replace the line
starting with "# Allowed event kinds..." so it no longer references PR 7), since
ack handling is already implemented in the copilot_ws routing (see
_ALLOWED_EVENT_KINDS and the ack routing code).
tests/routes/desktop_browser/test_copilot_agent_ws.py (1)

227-230: ⚡ Quick win

Replace fixed sleeps with predicate/event waits to reduce flaky WS tests.

Hardcoded sleep(0.05) makes these tests timing-sensitive under slow CI runners. A small wait helper that polls a concrete condition (e.g., mock await count) is more deterministic.

♻️ Example pattern
+def _wait_until(predicate, timeout_s=1.0, interval_s=0.01):
+    import time
+    deadline = time.time() + timeout_s
+    while time.time() < deadline:
+        if predicate():
+            return
+        time.sleep(interval_s)
+    raise AssertionError("condition not met before timeout")

         with ws_client.websocket_connect(
             f"/api/desktop/browser/copilot-agent?ticket={ticket}"
         ) as ws:
             ws.send_json(op_msg)
-            import time
-            time.sleep(0.05)
+            _wait_until(lambda: mock_iframe_ws.send_json.await_count == 1)

Also applies to: 268-270, 305-307, 572-574, 626-628

🤖 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 `@tests/routes/desktop_browser/test_copilot_agent_ws.py` around lines 227 -
230, Replace the fragile time.sleep(0.05) calls with a small polling wait helper
(e.g., wait_for(predicate, timeout=1.0, interval=0.01)) that waits until a
concrete condition is true; implement wait_for once in the test module and use
it where the code currently calls time.sleep(0.05) by passing a predicate that
checks the real signal (for example: lambda: my_mock.await_count >= expected or
lambda: mock_ws.send.call_count > 0), and swap each time.sleep(0.05) occurrence
(the literal time.sleep calls) to wait_for with an appropriate predicate and
timeout.
🤖 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 `@desktop/src/apps/BrowserApp/AgentCapabilitiesPanel.tsx`:
- Around line 33-44: Wrap the async load() with a request-version guard and
error handling: generate a local requestId (or capture current profileId) at
start of load(), run listCapabilities(profileId) and listAgents() inside
try/catch, and on success only call setGrants and setAgents if the captured
requestId/profileId still matches the latest one (or component is mounted) to
avoid stale overwrites; on error catch and handle the rejection (e.g., set an
error state or stop loading and log) so a rejected request doesn't leave the
panel stuck. Ensure the guard logic is coordinated with the existing
useEffect([profileId]) so each invocation of load() validates against the latest
profileId before mutating state.

In `@desktop/src/apps/BrowserApp/AnnotationLayer.tsx`:
- Around line 13-15: The marker ID and arrowhead fill are causing collisions and
wrong colors: replace the static MARKER_ID with a per-instance unique id (e.g.,
generate inside the AnnotationLayer component via React's useId or a uuid and
store as markerId) and update all references to use that markerId (both the
<marker id=...> and markerEnd="url(#...)" usages); also stop hardcoding the
arrowhead fill to DEFAULT_COLOR—use the annotation's color (falling back to
DEFAULT_COLOR) when rendering the <marker> so the arrowhead matches the stroke
color of the corresponding annotation. Ensure changes touch the
constants/variables around MARKER_ID, DEFAULT_COLOR and the AnnotationLayer
render logic that creates markers and sets markerEnd.

In `@desktop/src/apps/BrowserApp/CoPilotBanner.test.tsx`:
- Line 1: The test file imports vitest symbols as "import { describe, it,
expect, beforeEach, vi } from 'vitest';" but uses afterEach in the test body;
update the import to explicitly include afterEach so the file no longer relies
on globals. Locate the top-level import statement in CoPilotBanner.test.tsx and
add afterEach to the named imports from "vitest" (alongside describe, it,
expect, beforeEach, vi) so the test explicitly imports all used hooks.

In `@desktop/src/apps/BrowserApp/CoPilotBanner.tsx`:
- Around line 37-53: handleTakeBack currently aborts before flipping local
driving state if revokeCapability or unpinAgent throws; change it so the local
state is always cleared: call revokeCapability/unpinAgent inside a try/catch (or
try) and move useBrowserAgentStore.getState().setDrivingState(windowId, tabId,
agentId, "idle") into a finally block so it runs regardless of API failures;
keep removePinnedAgent and onTakeBack behavior but ensure setDrivingState
executes in finally (log errors from revoke/unpin rather than skipping the local
state flip).

In `@desktop/src/apps/BrowserApp/TabStrip.tsx`:
- Around line 104-107: Guard access to tab.pinnedAgentIds in the TabStrip
component's tabDriving logic to prevent crashes when older tab objects lack that
property: before iterating, ensure tab.pinnedAgentIds is an array (e.g.,
Array.isArray(tab.pinnedAgentIds) or default to []), then loop over it and check
s.drivingState[`${windowId}:${tab.id}:${aid}`] === "driving" as before and
return false if none match; update the useBrowserAgentStore selector that builds
tabDriving to use this safe fallback.

In `@tinyagentos/routes/desktop_browser/capability_routes.py`:
- Around line 44-50: The GrantRequest model accepts arbitrary expires_at strings
which later get ignored if malformed; update GrantRequest to validate expires_at
at grant time by either changing the field to datetime | None or adding a
pydantic `@validator` for expires_at that attempts to parse an ISO-8601 datetime
(or the expected format) and raises a validation error on failure; ensure the
validator normalizes the value (e.g., to timezone-aware UTC) and that any route
or function consuming GrantRequest (the grant handling code that reads
GrantRequest.expires_at) relies on the validated datetime type rather than raw
strings so invalid expiries are rejected upfront.

In `@tinyagentos/routes/desktop_browser/copilot_agent_ws.py`:
- Around line 41-53: The current _extract_host(msg) trusts agent-supplied
msg["host"], which must not be used for authorization; change the logic so
authorization uses an authoritative host value passed in from the server (e.g.
current_tab_host or validated_navigate_host) instead of msg["host"]. Modify
_extract_host to either accept an explicit trusted_host parameter (preferred) or
to only parse msg["url"] for display purposes (not auth), then update all
capability checks that call _extract_host (including the checks referenced
around the 91-101 region) to use the server-provided trusted host variable for
enforcement while continuing to optionally use msg.url/msg.host only for
UI/logging, not decision-making.
- Around line 85-90: The current per-op override of target_profile/target_tab
using target_profile = msg.get("profile_id", consumed.profile_id) and target_tab
= msg.get("tab_id", consumed.tab_id) bypasses the pin authorization that was
only checked for consumed.tab_id at connect time; update the message-handling
path to re-run the same pin/ownership check used at connect time whenever msg
contains a "profile_id" or "tab_id" override (i.e., detect when msg.get returns
a different value than consumed.profile_id/consumed.tab_id), and reject or
ignore the override (return error or revert to consumed values and log a
warning) unless that specific (profile, tab) is authorized for this agent;
ensure you reference the same authorization routine used at connect and apply it
to the computed target_profile/target_tab before proceeding with routing/drive
actions.

In `@tinyagentos/routes/desktop_browser/copilot.js`:
- Around line 85-95: The navigate function currently assigns untrusted args.url
directly to location.href; change it to validate and allow only http: and https:
schemes by parsing args.url (use the URL constructor with a base of
location.href to support relative URLs), check url.protocol is exactly 'http:'
or 'https:', and return an error for anything else; also wrap the URL parsing in
a try/catch to return { error: 'invalid url' } on parse failure before setting
location.href.

In `@tinyagentos/routes/desktop_browser/store.py`:
- Around line 689-707: Replace the read-then-delete loop with a single atomic
SQL delete: compute cutoff = (now -
timedelta(seconds=idle_timeout_s)).isoformat(), then run one await
self._db.execute("DELETE FROM drive_sessions WHERE last_op_at <= ?", (cutoff,))
instead of the initial SELECT and per-row DELETEs; commit, obtain the number of
deleted rows from the DELETE cursor (cursor.rowcount) and return that count.
Update references to datetime.fromisoformat/expired logic accordingly and remove
the per-row delete loop so freshly bumped sessions can't be deleted between
SELECT and DELETE.

---

Outside diff comments:
In `@desktop/src/apps/BrowserApp/BrowserApp.tsx`:
- Around line 78-126: The mobile branch in BrowserApp (when isMobile is true)
does not render the CapabilityPromptModal, so capability-needed events have no
UI; update the mobile JSX path to also mount <CapabilityPromptModal ... /> (same
props as used in the non-mobile branch) so the modal appears on mobile—locate
the mobile return block in BrowserApp.tsx and add the CapabilityPromptModal
component alongside WindowChooser/TabRenderer so it uses the same handlers/props
as the desktop branch.

In `@desktop/src/apps/BrowserApp/SettingsPanel.tsx`:
- Around line 51-57: The Escape key handler in SettingsPanel's useEffect always
calls onClose, which lets Escape dismiss both the panel and a child capabilities
modal; change the handler so it only calls onClose when capsOpen is false (i.e.,
if (e.key === "Escape" && !capsOpen) onClose()). Also add capsOpen to the
useEffect dependency array so the handler sees updates to that flag; reference
the useEffect block, the handler function, the capsOpen boolean, and onClose
callback when making the change.

---

Nitpick comments:
In `@desktop/src/apps/BrowserApp/AgentPresencePill.tsx`:
- Around line 152-167: The presence-dot span currently uses animate-ping for
isDriving which reads like a notification ping; change it to use the same pulse
animation with a brighter color and optionally a slightly larger size to convey
"more emphatic than watching." Specifically, update the class array in the span
(data-testid="presence-dot") so the isDriving branch uses "bg-green-300
animate-pulse" (or similar brighter bg class) and optionally "w-2.5 h-2.5"
instead of animate-ping; leave the anyWatching branch as "bg-green-400
animate-pulse" and the idle branch unchanged.

In `@desktop/src/stores/browser-agent-store.test.ts`:
- Around line 408-415: The test title and scenario disagree: update the test
around useBrowserAgentStore to match intent — either rename the it() description
from "isAnyDriving returns one of multiple driving agents" to "isAnyDriving
picks the driving agent when others are idle" if you want to keep agent-a idle,
or make both agents driving by calling
s.setDrivingState("win-1","tab-1","agent-a","driving") and
s.setDrivingState("win-1","tab-1","agent-b","driving") and change the assertion
to accept either agent (e.g.,
expect(["agent-a","agent-b"]).toContain(useBrowserAgentStore.getState().isAnyDriving("win-1","tab-1")));
ensure you modify the test that uses useBrowserAgentStore, setDrivingState and
isAnyDriving accordingly.

In `@tests/routes/desktop_browser/test_copilot_agent_ws.py`:
- Around line 227-230: Replace the fragile time.sleep(0.05) calls with a small
polling wait helper (e.g., wait_for(predicate, timeout=1.0, interval=0.01)) that
waits until a concrete condition is true; implement wait_for once in the test
module and use it where the code currently calls time.sleep(0.05) by passing a
predicate that checks the real signal (for example: lambda: my_mock.await_count
>= expected or lambda: mock_ws.send.call_count > 0), and swap each
time.sleep(0.05) occurrence (the literal time.sleep calls) to wait_for with an
appropriate predicate and timeout.

In `@tinyagentos/routes/desktop_browser/copilot_ws.py`:
- Around line 281-285: The inline comment above _ALLOWED_EVENT_KINDS is stale;
remove the parenthetical "Drive ack events come in PR 7." and update the comment
to simply state that these are the allowed event kinds from iframe→server (e.g.,
replace the line starting with "# Allowed event kinds..." so it no longer
references PR 7), since ack handling is already implemented in the copilot_ws
routing (see _ALLOWED_EVENT_KINDS and the ack routing code).
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 94fc4a1f-5894-47c6-9dc2-71af43c5109c

📥 Commits

Reviewing files that changed from the base of the PR and between 607a4ad and 96b47de.

📒 Files selected for processing (32)
  • desktop/src/apps/BrowserApp/AgentCapabilitiesPanel.test.tsx
  • desktop/src/apps/BrowserApp/AgentCapabilitiesPanel.tsx
  • desktop/src/apps/BrowserApp/AgentPresencePill.tsx
  • desktop/src/apps/BrowserApp/AnnotationLayer.test.tsx
  • desktop/src/apps/BrowserApp/AnnotationLayer.tsx
  • desktop/src/apps/BrowserApp/BrowserApp.tsx
  • desktop/src/apps/BrowserApp/CapabilityPromptModal.test.tsx
  • desktop/src/apps/BrowserApp/CapabilityPromptModal.tsx
  • desktop/src/apps/BrowserApp/Chrome.tsx
  • desktop/src/apps/BrowserApp/CoPilotBanner.test.tsx
  • desktop/src/apps/BrowserApp/CoPilotBanner.tsx
  • desktop/src/apps/BrowserApp/SettingsPanel.test.tsx
  • desktop/src/apps/BrowserApp/SettingsPanel.tsx
  • desktop/src/apps/BrowserApp/TabRenderer.tsx
  • desktop/src/apps/BrowserApp/TabStrip.tsx
  • desktop/src/apps/BrowserApp/agent-ws-bridge.test.ts
  • desktop/src/apps/BrowserApp/agent-ws-bridge.ts
  • desktop/src/lib/browser-capability-api.test.ts
  • desktop/src/lib/browser-capability-api.ts
  • desktop/src/stores/browser-agent-store.test.ts
  • desktop/src/stores/browser-agent-store.ts
  • tests/routes/desktop_browser/test_capability_routes.py
  • tests/routes/desktop_browser/test_copilot_agent_ws.py
  • tests/routes/desktop_browser/test_copilot_js.py
  • tests/routes/desktop_browser/test_drive_sessions.py
  • tinyagentos/routes/desktop_browser/__init__.py
  • tinyagentos/routes/desktop_browser/capability_routes.py
  • tinyagentos/routes/desktop_browser/copilot.js
  • tinyagentos/routes/desktop_browser/copilot_agent_ws.py
  • tinyagentos/routes/desktop_browser/copilot_ws.py
  • tinyagentos/routes/desktop_browser/schema.py
  • tinyagentos/routes/desktop_browser/store.py

Comment thread desktop/src/apps/BrowserApp/AgentCapabilitiesPanel.tsx
Comment thread desktop/src/apps/BrowserApp/AnnotationLayer.tsx Outdated
Comment thread desktop/src/apps/BrowserApp/CoPilotBanner.test.tsx Outdated
Comment thread desktop/src/apps/BrowserApp/CoPilotBanner.tsx
Comment thread desktop/src/apps/BrowserApp/TabStrip.tsx
Comment thread tinyagentos/routes/desktop_browser/capability_routes.py
Comment thread tinyagentos/routes/desktop_browser/copilot_agent_ws.py Outdated
Comment thread tinyagentos/routes/desktop_browser/copilot_agent_ws.py
Comment thread tinyagentos/routes/desktop_browser/copilot.js
Comment thread tinyagentos/routes/desktop_browser/store.py Outdated
# Allow the agent to target a specific (profile, tab) per op via msg fields,
# falling back to the ticket-bound (profile, tab). For PR 7 the typical agent
# only operates on the ticket's tab; cross-tab ops are out of scope.
target_profile = msg.get("profile_id", consumed.profile_id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Missing input validation for target_profile and target_tab; agent could supply None or empty strings, potentially causing runtime errors in store operations.

# Record authoritative current URL for this tab. Agent-side capability
# checks read from this rather than trusting agent-supplied msg["host"].
request.app.state.copilot_hub.set_tab_url(
user_id=user_id, profile_id=profile_id, tab_id=tab_id, url=url,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CRITICAL: Logic bug - set_tab_url records the initial request URL instead of the final URL after redirects, causing capability checks to use incorrect host.

# Using 0.6s so the test isn't flaky under heavy CI load (where the Python
# interpreter can pause for hundreds of ms between awaits — a tighter
# boundary would let the "fresh" session also slip past the cutoff).
await asyncio.sleep(0.6)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Potential race condition in test timing; asyncio.sleep(0.6) may not guarantee expiration under heavy system load, potentially causing intermittent test failures despite the comment's attempt to address CI flakiness.

# Now create the fresh session AFTER the sleep so its last_op_at is
# guaranteed within the timeout window when we prune.
await store.start_drive_session(
user_id="u1", profile_id="p1", tab_id="t1", agent_id="fresh-agent",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Edge case not handled; if start_drive_session fails for the fresh session, the test may pass incorrectly assuming prune removes only one, but missing error handling could mask issues.

@jaylfc
Copy link
Copy Markdown
Owner Author

jaylfc commented May 5, 2026

@coderabbitai full review — please re-review at f7abe37. Since your initial pass at 96b47de we addressed both critical security findings (server-tracked tab URL replaces agent-supplied host for capability checks; per-op tab override now re-checks pin), the 8 actionable items, and the 2 outside-diff items (mobile branch CapabilityPromptModal mount, Settings Esc dialog stack).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@jaylfc Thanks for the detailed summary of what was addressed! I'll kick off a full re-review of the PR at f7abe37, covering the security fixes (server-authoritative host for capability checks, per-op pin re-validation), the 8 actionable items, and the two out-of-diff fixes (mobile CapabilityPromptModal mount and Settings Esc dialog stack). Results will be posted shortly.

[full_review]
Re-review requested at f7abe37. Previous critical security findings (agent-supplied host replaced by server-tracked tab URL; per-op tab override re-checks pin) and all 8 actionable items confirmed addressed by the author. Please verify the mobile branch CapabilityPromptModal mount and the Settings Esc dialog stack fix as part of this pass.

@jaylfc
Copy link
Copy Markdown
Owner Author

jaylfc commented May 5, 2026

@coderabbitai full review — please re-review at 32c32d1. Since your initial pass at 96b47de: addressed 2 critical security findings (server-tracked tab URL replaces agent-supplied host; per-op tab override re-checks pin), 8 actionable items, 2 outside-diff items, and one Kilo CRITICAL (set_tab_url now uses final URL after redirects, not the initial requested URL).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@jaylfc Thanks for the comprehensive update! I'll kick off a full re-review of the PR at 32c32d1, taking into account all the fixes addressed since the initial pass at 96b47de:

  1. 2 critical security findings — server-tracked tab URL replacing agent-supplied host for capability checks, and per-op tab override re-checking pin.
  2. 8 actionable items from the prior review pass.
  3. 2 outside-diff items — mobile branch CapabilityPromptModal mount and Settings Esc dialog stack.
  4. Kilo CRITICALset_tab_url now uses the final URL after redirects rather than the initial requested URL.

Results will be posted shortly.

[full_review]
Re-review requested at 32c32d1. Since initial pass at 96b47de, the author has addressed: 2 critical security findings (server-tracked tab URL replaces agent-supplied host for capability checks; per-op tab override re-checks pin), 8 actionable items, 2 outside-diff items (mobile branch CapabilityPromptModal mount, Settings Esc dialog stack), and one Kilo CRITICAL (set_tab_url now uses final URL after redirects, not the initial requested URL). Please verify all fixes are correctly implemented and flag any remaining issues.

@jaylfc jaylfc merged commit dad1225 into master May 5, 2026
12 of 13 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