Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c128676
chore(release): v0.9.0 (#149)
tomymaritano Mar 13, 2026
e3a4058
ci: add auto-tag workflow missing from main (#153)
tomymaritano Mar 13, 2026
e5dc625
chore: release v0.9.0 (#155)
tomymaritano Mar 14, 2026
afc8d32
chore: sync develop with main after v0.9.0 release
tomymaritano Mar 14, 2026
1cc0044
docs: add ai-core provider abstraction spec and implementation plan
tomymaritano Mar 14, 2026
f65830b
chore: scaffold packages/ai-core package
tomymaritano Mar 14, 2026
4d7f7a5
feat(ai-core): define LLMEvent protocol and chat types
tomymaritano Mar 14, 2026
38dd2cf
fix(ai-core): add DOM lib for AbortSignal/ReadableStream and clean un…
tomymaritano Mar 14, 2026
a992d06
feat(ai-core): add LLMProvider interface and ProviderRegistry
tomymaritano Mar 14, 2026
10588b7
feat(ai-core): add ContextBuilder with token budgeting and trimming
tomymaritano Mar 14, 2026
2951ebe
feat(ai-core): add retry logic with exponential backoff and jitter
tomymaritano Mar 14, 2026
2781424
feat(ai-core): add reusable SSE stream parser
tomymaritano Mar 14, 2026
42fb7e5
feat(ai-core): add AnthropicProvider with SSE streaming
tomymaritano Mar 14, 2026
30e9268
feat(ai-core): add AIService orchestrator with context building and r…
tomymaritano Mar 14, 2026
e26efbc
feat(desktop): wire ai-core IPC bridge with streaming and batching
tomymaritano Mar 14, 2026
b40fe5f
feat(preload): expose streaming AI chat API via IPC
tomymaritano Mar 14, 2026
71c2b31
feat(desktop): migrate AI panel to streaming with provider abstraction
tomymaritano Mar 14, 2026
2f0837c
chore: remove deprecated @readied/ai-assistant package
tomymaritano Mar 14, 2026
f803742
fix(desktop): unify AI panel entry points and fix panel height
tomymaritano Mar 14, 2026
b504cd5
fix: pin @types/react to 18.x across monorepo via pnpm overrides
tomymaritano Mar 14, 2026
3629486
chore: merge develop and resolve conflicts
tomymaritano Mar 14, 2026
349b7de
docs: update CLAUDE.md with AI architecture, branch hygiene, and type…
tomymaritano Mar 14, 2026
ede2504
Merge remote-tracking branch 'remotes/origin/feature/ai-core-provider…
tomymaritano Mar 18, 2026
c8eb00c
docs: add AI tool use spec and implementation plan
tomymaritano Mar 18, 2026
dc62cfd
feat(ai-core): add tool_use/tool_result ContentPart variants and stop…
tomymaritano Mar 18, 2026
28ea1ef
feat(ai-core): add ToolRegistry with register/unregister/getDefinitions
tomymaritano Mar 18, 2026
53cf70c
feat(ai-core): parse tool_use SSE events and emit stop reason in Anth…
tomymaritano Mar 18, 2026
e45f442
feat(ai-core): implement runToolLoop orchestrator for multi-turn tool…
tomymaritano Mar 18, 2026
6e1a026
feat(ai-core): add chatWithTools method to AIService with tool loop i…
tomymaritano Mar 18, 2026
0f502d2
feat(desktop): extend IPC bridge with tool loop support and confirmat…
tomymaritano Mar 18, 2026
2111ea0
feat(desktop): register built-in AI tools (search, read, list, create)
tomymaritano Mar 18, 2026
2fed12a
feat(desktop): integrate tool use events and ToolCallBlock into AiPanel
tomymaritano Mar 18, 2026
e349b3f
feat(desktop): add renderer-executed tools (insert_text, replace_sele…
tomymaritano Mar 18, 2026
833ecf9
fix(ai): resolve CodeRabbit review issues (critical + major + minor)
tomymaritano Mar 18, 2026
e46c74f
fix(ai-core): use regex instead of literal string for auth error dete…
tomymaritano Mar 18, 2026
caf0c56
chore: merge develop into feature/ai-tool-use
tomymaritano Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,7 @@
"Bash(npx vitest:*)",
"Bash(do echo:*)",
"Bash(do gh:*)",
"Bash(npx tsc:*)",
"Bash(git stash:*)",
"Bash(git rm:*)",
"Bash(turbo typecheck:*)",
"Bash(npx turbo:*)"
"Bash(git tag:*)"
]
}
}
64 changes: 63 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
```
apps/
desktop/ # Electron app (main, preload, renderer)
docs-site/ # VitePress documentation
web/ # Next.js marketing site + docs
packages/
ai-core/ # Provider-agnostic AI: streaming, LLM providers, context builder
core/ # Domain logic + markdown parsing
command-registry/ # Command palette registry
plugin-api/ # Plugin system interfaces
storage-core/ # Storage interfaces (pure TS)
storage-sqlite/ # SQLite adapter (peerDep for better-sqlite3)
licensing/ # License validation
product-config/ # Product configuration
sync-core/ # Sync engine
```

## Commands
Expand Down Expand Up @@ -67,6 +71,12 @@ Pattern for workspace packages with native deps:
}
```

## Type Version Alignment

`@types/react` is pinned to `18.3.27` via `pnpm.overrides` in root `package.json`. This prevents type mismatches when packages like `lucide-react` resolve a different `@types/react` version than the app uses.

**If you see `'X' cannot be used as a JSX component` errors:** Check that `pnpm.overrides` in root `package.json` still pins `@types/react` to match the React version used by `apps/desktop`.

## Testing

- `pnpm test` runs all tests **except** storage-sqlite (safe to run always)
Expand Down Expand Up @@ -138,6 +148,13 @@ git pull origin develop
git checkout -b feature/my-feature
```

**Keep branch in sync (do this daily or before pushing):**

```bash
git fetch origin develop
git rebase origin/develop
```

**Creating PR:**

```bash
Expand All @@ -153,6 +170,16 @@ git pull origin develop
git branch -d feature/my-feature
```

### Branch Hygiene (Critical)

Long-lived branches cause painful merge conflicts. Follow these rules:

- **Rebase daily:** `git fetch origin develop && git rebase origin/develop` before starting work each day
- **Small PRs:** Prefer 3 small PRs over 1 large one. Split by layer (types → logic → UI)
- **Max branch lifetime:** 2-3 days. If work takes longer, split into incremental PRs
- **Don't touch unrelated files:** Avoid changes to `package.json`, lockfiles, or `apps/web` unless that's the PR's purpose — these are high-conflict files
- **Rebase before pushing:** Always rebase against latest develop before `git push` to catch conflicts early

### Commit Messages

Use conventional commits:
Expand Down Expand Up @@ -251,6 +278,41 @@ case 'tag':
- [ ] Sidebar uses `useNavigation()` hook
- [ ] No implicit flags (`!== null`)

## AI Architecture

The AI system lives in `packages/ai-core` with a provider-agnostic, streaming-first design.

```
Renderer (AiPanel) → IPC → Main (ipc-ai.ts) → AIService → ProviderRegistry → Provider → SSE stream
← batched LLMEvents (text/error/done) ←
```
Comment on lines +285 to +288

@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 a language to the fenced code block.

The fence opened at Line 265 has no language identifier (MD040).

📝 Proposed fix
-```
+```text
Renderer (AiPanel) → IPC → Main (ipc-ai.ts) → AIService → ProviderRegistry → Provider → SSE stream
                   ← batched LLMEvents (text/error/done) ←
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 265-265: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@CLAUDE.md` around lines 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.

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 — cosmetic, will fix separately.

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 51 seconds before sending another message.


**Key packages and files:**

- `packages/ai-core/` — LLMProvider interface, ProviderRegistry, AnthropicProvider, ContextBuilder, AIService
- `apps/desktop/src/main/ai/ipc-ai.ts` — IPC bridge, 50ms batched event streaming
- `apps/desktop/src/preload/index.ts` — `window.readied.ai` API (chat, onEvent, cancel)
- `apps/desktop/src/renderer/components/ai/AiPanel.tsx` — Chat UI with streaming

**Adding a new LLM provider:**

1. Create `packages/ai-core/src/providers/my-provider.ts` implementing `LLMProvider`
2. Register in `ProviderRegistry` at `apps/desktop/src/main/ai/ipc-ai.ts`
3. Add option to `apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx`

**Key types:**

- `LLMEvent` — Protocol: `text` (delta), `error` (with code), `done`, `tool_call`, `tool_result`
- `ChatOptions` — Provider, model, messages, tools, maxTokens
- `LLMProvider` — `chat(options): AsyncGenerator<LLMEvent>` + `models()` + `validateKey()`

**Rules:**

- **No SDK dependencies in ai-core:** Providers use native `fetch` + SSE parsing
- **Streaming only:** No request/response pattern — everything streams via `LLMEvent`
- **Single panel instance:** Both Cmd+K and Sparkles button toggle the same AiPanel in App.tsx via CustomEvent (`readied:ai:toggle-panel`)
- **Settings store is source of truth:** API key, model, and provider come from Zustand settings store (`selectAi` selector), not plugin config

## Documentation

- **Architecture decisions:** `plan.md`
Expand Down
140 changes: 140 additions & 0 deletions apps/desktop/src/main/ai/built-in-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// apps/desktop/src/main/ai/built-in-tools.ts
import type { ToolRegistry } from '@readied/ai-core';

/**
* Register built-in AI tools for note operations.
*
* Read tools (auto-execute): search_notes, read_note, list_notebooks
* Write tools (require confirmation): create_note, insert_text, replace_selection
*
* insert_text and replace_selection execute in the renderer (they need editor access).
* They delegate via IPC: main → renderer → main.
*/
export function registerBuiltInTools(
registry: ToolRegistry,
deps: {
searchNotes: (
query: string,
limit?: number
) => Promise<Array<{ id: string; title: string; snippet: string }>>;
readNote: (id: string) => Promise<{ id: string; title: string; content: string } | null>;
listNotebooks: () => Promise<Array<{ id: string; name: string; noteCount: number }>>;
createNote: (title: string, content: string, notebookId?: string) => Promise<{ id: string }>;
}
): void {
registry.register({
name: 'search_notes',
description: 'Search notes by keyword query. Returns matching note IDs, titles, and snippets.',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
limit: { type: 'number', description: 'Max results (default 10)' },
},
required: ['query'],
},
requiresConfirmation: false,
execute: async args => {
const results = await deps.searchNotes(args.query as string, (args.limit as number) ?? 10);
return { ok: true, content: JSON.stringify(results) };
},
});
Comment on lines +37 to +41

@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?


registry.register({
name: 'read_note',
description: 'Read the full content of a note by its ID.',
parameters: {
type: 'object',
properties: {
id: { type: 'string', description: 'Note ID' },
},
required: ['id'],
},
requiresConfirmation: false,
execute: async args => {
const note = await deps.readNote(args.id as string);
if (!note) return { ok: false, content: 'Note not found', error: 'Note not found' };
return { ok: true, content: JSON.stringify(note) };
},
});

registry.register({
name: 'list_notebooks',
description: 'List all notebooks with their names and note counts.',
parameters: {
type: 'object',
properties: {},
},
requiresConfirmation: false,
execute: async () => {
const notebooks = await deps.listNotebooks();
return { ok: true, content: JSON.stringify(notebooks) };
},
});

registry.register({
name: 'create_note',
description: 'Create a new note in a notebook.',
parameters: {
type: 'object',
properties: {
title: { type: 'string', description: 'Note title' },
content: { type: 'string', description: 'Note content in markdown' },
notebookId: {
type: 'string',
description: 'Target notebook ID (optional, uses default)',
},
},
required: ['title', 'content'],
},
requiresConfirmation: true,
execute: async args => {
const result = await deps.createNote(
args.title as string,
args.content as string,
args.notebookId as string | undefined
);
return { ok: true, content: JSON.stringify(result) };
},
});

registry.register({
name: 'insert_text',
description: 'Insert text into the current note at the cursor position or at the end.',
parameters: {
type: 'object',
properties: {
text: { type: 'string', description: 'Text to insert' },
position: {
type: 'string',
description: "Where to insert: 'cursor' (default) or 'end'",
},
},
required: ['text'],
},
requiresConfirmation: true,
rendererOnly: true,
execute: async () => {
// Execution handled by ipc-ai.ts via executeToolInRenderer
return { ok: false, content: 'Not reachable', error: 'Renderer tool called without IPC' };
},
});

registry.register({
name: 'replace_selection',
description: 'Replace the currently selected text in the editor.',
parameters: {
type: 'object',
properties: {
text: { type: 'string', description: 'Replacement text' },
},
required: ['text'],
},
requiresConfirmation: true,
rendererOnly: true,
execute: async () => {
// Execution handled by ipc-ai.ts via executeToolInRenderer
return { ok: false, content: 'Not reachable', error: 'Renderer tool called without IPC' };
},
});
}
Loading
Loading