Skip to content

feat(ai): add tool use (function calling) to AI assistant#165

Merged
github-actions[bot] merged 36 commits into
developfrom
feature/ai-tool-use
Mar 18, 2026
Merged

feat(ai): add tool use (function calling) to AI assistant#165
github-actions[bot] merged 36 commits into
developfrom
feature/ai-tool-use

Conversation

@tomymaritano

@tomymaritano tomymaritano commented Mar 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • ai-core: Add ToolRegistry, runToolLoop orchestrator, chatWithTools() on AIService, tool_use SSE parsing in AnthropicProvider, and stop event with tool_use/tool_result ContentPart types
  • desktop (main): Register 6 built-in tools (search_notes, read_note, list_notebooks, create_note, insert_text, replace_selection) with confirmation flow and renderer-executed tool IPC bridge
  • desktop (renderer): ToolCallBlock component with status indicators/icons, tool call tracking in AiPanel, and renderer-side execution of editor tools (insert_text, replace_selection)

Tools

Tool Type Confirmation
search_notes Read (main) Auto
read_note Read (main) Auto
list_notebooks Read (main) Auto
create_note Write (main) Required
insert_text Write (renderer) Required
replace_selection Write (renderer) Required

Test plan

  • All 153 tests pass (pnpm test)
  • Typecheck clean (pnpm typecheck — main, preload, renderer)
  • Build succeeds (pnpm build)
  • Manual: open AI panel, send message with tools: true, verify tool calls render
  • Manual: approve/reject tool confirmation flow
  • Manual: verify insert_text and replace_selection modify the editor

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Streaming AI chat with multi-turn conversations and built-in note management tools (search, read, create, and editor actions)
    • Tool confirmation UI allowing users to approve/reject AI tool executions before they run
    • Enhanced chat interface with provider validation and improved request controls
  • Documentation

    • Added comprehensive AI architecture and tool-use implementation guides
  • Style

    • Updated visual color opacity tokens across UI components for consistency

tomymaritano and others added 30 commits March 13, 2026 18:49
## Summary

Release 0.9.0 — a major milestone with AI features, sync stability, and
a complete website redesign.

- **Website redesign** with shadcn/ui + Magic UI
- **Auth UX rethink** — Enable Sync flow + middleware fix (500 → 401)
- **Sync stability** — error propagation, exponential backoff, abort on
logout, typed token refresh
- **Sync onboarding** — prompt after 5 notes + offline queue visibility
- **AI Commands (Cmd+K v1)** — command panel, settings, keybindings
- **AI Knowledge (Cmd+K v2)** — RAG, ask notes, related context
- **AI Extensibility** — plugin API, presets import/export
- **API documentation**

## Version bumps

| File | From | To |
|------|------|----|
| `package.json` | 0.2.0 | 0.9.0 |
| `apps/desktop/package.json` | 0.8.0 | 0.9.0 |

## Checklist

- [x] Version bumped in root `package.json`
- [x] Version bumped in `apps/desktop/package.json`
- [x] CHANGELOG.md updated
- [x] Tests pass
- [ ] QA smoke test on macOS
- [ ] QA smoke test on Windows
- [ ] Tag `v0.9.0` after merge

## Post-merge

After merging to `main`:
1. Tag `v0.9.0` on main
2. Merge `main` back into `develop`
3. Delete `release/0.9.0` branch

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* AI Assistant panel with modeful interface (chat/ask-notes modes),
configurable via settings
  * AI command presets support (export/import functionality)
  * Sync status monitoring with error tracking and pending change count
  * Enable Sync onboarding flow for new users
  * Redesigned website with modernized design system
* Settings panel for AI configuration (API key, model selection, context
limits)

* **Bug Fixes**
  * Improved token handling for deep-link authentication
  * Better sync error classification and exponential backoff
  * Enhanced device limit error handling

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary

- Adds `auto-tag.yml` workflow that was created during v0.9.0
development but missed the squash merge to main
- Without this workflow, release PRs don't trigger automatic tag
creation, breaking the release pipeline

## Context

The release pipeline chain is: `release/* PR merge → auto-tag creates
vX.Y.Z → release.yml builds + publishes`

This workflow was in `release/0.9.0` but the squash merge of PR #149
didn't include it, so v0.9.0 never got tagged or released.

## After merge

Once this is on main, I'll manually create the `v0.9.0` tag to trigger
the release build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Release v0.9.0

### Highlights

- **Auth → Payment → Sync flow**: License-gated sync with smart step
routing in EnableSyncModal
- **Stripe checkout**: Monthly (€2/mo) and annual (€20/year) plan
selection
- **Sidebar drag-and-drop**: Grip handle, circular reference prevention,
child depth propagation
- **CI/CD**: Auto-deploy API — develop → staging, main → production
- **Design tokens**: SyncStatusIndicator uses CSS variables instead of
hardcoded colors
- **AI features**: Command execution, plugin bridge, knowledge system
(Phase 4-5)
- **UI cleanup**: Tailwind v4 canonical classes, removed dead
design-system package

### Changes since v0.8.0

- feat(sync): connect auth → payment → sync flow with license gating
(#154)
- chore: Tailwind v4 canonical classes + tsconfig cleanup (#151)
- feat(ai): complete Phase 4-5 — AI command execution + plugin bridge
(#150)
- feat(ai,plugins): complete Phase 4-5 — AI knowledge & extensibility
- feat(desktop,api): complete Phase 1-3 roadmap implementation

### Checklist

- [x] `pnpm typecheck` passes
- [x] `pnpm test` passes
- [x] `package.json` version is `0.9.0`
- [x] All features merged to develop first
- [x] API production secrets configured (Stripe, Resend)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

# Release Notes

* **New Features**
* Added AI-powered text commands (summarize, rewrite, tweet) with plugin
integration.
* Introduced drag-and-drop notebook reordering with automatic circular
reference protection.
  * Enhanced sync onboarding with license-based feature gating.
  * Improved magic link authentication with automatic verification.

* **Bug Fixes**
  * Fixed token refresh infinite loop handling.
  * Added URL encoding for verification links.
  * Improved cleanup for animated UI components.

* **Improvements**
  * Enhanced error handling for authentication and sync failures.
  * Added accessibility attributes (ARIA labels, semantic HTML).
  * Updated visual styling with new token-driven design system.

* **Refactor**
  * Migrated from design-system package to plugin-api package.
  * Consolidated design tokens and theme system.

* **Chores**
  * Updated GitHub Actions release automation.
  * Refreshed project documentation and contribution guidelines.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Includes:
- Design spec for provider-agnostic AI architecture
- Implementation plan with 12 tasks across 6 chunks
- Fix vercel.json cleanUrls for magic link 404

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…used imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements context assembly for LLM calls with token-aware budget management:
drops oldest history messages and truncates/excludes notes that exceed limits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements parseSSEStream as an async generator that handles chunked
Uint8Array streams, double-newline event boundaries, event/data field
parsing, comment skipping, and malformed JSON resilience.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements AnthropicProvider with full SSE streaming support, HTTP error
classification, usage token tracking, static model list, and config validation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…etry

Implements AIServiceImpl that wires together ProviderRegistry, buildContext,
and withRetry into a single chat() entry point returning an abortable
AsyncIterable<LLMEvent> handle with start/done envelope events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace legacy ai:query handler with ai-core-based streaming IPC:
- Add @readied/ai-core dependency, remove @readied/ai-assistant
- Create apps/desktop/src/main/ai/setup.ts: singleton AIService factory using ProviderRegistry + AnthropicProvider with net.fetch
- Create apps/desktop/src/main/ai/ipc-ai.ts: registerAIHandlers with ai:chat (streaming), ai:cancel, ai:validate, ai:exportPreset, ai:importPreset; 50ms text batching + per-window handle tracking
- Remove old registerAiHandlers() inline function from index.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace ai.query() with ai.chat(), ai.onEvent(), ai.cancel(), ai.validate().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace synchronous ai.query() calls with event-driven ai.chat() streaming.
Add provider field to settings schema (v3 migration), copy aiCommandTypes
to ai-core, and update all renderer imports from @readied/ai-assistant
to @readied/ai-core.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All functionality has been migrated to @readied/ai-core.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Merge duplicate AI panel systems (plugin + App.tsx) into single
  panel instance managed by App.tsx
- Plugin's Sparkles button now dispatches CustomEvent instead of
  managing its own panel, eliminating the duplicate panel that
  appeared inside the editor zone
- Fix AI panel CSS from fixed height (350px) to 100% to fill
  the side panel container properly
- Default to 'ask-notes' mode (RAG) when opening panel via ⌘K
- Make React DevTools a dynamic import so it stays out of
  production bundle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
lucide-react@0.562 ships types against @types/react@19.x, but the
desktop app uses React 18. pnpm hoisted the 19.x types from apps/web,
causing a type identity mismatch (bigint not in React 18 ReactNode).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Accept all apps/web changes from develop
- Move aiCommandTypes from ai-assistant into ai-core
- Fix test imports for renamed ai-command-types module
- Regenerate pnpm-lock.yaml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… pinning

- Add ai-core and other missing packages to structure diagram
- Document branch hygiene rules to prevent merge conflict buildup
- Add AI architecture section (streaming flow, provider pattern, key files)
- Document @types/react pinning via pnpm.overrides

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-abstraction' into feature/ai-tool-use

# Conflicts:
#	.claude/settings.local.json
#	apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx
#	apps/web/app/(marketing)/changelog/page.tsx
#	apps/web/app/(marketing)/download/page.tsx
#	apps/web/app/(marketing)/pricing/page.tsx
#	apps/web/app/docs/[[...slug]]/page.tsx
#	apps/web/components/Footer.tsx
#	apps/web/components/Navbar.tsx
#	apps/web/components/landing/Hero.tsx
#	apps/web/components/landing/VideoGuides.tsx
#	apps/web/components/magicui/animated-shiny-text.tsx
#	apps/web/components/magicui/border-beam.tsx
#	apps/web/components/magicui/hero-video-dialog.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… event

Extends LLMEvent union with stop reason and ContentPart with tool_use/tool_result
variants to support tool use in conversation history.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pure TypeScript tool registry for managing tool registrations with
execute callbacks and confirmation requirements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ropicProvider

Adds content_block_start/delta/stop handling for tool_use blocks with
JSON accumulation, and emits stop event with reason from message_delta.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… use

Async generator that manages provider chat → tool execution → re-send
cycle with abort signal support, max round-trips, and error handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ntegration

Adds ToolChatRequest, ToolChatHandle, and chatWithTools() that wraps
runToolLoop with start/done lifecycle events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ion flow

Adds tool registry to setup, tool-aware ai:chat handler with executeTool
callback, ai:tool-confirm IPC handler with 60s timeout, and preload
confirmTool method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tomymaritano and others added 3 commits March 18, 2026 01:29
Adds search_notes, read_note, list_notebooks (auto-execute) and
create_note (requires confirmation) tools wired to SQLite repositories.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds ToolCallBlock component with status indicators, confirmation
buttons, and expandable args/result. AiPanel now tracks tool calls,
handles tool_call/tool_confirm_needed/tool_executing/tool_complete
events, and passes tools: true in chat requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ction)

Implements bidirectional IPC for tools that need editor access:
- Main sends ai:tool-execute-in-renderer to renderer
- Renderer executes via CodeMirror and returns result
- Added rendererOnly flag to ToolRegistration for clean separation

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

vercel Bot commented Mar 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
readide Ready Ready Preview, Comment Mar 18, 2026 3:34pm

Request Review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Introduces a comprehensive AI tool-use system integrating a new ai-core package with streaming infrastructure, tool orchestration via ToolRegistry and runToolLoop, Electron IPC handlers for tool confirmation and renderer execution, built-in note management tools, and corresponding renderer UI components and styling.

Changes

Cohort / File(s) Summary
AI Core Architecture
packages/ai-core/src/types.ts, packages/ai-core/src/provider.ts, packages/ai-core/src/provider-registry.ts, packages/ai-core/src/ai-service.ts, packages/ai-core/src/context-builder.ts, packages/ai-core/src/retry.ts
Extends LLMEvent with stop event and tool_use/tool_result ContentPart variants; adds chatWithTools pathway with ToolChatRequest/ToolChatHandle; updates retry logic to prevent retrying after payload emission; ensures context budget non-negative.
AI Core Tool System
packages/ai-core/src/tool-registry.ts, packages/ai-core/src/tool-loop.ts, packages/ai-core/src/index.ts
Introduces ToolRegistry for tool registration/discovery with lifecycle management, runToolLoop for multi-turn tool orchestration with max round-trips and abort handling, and consolidated public exports for all AI core types and utilities.
AI Core Anthropic Integration
packages/ai-core/src/providers/anthropic.ts, packages/ai-core/src/providers/sse-parser.ts
Extends AnthropicProvider to parse and emit tool_use blocks with id/name/input from SSE streams, accumulate tool deltas, emit tool_call events, and surface stop_reason; normalizes line endings and handles final buffered SSE data.
AI Core Test Suite
packages/ai-core/tests/ai-service.test.ts, packages/ai-core/tests/tool-loop.test.ts, packages/ai-core/tests/tool-registry.test.ts, packages/ai-core/tests/types.test.ts, packages/ai-core/tests/retry.test.ts, packages/ai-core/tests/providers/anthropic-tool-parsing.test.ts
Adds comprehensive tests for AIServiceImpl (chat/chatWithTools, streaming, cancellation), ToolLoop orchestration (round-trips, tool execution, abort), ToolRegistry (registration/retrieval/definitions), type contracts (LLMEvent/ContentPart variants), and AnthropicProvider tool parsing.
Desktop AI Main Process
apps/desktop/src/main/ai/setup.ts, apps/desktop/src/main/ai/ipc-ai.ts, apps/desktop/src/main/ai/built-in-tools.ts
Introduces createAIService/getToolRegistry singletons, wires tool-enabled IPC handlers with confirmation flows and renderer-only tool execution delegates, and registers built-in search/read/list/create note tools with repository integration.
Desktop Main Process Integration
apps/desktop/src/main/index.ts
Wires AI service and tool registry setup, registers built-in tools with note/notebook repository access, and updates registerAIHandlers invocation with tool registry parameter.
Desktop Preload & Renderer API
apps/desktop/src/preload/index.ts
Replaces ai.query with ai.chat streaming method, adds onEvent/cancel/validate utilities, and introduces confirmTool/onToolExecuteRequest/sendToolResult for tool execution workflows.
Desktop Renderer AI Components
apps/desktop/src/renderer/components/ai/AiPanel.tsx, apps/desktop/src/renderer/components/ai/ToolCallBlock.tsx
AiPanel integrates ToolCallBlock rendering, tool event handling (tool_call/tool_confirm_needed/tool_executing/tool_complete), and renderer tool execution listener; ToolCallBlock displays tool call details, status, results, and confirmation UI with expand/collapse behavior.
Desktop Renderer Styling
apps/desktop/src/renderer/styles/ai-panel.css
Adds comprehensive styling for tool call blocks including header, status icons, action buttons, details sections, and ai-spin animation.
Desktop Settings Store
apps/desktop/src/renderer/stores/settings/settingsStore.ts
Reorders settings assignment relative to v2→v3 migration logic, executing cast only after migration steps complete.
Web Marketing UI Colors
apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx, apps/web/app/(marketing)/changelog/page.tsx, apps/web/app/(marketing)/download/page.tsx, apps/web/app/(marketing)/pricing/page.tsx, apps/web/components/Footer.tsx, apps/web/components/Navbar.tsx, apps/web/components/landing/Hero.tsx, apps/web/components/landing/VideoGuides.tsx, apps/web/components/magicui/animated-shiny-text.tsx, apps/web/components/magicui/border-beam.tsx, apps/web/components/magicui/hero-video-dialog.tsx
Updates Tailwind color opacity utilities from shorthand (e.g., white/6, accent/5) to arbitrary notation (e.g., white/[0.06], accent/[0.05]) and corrects gradient syntax from bg-linear-to-* to bg-gradient-to-*; NumberTicker added to pricing display.
Web Docs & Components
apps/web/app/docs/[[...slug]]/page.tsx
Shifts MDX component integration from useMDXComponents hook to explicit component registry with default set plus augmented UI components (Card, Cards, Callout, Step, Steps, Tab, Tabs, Accordion, Accordions, File, Folder, Files, TypeTable).
Documentation & Configuration
.claude/settings.local.json, CLAUDE.md, docs/superpowers/plans/2026-03-14-ai-core-provider-abstraction.md, docs/superpowers/plans/2026-03-18-ai-tool-use.md, docs/superpowers/specs/2026-03-14-ai-core-provider-abstraction-design.md, docs/superpowers/specs/2026-03-18-ai-tool-use-design.md
Adds "Bash(git tag:*)" to settings allowlist; updates CLAUDE.md with new packages and AI Architecture section; introduces comprehensive design specs and implementation plans for provider-agnostic streaming architecture and tool-use orchestration.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Renderer as Renderer<br/>(AiPanel)
    participant Main as Main Process<br/>(IPC)
    participant AIService as AIService<br/>& ToolLoop
    participant Provider as LLM Provider<br/>(Anthropic)
    participant ToolRegistry as ToolRegistry

    User->>Renderer: Start AI chat (with tools flag)
    Renderer->>Main: ai:chat(request with tools)
    Main->>AIService: chatWithTools(request)
    
    loop Tool Loop (max rounds)
        AIService->>Provider: chat(messages + tools)
        Provider-->>AIService: LLMEvent stream (text, tool_call, stop)
        
        AIService->>Main: Forward events to renderer
        Main->>Renderer: ai:event (type: text/tool_call/stop)
        
        alt Tool Call Detected
            Renderer->>User: Display ToolCallBlock (pending_confirmation)
            User->>Renderer: Confirm or Reject tool
            
            alt User Confirms
                Renderer->>Main: ai:tool-confirm(requestId, callId, approved: true)
                Main->>ToolRegistry: get(toolName)
                ToolRegistry-->>Main: Tool registration
                
                alt Renderer-Only Tool
                    Main->>Renderer: ai:tool-execute-request(toolName, args)
                    Renderer->>Renderer: Execute tool (e.g., insert_text)
                    Renderer->>Main: ai:tool-result(requestId, callId, result)
                    Main->>AIService: Resolve pendingRendererResults
                else Main Process Tool
                    Main->>Main: Execute tool (searchNotes, readNote, etc.)
                    AIService->>AIService: Collect tool result
                end
                
                AIService->>AIService: Append tool_result to messages
            else User Rejects
                Renderer->>Main: ai:tool-confirm(requestId, callId, approved: false)
                Main->>AIService: Resolve with rejection
                AIService->>AIService: Skip execution, continue
            end
            
            Renderer->>Renderer: Update ToolCallBlock (executing→complete)
        end
        
        alt Stop Reason = tool_use
            AIService->>AIService: Next round (messages updated)
        else Stop Reason = end_turn or max_tokens
            AIService->>AIService: Exit loop
        end
    end
    
    AIService->>Main: Emit done event (with durationMs)
    Main->>Renderer: ai:event (type: done)
    Renderer->>User: Display final response + tool results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #155: Implements same tool-registry/ToolLoop changes, IPC orchestration, renderer tool UI, and built-in tool wiring across ai-core, desktop main/preload, and renderer components.
  • PR #156: Introduces matching ai-core provider-agnostic streaming architecture, Anthropic provider support, AIService/IPC integration, and preload/renderer streaming refactoring.
  • PR #151: Overlaps on Tailwind color/opacity utility updates across web UI components (marketing pages, navbar, footer, hero).

Suggested labels

app:desktop, app:web, feature, size/XL

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat(ai): add tool use (function calling) to AI assistant' accurately and concisely describes the primary change—implementing tool use (function calling) capability for the AI assistant system.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ai-tool-use
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@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: 35

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx (1)

50-53: ⚠️ Potential issue | 🟠 Major

Model options are hardcoded to Claude models regardless of selected provider.

When users select OpenAI or Ollama as their provider, they're still presented with Claude model options. Either dynamically populate models based on the selected provider, or clarify that only Anthropic models are currently supported.

💡 Suggested approach
  1. Fetch available models from the provider via window.readied.ai.listModels(provider)
  2. Or filter modelOptions based on ai.provider
  3. Update the description from "Claude model" to be provider-agnostic
         <SettingRow
           label="Model"
-          description="Claude model to use for AI queries"
+          description="Model to use for AI queries"
           htmlFor="aiModel"
         >

Also applies to: 254-269

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx` around lines
50 - 53, The hardcoded modelOptions array in AiSection is showing Claude models
regardless of ai.provider; replace this with provider-aware logic: either call
window.readied.ai.listModels(ai.provider) inside the AiSection component (e.g.,
on provider change) to populate modelOptions dynamically, or filter a master
models list by ai.provider before setting modelOptions; also update any UI label
text that says "Claude model" to a provider-agnostic string and apply the same
fix where modelOptions is duplicated (the other modelOptions usage referenced in
the file). Ensure you reference and update the modelOptions variable, the
ai.provider state, and the call site using window.readied.ai.listModels so the
dropdown reflects the selected provider.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/ai/built-in-tools.ts`:
- Around line 37-41: The execute handler for this built-in tool currently casts
args.query and args.limit without validation; add runtime checks in the execute
function to validate that args.query is a non-empty string and args.limit (if
provided) is a positive integer before calling deps.searchNotes, returning a
structured error response (e.g., { ok: false, error: '...' }) when validation
fails; use the same symbols (execute, args.query, args.limit, deps.searchNotes)
so the fix is localized to this function and avoid relying solely on the JSON
schema.

In `@apps/desktop/src/main/ai/ipc-ai.ts`:
- Around line 96-105: The IPC handlers (ipcMain.handle('ai:cancel', etc.) mutate
shared state in activeHandles without verifying the caller; store the
originating sender id (use _event.sender.id / webContents.id) when creating each
entry in activeHandles and, in all handlers that inspect or modify entries
(including the cancel path using requestId and handle.abort), first lookup the
entry and compare its stored ownerId to _event.sender.id, rejecting the call if
they differ; update the activeHandles entry structure to include ownerId,
enforce the ownership check in all related handlers (the cancel handler and the
others flagged around lines 165–190), and only then perform abort(), delete(),
or resolve operations.
- Around line 107-130: The current ai:validate handler calls service.chat and
immediately aborts the returned handle so executeChat never runs and invalid
providers/keys/baseUrls still return ok: true; update the ai:validate
implementation (ipcMain.handle for 'ai:validate') to perform a real validation:
either call the provider's validation method (e.g., provider.validate(...) or
provider.listModels(...)) obtained from the registry/service, or if using
service.chat keep the handle but consume handle.events until you receive the
first non-'start' event (or an error) before deciding success, then
abort/cleanup the stream and return the appropriate {ok:false, error:...} on
failure instead of returning true prematurely; ensure errors thrown by
executeChat or provider calls are caught and converted to a message.

In `@apps/desktop/src/main/ai/setup.ts`:
- Line 13: The code currently force-casts net.fetch to FetchFn when registering
AnthropicProvider (registry.register(new AnthropicProvider(net.fetch as unknown
as FetchFn))), which hides a type mismatch; replace the cast with an explicit
adapter or runtime-validated wrapper that maps Electron's net.fetch shape to the
package's FetchFn contract (e.g., implement an adaptNetFetchToFetchFn function
and pass adaptNetFetchToFetchFn(net.fetch) into new AnthropicProvider), and add
a small runtime assertion/test that the adapted function conforms to FetchFn
behavior before registering to ensure the contract is explicit and safe.

In `@apps/desktop/src/main/index.ts`:
- Around line 2354-2360: The createNote handler currently returns a silent empty
id on failure; change createNote (the exported async function that calls
createNoteOperation with noteRepo) to propagate the failure as an error result
instead of returning { id: '' } — e.g., when createNoteOperation returns
result.ok === false, return an object matching the tool executor contract (like
{ ok: false, error: result.error || 'createNote failed' }) so callers (and the
built-in-tools.ts executor) can handle the failure rather than receiving a bogus
empty id.
- Around line 2350-2352: The listNotebooks implementation returns noteCount: 0
for every notebook; update listNotebooks to populate the real note counts by
using the repository method that returns metadata (e.g., nbRepo.getWithMetadata
or an equivalent), or by calling nbRepo.getWithMetadata(nb.id) per notebook (in
parallel) and mapping metadata.noteCount into the returned object instead of
hardcoding 0; alternatively, if the repository cannot provide counts, document
that noteCount is unsupported rather than returning 0.

In `@apps/desktop/src/renderer/components/ai/AiPanel.tsx`:
- Around line 171-175: The error case in the stream handler (the `case 'error'`
block) is tearing down UI state for transient retryable errors emitted by
`withRetry()`; update the `case 'error'` handler so it only calls setError,
setLoading(false) and clears activeRequestRef.current when the error is a
final/non-retryable failure (mirror the same guard used in the initial-command
listener before `onCommandExecuted()` / `commandCleanup()`), otherwise ignore
transient/backoff errors so later retried `text`/`done` events from the same
request are processed; locate the `case 'error'` block and add the
retryable/final error check (using the same predicate or property the
initial-command listener uses) before performing teardown.
- Around line 277-346: The command-specific onEvent handler races with the
shared listener because it compares the incoming requestId to
activeRequestRef.current which the shared listener may clear first; capture the
command's request id when you create the handler (e.g. const commandRequestId =
activeRequestRef.current) and use that captured commandRequestId inside the
closure instead of activeRequestRef.current so the
replaceSelection/insertAtCursor logic and onCommandExecuted() always run for
this command's events; update the conditional (if (requestId !==
commandRequestId) return;) and keep the rest of the handler (accumulatedText,
switch cases, commandCleanup()) the same.
- Around line 228-240: The handler for the 'tool_complete' event unconditionally
sets status: 'complete', which masks rejected/failed results; update the
setToolCalls callback (inside the 'tool_complete' case) to inspect e.result.ok
and any existing status (keep 'rejected' if already set) and set status to
'complete' only when e.result.ok is true, otherwise set a failure status (e.g.,
'failed' or preserve 'rejected') and attach e.result; reference the existing
variables setToolCalls, event (cast to e), e.call.id, existing, and e.result
when implementing the change.

In `@apps/desktop/src/renderer/components/ai/ToolCallBlock.tsx`:
- Around line 72-81: The Run/Cancel buttons in ToolCallBlock (rendered when
status === 'pending_confirmation') can be double-clicked because status updates
are asynchronous; wrap onConfirm and onReject with a local loading boolean
(e.g., isActioning) in the ToolCallBlock component, set isActioning = true
immediately in the click handlers before invoking the passed callbacks, and
disable both buttons when isActioning is true (or while status !==
'pending_confirmation'); also ensure isActioning is cleared if the parent status
prop changes or after the callback's promise resolves/rejects so the UI stays in
sync.
- Around line 47-70: The header currently uses a non-interactive div with
onClick (the element rendering className "ai-tool-call-header" that toggles
expanded via setExpanded and reads expanded) so keyboard users can't activate
it; replace the div with an actual interactive element (preferably a <button>)
or at minimum add role="button", tabIndex={0} and an onKeyDown handler that
calls setExpanded on Enter/Space, and ensure aria-expanded={expanded} and an
accessible label are present; also update CSS to reset button default styles so
the visual remains the same for the element that replaces the div.

In `@apps/desktop/src/renderer/stores/settings/schema.ts`:
- Around line 64-67: SettingsSchemaV2 currently ends up requiring the new
AiSettings.provider field, collapsing v2/v3 boundaries; fix by creating a
v2-specific AI settings type (e.g., AiSettingsV2) that omits or excludes the
provider field (or explicitly defines the older shape) and use that type inside
SettingsSchemaV2 instead of AiSettings so migrations/validation keep the real
persisted v2 shape separate from the new v3 AiSettings; update any references
that build SettingsSchemaV2 to use AiSettingsV2 (or an Omit<AiSettings,
'provider'> type) and keep AiSettings as the v3 shape with provider.

In `@apps/desktop/src/renderer/stores/settings/settingsStore.ts`:
- Around line 96-107: The code assigns settings = mutable before running the v3
migration, so the migration changes to mutable (adding ai.provider and bumping
version) are never returned; fix by ensuring the post-migration state is
returned — either move the settings = mutable as SettingsSchema assignment to
after the migration block or reassign settings = mutable as SettingsSchema right
before the return; reference the variables/members named settings, mutable,
SettingsSchema and the v3 migration block that sets version: 3 and ai: {
provider: 'anthropic', ...mutable.ai } so the returned object includes the
migrated values.

In `@apps/desktop/src/renderer/styles/ai-panel.css`:
- Line 305: The CSS rule uses the deprecated value "word-break: break-word";
replace that declaration with a non-deprecated alternative by either using
"overflow-wrap: break-word;" for natural wrapping or "word-break: break-all;"
for aggressive breaking—update the rule that currently contains "word-break:
break-word;" to one of these alternatives (or include both: "overflow-wrap:
break-word;" and a fallback "word-break: break-all;") so the selector no longer
relies on the deprecated value.

In `@apps/web/app/`(marketing)/auth/verify/AuthVerifyContent.tsx:
- Around line 42-43: Replace hardcoded hex color classes in AuthVerifyContent
(e.g., the h1 with className "text-2xl font-bold text-[`#f4f4f5`] mb-3" and the p
with "text-[`#a1a1aa`] mb-6", plus other occurrences noted around lines 63-65,
82-83, 94-102) with Tailwind theme tokens or named color classes such as
text-zinc-50 and text-zinc-400, or better yet use semantic theme tokens you’ve
defined (e.g., text-foreground, text-muted) from your
Tailwind/@readied/product-config; also normalize border opacity notation to
border-white/6 where applicable. Ensure you update the className strings on the
relevant JSX elements in AuthVerifyContent.tsx (h1, p, and any other elements
flagged) to use the token names instead of hex values.

In `@apps/web/app/`(marketing)/pricing/page.tsx:
- Around line 112-132: The current annual NumberTicker uses decimalPlaces={0},
which will silently round if the yearly price ever includes cents; update the
annual block to compute decimalPlaces dynamically (e.g., decimalPlaces =
annualAmountCents % 100 === 0 ? 0 : 2) and pass that value to the NumberTicker
for annualPrice (or, if you don't have annualAmountCents, derive it from
annualPrice by converting to cents with safe rounding and then checking cents %
100), ensuring the NumberTicker usage (component name NumberTicker and prop
decimalPlaces) reflects the computed value rather than a hardcoded 0.

In `@apps/web/components/Footer.tsx`:
- Around line 137-150: The social link rendering is duplicated: the map over
socialLinks in Footer (the block rendering icon-only links) repeats the same
href/label/target/rel/aria pattern already used earlier; extract a small
reusable SocialLink component (e.g., SocialLink({ social, iconOnly?: boolean }))
and replace both maps to call it with iconOnly=true for the icon-row and
iconOnly=false for the labeled row, preserving className variants and props
(href, target, rel, aria-label, and rendering social.icon and optionally
social.label).

In `@CLAUDE.md`:
- Around line 265-268: The fenced code block containing "Renderer (AiPanel) →
IPC → Main (ipc-ai.ts) → AIService → ProviderRegistry → Provider → SSE stream ←
batched LLMEvents (text/error/done) ←" is missing a language identifier; update
that opening ``` to include a language (for example ```text) so the code block
conforms to MD040 and renders correctly.

In `@docs/superpowers/specs/2026-03-14-ai-core-provider-abstraction-design.md`:
- Around line 44-112: The fenced code blocks in the spec are missing language
identifiers which breaks syntax highlighting; update each fenced block by adding
an appropriate language tag (use text for ASCII diagrams and the specific
language where applicable). For example, add ```text to the block that begins
with "Renderer" and likewise tag the blocks containing "AIService",
"ContextBuilder", "ProviderRegistry", and "LLMProvider" (or replace with a more
specific language tag if the block contains real code rather than an ASCII
diagram). Ensure every triple-backtick fence in the file has a language
specifier.

In `@docs/superpowers/specs/2026-03-18-ai-tool-use-design.md`:
- Around line 185-199: The public API doc for chatWithTools currently declares
it returning ChatHandle but must return the richer ToolChatHandle because
tool-enabled chats stream both ToolLoopEvent and LLMEvent; update the signature
for chatWithTools(request: ToolChatRequest): ToolChatHandle (and any related
docs) so the return type reflects ToolLoopEvent support and reference
ToolChatRequest, chatWithTools, ToolChatHandle, ToolLoopEvent and LLMEvent when
making the change.

In `@package.json`:
- Around line 59-64: The pnpm.overrides entry is forcing `@types/react` and
`@types/react-dom` to React 18 versions which conflicts with apps/web's
react@^19.0.0; update the overrides so they do not globally pin React 18 types —
either remove the global pnpm.overrides for "@types/react" and
"@types/react-dom" and instead add package-scoped overrides for packages that
still need React 18, or change the override to the matching React 19 type
versions; look for the pnpm.overrides block (symbols: "pnpm", "overrides",
"@types/react", "@types/react-dom") and adjust to either remove the global pins
or replace them with per-package overrides targeting the specific packages or
update to React 19 `@types` versions to match apps/web's react@^19.0.0.

In `@packages/ai-core/src/ai-command-types.test.ts`:
- Around line 1-232: This file duplicates the same test suite for
resolveTemplate, validateAiCommandDefinition, validateAiCommandPreset, and
serializePreset/parsePreset that exists elsewhere; remove this duplicate test
file (or consolidate by deleting its contents) so the tests only exist in one
location, ensuring only one set of describe blocks referencing resolveTemplate,
validateAiCommandDefinition, validateAiCommandPreset, serializePreset, and
parsePreset remains in the repo.

In `@packages/ai-core/src/ai-service.ts`:
- Around line 100-170: The request lifecycle in executeChatWithTools currently
does provider lookup, model discovery, context building, and emits the initial
'start' event outside the try/finally so activeRequests.delete and the final
'done' yield can be skipped if any of those steps throws or the iterator is
closed; refactor executeChatWithTools (and the other generator at lines 173-247)
to move provider = this.registry.get(...), model discovery (models.find...),
context construction (buildContext), creation/emission of the initial 'start'
event, and the runToolLoop logic into a single outer try/catch/finally block so
that activeRequests.delete(requestId) and the final 'done' yield always run,
preserve the inner catch behavior that yields a provider_error on non-abort
errors, and keep runToolLoop/toolLoop handling the same inside the try.

In `@packages/ai-core/src/context-builder.ts`:
- Around line 14-20: The ContextSources.interface declares toolResults but
buildContext does not use it; add a brief clarifying comment above the
toolResults property in the ContextSources interface stating it is reserved for
future tool loop support and currently unused by buildContext (or remove the
property if you prefer to keep the interface minimal) so future readers know
this is intentional; reference the ContextSources interface and the buildContext
function in the comment to make the intent explicit.
- Around line 93-95: buildContext computes available as budget.maxContextTokens
- budget.maxResponseTokens but doesn't guard against negative values; update the
start of buildContext to clamp available to a non‑negative value (e.g. available
= Math.max(0, budget.maxContextTokens - budget.maxResponseTokens)) and, if
appropriate for your flow, handle the zero case early (return an empty/zeroed
ContextBuildResult or skip token allocation) so downstream logic using available
won't misbehave; reference: buildContext, ContextBudget, available.

In `@packages/ai-core/src/providers/anthropic.ts`:
- Around line 191-198: When JSON.parse(block.jsonBuf || '{}') fails we currently
swallow the error and return an empty args object; change the catch to capture
the parse error and surface it in the yielded tool call so downstream handlers
can detect malformed JSON. Specifically, in the try/catch around JSON.parse
(reference block.jsonBuf and the args variable) keep the fallback args = {} but
store the caught error message (e.g., parseError or rawJson) and include that in
the yielded object for type:'tool_call' (the same place you call yield { type:
'tool_call', id: block.id, name: block.name, args }) before calling
toolBlocks.delete(index). Ensure the parse error info is non-throwing and
concise so existing consumers can opt-in to logging or error handling.
- Around line 36-46: The classifyHttpStatus function currently maps any 400 to
context_overflow which is too broad; update the logic so classifyHttpStatus (or
the Anthropic response handler that calls it) examines the response body/error
payload for specific Anthropic validation messages (e.g., schema errors,
invalid_params, or explicit "context length" indicators) and only return {code:
'context_overflow', retryable: false} when the body clearly indicates
token/context limits; otherwise map known validation errors to distinct codes
(e.g., 'invalid_request' or 'validation_error') or default to provider_error,
and ensure this check is implemented where the response body is available (the
error handling path that invokes classifyHttpStatus).

In `@packages/ai-core/src/providers/sse-parser.ts`:
- Around line 15-23: The SSE parsing assumes '\n\n' framing and drops
EOF-terminated frames; update the logic around reader.read/decoder.decode and
buffer handling to normalize CRLF to LF (e.g., replace '\r\n' with '\n' on
incoming chunks) before splitting, split on '\n\n' to get complete events, and
when reader.read() returns done=true flush/process any remaining buffer content
as a final event (don't just break). Also add a regression test that sends
frames terminated with '\r\n\r\n' and a frame that ends at EOF (CRLF+EOF) to
ensure both are yielded.

In `@packages/ai-core/src/retry.ts`:
- Around line 33-37: The auth error check uses msg.includes('invalid.*key')
which treats the pattern as a literal string and misses messages like "invalid
api key"; in the function where msg is defined (variable msg) replace the
literal include with a regex test (e.g., /invalid.*key/i.test(msg)) or check for
both 'invalid' and 'key' substrings case-insensitively so that messages such as
"invalid api key" correctly return 'auth_failed' from the classification logic.
- Around line 57-75: The retry loop currently restarts fn() even after yielding
payload events which causes duplicate outputs; add a flag (e.g., emittedPayload)
that is set when yielding any payload event (such as event.type === 'text' or
'tool_call' or any non-control event), and only allow the retry logic inside the
for-await when emittedPayload is false; specifically, in the block handling
event.type === 'error' and retryable codes, check emittedPayload and if true
treat the error as non-retryable (yield { ...event, retryable: false } and
return) instead of breaking to retry; update the loop around fn(), and
references to attempt, opts.maxRetries, opts.retryableCodes and fn() accordingly
so retries only happen before any payload escapes the wrapper.

In `@packages/ai-core/src/tool-registry.ts`:
- Around line 20-27: The unregister callback is not instance-safe because it
always deletes by tool.name; change register (in register(tool:
ToolRegistration)) to store a registration token alongside the tool in the
this.tools map (e.g., map value becomes an object {tool, token}) and capture
that token in the returned disposer closure; when the disposer runs, only delete
this.tools.get(tool.name) if its stored token equals the captured token so a
later re-registration with the same name isn't removed by an earlier disposer.

In `@packages/ai-core/tests/ai-service.test.ts`:
- Around line 124-135: The test currently only checks that handles returned from
service.chat have requestId values but doesn't assert that cancelAll() actually
aborts them; update the test using the handles (from AIServiceImpl.chat) to
consume their event streams or returned promises and assert that the
terminal/done event includes cancelled: true (e.g., subscribe or await
handle.events or handle.result and verify an event object with done: {
cancelled: true } for both handles after calling service.cancelAll()), keeping
existing setup with ProviderRegistry and createMockProvider.

In `@packages/ai-core/tests/providers/anthropic.test.ts`:
- Around line 102-112: The test for 401 currently inspects properties of
errorEvent without asserting it exists, so add an existence assertion before
property checks: after collecting events with collectEvents(provider) and
locating errorEvent (const errorEvent = events.find(...)), add
expect(errorEvent).toBeDefined() (or equivalent) to ensure the test fails if no
error event was emitted, then keep the existing checks on errorEvent.code and
errorEvent.retryable for AnthropicProvider handling of a 401.

In `@packages/ai-core/tests/retry.test.ts`:
- Around line 79-88: The mock async iterator in the test never signals
completion because its second next() return uses done: false for the `{ type:
'done', durationMs: 50 }` event; update the iterator returned by the test (the
next() closure that closes over `yielded`) to return `done: true` for the
`'done'` event so the iterator terminates correctly (i.e., adjust the second
return of the `next()` method from `{ done: false, value: { type: 'done', ... }
}` to `{ done: true, value: { type: 'done', ... } }`).

In `@packages/ai-core/tests/tool-loop.test.ts`:
- Around line 3-7: Remove the unused type import and alias: delete ToolCall from
the import list in the top of the test (keep ToolLoopOptions and ToolLoopEvent)
and remove the unused type alias ChatFn (the "type ChatFn = ..." declaration);
this cleans up unused symbols while leaving LLMEvent, ChatOptions, LLMProvider,
and ProviderConfig intact.

---

Outside diff comments:
In `@apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx`:
- Around line 50-53: The hardcoded modelOptions array in AiSection is showing
Claude models regardless of ai.provider; replace this with provider-aware logic:
either call window.readied.ai.listModels(ai.provider) inside the AiSection
component (e.g., on provider change) to populate modelOptions dynamically, or
filter a master models list by ai.provider before setting modelOptions; also
update any UI label text that says "Claude model" to a provider-agnostic string
and apply the same fix where modelOptions is duplicated (the other modelOptions
usage referenced in the file). Ensure you reference and update the modelOptions
variable, the ai.provider state, and the call site using
window.readied.ai.listModels so the dropdown reflects the selected provider.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 80dc1082-da3c-4a51-8ec4-ebd3ecb601e8

📥 Commits

Reviewing files that changed from the base of the PR and between f686ab6 and e349b3f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (65)
  • .claude/settings.local.json
  • CLAUDE.md
  • apps/desktop/package.json
  • apps/desktop/src/main/ai/built-in-tools.ts
  • apps/desktop/src/main/ai/ipc-ai.ts
  • apps/desktop/src/main/ai/setup.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/preload/index.ts
  • apps/desktop/src/renderer/App.tsx
  • apps/desktop/src/renderer/components/ai/AiPanel.tsx
  • apps/desktop/src/renderer/components/ai/ToolCallBlock.tsx
  • apps/desktop/src/renderer/hooks/useRegisterPluginAiCommands.ts
  • apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx
  • apps/desktop/src/renderer/plugins/aiAssistant.tsx
  • apps/desktop/src/renderer/stores/settings/schema.ts
  • apps/desktop/src/renderer/stores/settings/settingsStore.ts
  • apps/desktop/src/renderer/styles/ai-panel.css
  • apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx
  • apps/web/app/(marketing)/changelog/page.tsx
  • apps/web/app/(marketing)/download/page.tsx
  • apps/web/app/(marketing)/pricing/page.tsx
  • apps/web/app/docs/[[...slug]]/page.tsx
  • apps/web/components/Footer.tsx
  • apps/web/components/Navbar.tsx
  • apps/web/components/landing/Hero.tsx
  • apps/web/components/landing/VideoGuides.tsx
  • apps/web/components/magicui/animated-shiny-text.tsx
  • apps/web/components/magicui/border-beam.tsx
  • apps/web/components/magicui/hero-video-dialog.tsx
  • docs/superpowers/plans/2026-03-14-ai-core-provider-abstraction.md
  • docs/superpowers/plans/2026-03-18-ai-tool-use.md
  • docs/superpowers/specs/2026-03-14-ai-core-provider-abstraction-design.md
  • docs/superpowers/specs/2026-03-18-ai-tool-use-design.md
  • package.json
  • packages/ai-assistant/src/claude-client.ts
  • packages/ai-assistant/src/index.ts
  • packages/ai-assistant/src/prompts.ts
  • packages/ai-assistant/src/rag.ts
  • packages/ai-core/package.json
  • packages/ai-core/src/ai-command-types.test.ts
  • packages/ai-core/src/ai-command-types.ts
  • packages/ai-core/src/ai-service.ts
  • packages/ai-core/src/context-builder.ts
  • packages/ai-core/src/index.ts
  • packages/ai-core/src/provider-registry.ts
  • packages/ai-core/src/provider.ts
  • packages/ai-core/src/providers/anthropic.ts
  • packages/ai-core/src/providers/sse-parser.ts
  • packages/ai-core/src/retry.ts
  • packages/ai-core/src/tool-loop.ts
  • packages/ai-core/src/tool-registry.ts
  • packages/ai-core/src/types.ts
  • packages/ai-core/tests/ai-command-types.test.ts
  • packages/ai-core/tests/ai-service.test.ts
  • packages/ai-core/tests/context-builder.test.ts
  • packages/ai-core/tests/provider-registry.test.ts
  • packages/ai-core/tests/providers/anthropic-tool-parsing.test.ts
  • packages/ai-core/tests/providers/anthropic.test.ts
  • packages/ai-core/tests/providers/sse-parser.test.ts
  • packages/ai-core/tests/retry.test.ts
  • packages/ai-core/tests/tool-loop.test.ts
  • packages/ai-core/tests/tool-registry.test.ts
  • packages/ai-core/tests/types.test.ts
  • packages/ai-core/tsconfig.json
  • vercel.json
💤 Files with no reviewable changes (4)
  • packages/ai-assistant/src/rag.ts
  • packages/ai-assistant/src/index.ts
  • packages/ai-assistant/src/prompts.ts
  • packages/ai-assistant/src/claude-client.ts

Comment on lines +37 to +41
execute: async args => {
const results = await deps.searchNotes(args.query as string, (args.limit as number) ?? 10);
return { ok: true, content: JSON.stringify(results) };
},
});

@coderabbitai coderabbitai Bot Mar 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding runtime validation for tool arguments.

The type assertions (args.query as string, args.limit as number) assume the LLM provides correctly typed arguments. If the LLM sends malformed input (e.g., query as a number or missing entirely), the code may behave unexpectedly. While the JSON schema defines required: ['query'], the provider may not enforce this strictly.

💡 Suggested defensive validation
     execute: async args => {
+      const query = typeof args.query === 'string' ? args.query : String(args.query ?? '');
+      const limit = typeof args.limit === 'number' ? args.limit : 10;
-      const results = await deps.searchNotes(args.query as string, (args.limit as number) ?? 10);
+      const results = await deps.searchNotes(query, limit);
       return { ok: true, content: JSON.stringify(results) };
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
execute: async args => {
const results = await deps.searchNotes(args.query as string, (args.limit as number) ?? 10);
return { ok: true, content: JSON.stringify(results) };
},
});
execute: async args => {
const query = typeof args.query === 'string' ? args.query : String(args.query ?? '');
const limit = typeof args.limit === 'number' ? args.limit : 10;
const results = await deps.searchNotes(query, limit);
return { ok: true, content: JSON.stringify(results) };
},
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/ai/built-in-tools.ts` around lines 37 - 41, The execute
handler for this built-in tool currently casts args.query and args.limit without
validation; add runtime checks in the execute function to validate that
args.query is a non-empty string and args.limit (if provided) is a positive
integer before calling deps.searchNotes, returning a structured error response
(e.g., { ok: false, error: '...' }) when validation fails; use the same symbols
(execute, args.query, args.limit, deps.searchNotes) so the fix is localized to
this function and avoid relying solely on the JSON schema.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — valid nitpick. Tool args are schema-constrained by the provider. Will consider runtime validation in a follow-up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@tomymaritano, understood! Schema-based constraints do provide a reasonable first line of defense. Whenever you're ready to tackle the follow-up, would you like me to open a GitHub issue to track adding runtime validation for tool arguments in built-in-tools.ts?

Comment thread apps/desktop/src/main/ai/ipc-ai.ts Outdated
Comment thread apps/desktop/src/main/ai/ipc-ai.ts
if (service) return service;

const registry = new ProviderRegistry();
registry.register(new AnthropicProvider(net.fetch as unknown as FetchFn));

@coderabbitai coderabbitai Bot Mar 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the FetchFn type definition to understand expected signature
ast-grep --pattern 'type FetchFn = $_'

# Also check if there are any Response type differences
rg -n 'FetchFn' --type ts -C 3

Repository: tomymaritano/readide

Length of output: 6656


Document or address the intentional type mismatch in net.fetch cast.

The double cast as unknown as FetchFn is necessary because net.fetch doesn't natively conform to the custom FetchFn interface defined in packages/ai-core/src/provider.ts. This is an intentional design choice—the custom FetchFn interface provides abstraction decoupled from the standard Fetch API. However, this reliance on type assertion bypasses TypeScript's type safety and could mask issues if Electron's net.fetch behavior diverges from expected behavior. Consider either: (1) adding a test validating that net.fetch satisfies the required FetchFn contract at runtime, or (2) creating a wrapper that explicitly adapts net.fetch to the FetchFn interface to make the contract explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/ai/setup.ts` at line 13, The code currently force-casts
net.fetch to FetchFn when registering AnthropicProvider (registry.register(new
AnthropicProvider(net.fetch as unknown as FetchFn))), which hides a type
mismatch; replace the cast with an explicit adapter or runtime-validated wrapper
that maps Electron's net.fetch shape to the package's FetchFn contract (e.g.,
implement an adaptNetFetchToFetchFn function and pass
adaptNetFetchToFetchFn(net.fetch) into new AnthropicProvider), and add a small
runtime assertion/test that the adapted function conforms to FetchFn behavior
before registering to ensure the contract is explicit and safe.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — trivial nitpick, the type works as-is.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rate Limit Exceeded

@tomymaritano have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 59 seconds before sending another message.

Comment on lines +2350 to +2352
listNotebooks: async () => {
const notebooks = await nbRepo.getAll();
return notebooks.map(nb => ({ id: nb.id, name: nb.name, noteCount: 0 }));

@coderabbitai coderabbitai Bot Mar 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

noteCount is hardcoded to 0.

The listNotebooks implementation returns noteCount: 0 for all notebooks. If the AI uses this data to make decisions (e.g., "which notebook has the most notes?"), it will receive inaccurate information. Consider populating this from getWithMetadata or document that noteCount is not supported.

         listNotebooks: async () => {
           const notebooks = await nbRepo.getAll();
-          return notebooks.map(nb => ({ id: nb.id, name: nb.name, noteCount: 0 }));
+          // Note: noteCount not available from getAll(), returning 0
+          // Consider using getWithMetadata() if counts are needed
+          return notebooks.map(nb => ({ id: nb.id, name: nb.name, noteCount: 0 }));
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
listNotebooks: async () => {
const notebooks = await nbRepo.getAll();
return notebooks.map(nb => ({ id: nb.id, name: nb.name, noteCount: 0 }));
listNotebooks: async () => {
const notebooks = await nbRepo.getAll();
// Note: noteCount not available from getAll(), returning 0
// Consider using getWithMetadata() if counts are needed
return notebooks.map(nb => ({ id: nb.id, name: nb.name, noteCount: 0 }));
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/index.ts` around lines 2350 - 2352, The listNotebooks
implementation returns noteCount: 0 for every notebook; update listNotebooks to
populate the real note counts by using the repository method that returns
metadata (e.g., nbRepo.getWithMetadata or an equivalent), or by calling
nbRepo.getWithMetadata(nb.id) per notebook (in parallel) and mapping
metadata.noteCount into the returned object instead of hardcoding 0;
alternatively, if the repository cannot provide counts, document that noteCount
is unsupported rather than returning 0.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 833ecf9. Kept noteCount: 0 as a placeholder since no per-notebook count method exists yet in the storage layer. Will address in a follow-up once the API is available.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rate Limit Exceeded

@tomymaritano have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 34 seconds before sending another message.

Comment thread packages/ai-core/src/tool-registry.ts
Comment on lines +124 to +135
it('cancelAll aborts all active requests', () => {
const registry = new ProviderRegistry();
registry.register(createMockProvider([{ type: 'text', delta: 'hi' }]));
const service = new AIServiceImpl(registry);

const handle1 = service.chat(createRequest());
const handle2 = service.chat(createRequest());
service.cancelAll();

expect(handle1.requestId).toBeTruthy();
expect(handle2.requestId).toBeTruthy();
});

@coderabbitai coderabbitai Bot Mar 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test doesn't verify cancelAll actually aborts the handles.

The test only asserts that handles have requestId values, but doesn't verify that cancelAll() actually cancelled them. Consider consuming the events and checking for done.cancelled: true.

🔧 Proposed fix
   it('cancelAll aborts all active requests', async () => {
     const registry = new ProviderRegistry();
     registry.register(createMockProvider([{ type: 'text', delta: 'hi' }]));
     const service = new AIServiceImpl(registry);

     const handle1 = service.chat(createRequest());
     const handle2 = service.chat(createRequest());
     service.cancelAll();

-    expect(handle1.requestId).toBeTruthy();
-    expect(handle2.requestId).toBeTruthy();
+    const events1 = await collectEvents(handle1);
+    const events2 = await collectEvents(handle2);
+    
+    const done1 = events1.find(e => e.type === 'done');
+    const done2 = events2.find(e => e.type === 'done');
+    
+    expect(done1?.type === 'done' && done1.cancelled).toBe(true);
+    expect(done2?.type === 'done' && done2.cancelled).toBe(true);
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('cancelAll aborts all active requests', () => {
const registry = new ProviderRegistry();
registry.register(createMockProvider([{ type: 'text', delta: 'hi' }]));
const service = new AIServiceImpl(registry);
const handle1 = service.chat(createRequest());
const handle2 = service.chat(createRequest());
service.cancelAll();
expect(handle1.requestId).toBeTruthy();
expect(handle2.requestId).toBeTruthy();
});
it('cancelAll aborts all active requests', async () => {
const registry = new ProviderRegistry();
registry.register(createMockProvider([{ type: 'text', delta: 'hi' }]));
const service = new AIServiceImpl(registry);
const handle1 = service.chat(createRequest());
const handle2 = service.chat(createRequest());
service.cancelAll();
const events1 = await collectEvents(handle1);
const events2 = await collectEvents(handle2);
const done1 = events1.find(e => e.type === 'done');
const done2 = events2.find(e => e.type === 'done');
expect(done1?.type === 'done' && done1.cancelled).toBe(true);
expect(done2?.type === 'done' && done2.cancelled).toBe(true);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/tests/ai-service.test.ts` around lines 124 - 135, The test
currently only checks that handles returned from service.chat have requestId
values but doesn't assert that cancelAll() actually aborts them; update the test
using the handles (from AIServiceImpl.chat) to consume their event streams or
returned promises and assert that the terminal/done event includes cancelled:
true (e.g., subscribe or await handle.events or handle.result and verify an
event object with done: { cancelled: true } for both handles after calling
service.cancelAll()), keeping existing setup with ProviderRegistry and
createMockProvider.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — the test verifies handle creation. Will add an explicit cancelAll assertion in a follow-up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rate Limit Exceeded

@tomymaritano have exceeded the limit for the number of chat messages per hour. Please wait 1 minutes and 41 seconds before sending another message.

Comment on lines +102 to +112
it('emits error on 401', async () => {
const fetchFn = createMockFetch([], 401);
const provider = new AnthropicProvider(fetchFn);
const events = await collectEvents(provider);

const errorEvent = events.find(e => e.type === 'error');
if (errorEvent?.type === 'error') {
expect(errorEvent.code).toBe('auth_failed');
expect(errorEvent.retryable).toBe(false);
}
});

@coderabbitai coderabbitai Bot Mar 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add assertion that errorEvent is defined before checking its properties.

The 429 test (line 95) asserts expect(errorEvent).toBeDefined() before the conditional check, but this test skips that assertion. If no error event is emitted, the test silently passes without verifying the expected behavior.

🔧 Proposed fix
   it('emits error on 401', async () => {
     const fetchFn = createMockFetch([], 401);
     const provider = new AnthropicProvider(fetchFn);
     const events = await collectEvents(provider);

     const errorEvent = events.find(e => e.type === 'error');
+    expect(errorEvent).toBeDefined();
     if (errorEvent?.type === 'error') {
       expect(errorEvent.code).toBe('auth_failed');
       expect(errorEvent.retryable).toBe(false);
     }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ai-core/tests/providers/anthropic.test.ts` around lines 102 - 112,
The test for 401 currently inspects properties of errorEvent without asserting
it exists, so add an existence assertion before property checks: after
collecting events with collectEvents(provider) and locating errorEvent (const
errorEvent = events.find(...)), add expect(errorEvent).toBeDefined() (or
equivalent) to ensure the test fails if no error event was emitted, then keep
the existing checks on errorEvent.code and errorEvent.retryable for
AnthropicProvider handling of a 401.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acknowledged — the existing assertion pattern is sufficient for the test's purpose.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@tomymaritano, understood — happy to defer to your judgment on this one. The optional chaining pattern does work in practice as long as the intent is clear to readers.

( (^_^) )


🧠 Learnings used
Learnt from: CR
Repo: tomymaritano/readide PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-14T20:03:25.242Z
Learning: Core domain changes require test coverage

Comment thread packages/ai-core/tests/retry.test.ts
Comment thread packages/ai-core/tests/tool-loop.test.ts Outdated
Critical fixes:
- Fix v3 settings migration result being dropped before return
- Prevent retry after payload has been yielded (avoids duplicate output/tool runs)
- Scope IPC handlers to owning window (cancel, tool-confirm, renderer-result)

Major fixes:
- ai:validate now consumes stream to actually test provider connection
- SSE parser handles CRLF line endings and remaining buffer at EOF
- Tool registry unregister is instance-safe (won't remove re-registered tools)
- ai-service wraps full request lifecycle in try/finally (incl. provider lookup)
- AiPanel: retryable errors don't tear down the stream
- AiPanel: command listener no longer races with shared stream listener
- Remove duplicate test suite from src/ (kept tests/ version)

Minor fixes:
- createNote throws on failure instead of returning empty id
- ToolCallBlock header is keyboard-accessible (role, tabIndex, onKeyDown)
- CSS: use overflow-wrap instead of deprecated word-break: break-word
- Tool call status reflects error/rejected state, not always 'complete'
- Context builder guards against negative available budget
- Anthropic 400 errors classified as invalid_request, not context_overflow
- Add invalid_request to LLMErrorCode type
- Fix mock iterator termination in retry test
- Remove unused imports in tool-loop test

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

`msg.includes('invalid.*key')` matched the literal string, not a pattern.
Messages like "invalid api key" now correctly classify as auth_failed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts keeping tool use features + CodeRabbit fixes.
Incoming from develop: ai-core base (#156), release pipeline (#157), cleanUrls fix (#164).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file app:web app:desktop labels Mar 18, 2026
@github-actions github-actions Bot enabled auto-merge (squash) March 18, 2026 15:33
if (msg.includes('429') || msg.includes('rate limit') || msg.includes('rate_limit'))
return 'rate_limit';
if (msg.includes('401') || msg.includes('unauthorized') || msg.includes('invalid.*key'))
if (msg.includes('401') || msg.includes('unauthorized') || /invalid.*key/.test(msg))

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings starting with 'invalid' and with many repetitions of 'invalid'.
@github-actions github-actions Bot merged commit 9d64ea9 into develop Mar 18, 2026
13 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:desktop app:web dependencies Pull requests that update a dependency file size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants