Skip to content

feat: ai features#11

Merged
ryansoe merged 1 commit intomainfrom
feat/ai-features
Mar 2, 2026
Merged

feat: ai features#11
ryansoe merged 1 commit intomainfrom
feat/ai-features

Conversation

@ryansoe
Copy link
Owner

@ryansoe ryansoe commented Mar 2, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added AI-powered quick edit for selected code with keyboard shortcut.
    • Introduced code suggestions displayed inline while editing.
    • Added contextual tooltip menu for selected text with quick actions.
    • Integrated toast notifications for user feedback.
  • Improvements

    • Text selection is now enabled throughout the application.
    • Updated dependencies for better performance.

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

📝 Walkthrough

Walkthrough

This PR introduces AI-powered code suggestions and quick-edit features for the editor through new backend API routes integrated with Claude, adds corresponding CodeMirror extensions with state management and fetching logic, replaces unused CodeMirror dependencies with the ky HTTP client, enables text selection via CSS, adds a toast notification component, and integrates cleanup logic for debounced updates.

Changes

Cohort / File(s) Summary
Dependency Management
package.json
Removed @codemirror/commands and @codemirror/view dependencies; added ky (^1.14.3) HTTP client library.
Backend API Routes
src/app/api/quick-edit/route.ts, src/app/api/suggestion/route.ts
Added POST handlers for AI-powered features: quick-edit performs Clerk auth, validates inputs, scrapes URLs from instructions via firecrawl, and uses Claude Haiku to edit selected code; suggestion handler authenticates, validates code input, builds a contextual prompt, and requests code suggestions from Claude with structured output schema.
UI & Layout
src/app/globals.css, src/app/layout.tsx
Removed select-none utility from body rule to enable text selection; imported and wrapped children with Sonner Toaster component for notifications.
Editor Core Components
src/features/editor/components/code-editor.tsx, src/features/editor/components/editor-view.tsx
Added new extensions (suggestion, quickEdit, selectionTooltip) to CodeEditor; consolidated duplicate indent-with-tab keybinding; added cleanup effect to cancel debounced update timeouts on activeTabId changes.
Quick Edit Extension
src/features/editor/extensions/quick-edit/fetcher.ts, src/features/editor/extensions/quick-edit/index.ts
Implemented complete quick-edit feature: fetcher validates payload and POST to /api/quick-edit with ky client (30s timeout, no retries); extension provides CodeMirror state field, tooltip UI with input and submit/cancel actions, Mod-k keyboard shortcut, and selection replacement logic with AbortController support.
Suggestion Extension
src/features/editor/extensions/suggestion/fetcher.ts, src/features/editor/extensions/suggestion/index.ts
Implemented code suggestion feature: fetcher validates and POSTs to /api/suggestion using ky (10s timeout); extension includes debounced fetch trigger, inline ghost-text widget rendering at cursor, Tab-key acceptance binding, and payload construction with file context (line numbers, surrounding lines, cursor position).
Selection Tooltip
src/features/editor/extensions/selection-tooltip.ts
Added CodeMirror extension rendering contextual tooltip on non-empty selection with "Add to Chat" and "Quick Edit" (⌘K shortcut) actions; tooltip hides when Quick Edit is active and captures EditorView instance via update listener.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Editor as Editor Component
    participant Suggestion as Suggestion Extension
    participant Fetcher as Suggestion Fetcher
    participant API as /api/suggestion Route
    participant Claude as Claude AI

    User->>Editor: Type code / move cursor
    Editor->>Suggestion: onChange / onSelectionChange
    Suggestion->>Suggestion: Debounce trigger (500ms)
    Suggestion->>Fetcher: fetcher(payload, signal)
    Fetcher->>API: POST /api/suggestion (ky request)
    API->>Claude: Generate suggestion (claude-haiku-4-5)
    Claude->>API: Return suggestion string
    API->>Fetcher: Response with suggestion
    Fetcher->>Suggestion: Return suggestion string
    Suggestion->>Suggestion: Update state, render widget
    Suggestion->>Editor: Display inline ghost text at cursor
    User->>Editor: Press Tab
    Editor->>Suggestion: Accept suggestion
    Suggestion->>Editor: Insert suggestion at cursor
Loading
sequenceDiagram
    participant User
    participant Editor as Editor View
    participant QuickEdit as Quick Edit Extension
    participant Tooltip as Selection Tooltip
    participant Fetcher as Quick Edit Fetcher
    participant API as /api/quick-edit Route
    participant Firecrawl as Firecrawl Service
    participant Claude as Claude AI

    User->>Editor: Select code
    Editor->>Tooltip: onSelectionChange
    Tooltip->>Tooltip: Render tooltip (Add to Chat, Quick Edit)
    User->>Tooltip: Click "Quick Edit" or Mod-k
    Tooltip->>QuickEdit: Trigger showQuickEditEffect
    QuickEdit->>QuickEdit: Display tooltip with input field
    User->>QuickEdit: Type instruction + Submit
    QuickEdit->>Fetcher: fetcher(selectedCode, fullCode, instruction, signal)
    Fetcher->>API: POST /api/quick-edit (ky request)
    API->>API: Extract URLs from instruction (regex)
    API->>Firecrawl: Scrape URLs (markdown format)
    Firecrawl->>API: Return scraped content
    API->>Claude: Generate edited code (claude-haiku-4-5)
    Claude->>API: Return editedCode
    API->>Fetcher: Response with editedCode
    Fetcher->>QuickEdit: Return editedCode
    QuickEdit->>Editor: Replace selection with editedCode
    QuickEdit->>QuickEdit: Hide tooltip
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #2: Modifies the same src/app/layout.tsx RootLayout component, adding different provider-related changes that may require coordination.

Poem

🐰 A rabbit hops through code with glee,
AI whispers suggestions so cleverly!
Quick edits bloom, tooltips appear,
Now editing's faster—no need to fear!
With ky and Claude, we hop so far,
Code quality shines like a brilliant star!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: ai features' is vague and generic; it does not clearly convey the specific changes or main objective of the pull request. Use a more descriptive title that highlights the primary feature or change, such as 'feat: add AI-powered code editing and suggestion capabilities' or 'feat: integrate AI quick-edit and suggestion extensions'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ai-features

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

Copy link

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

Caution

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

⚠️ Outside diff range comments (1)
package.json (1)

11-60: ⚠️ Potential issue | 🟠 Major

Add missing CodeMirror core package declarations to dependencies.

The code imports @codemirror/view and @codemirror/commands directly, but these are not declared in package.json. While they're currently installed as transitive dependencies from @codemirror/lang-* packages, this is fragile and can break under strict dependency resolution.

📦 Suggested patch
   "dependencies": {
+    "@codemirror/commands": "^6.10.2",
+    "@codemirror/view": "^6.39.15",
     "@codemirror/lang-css": "^6.3.1",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 11 - 60, Add explicit dependencies for the
CodeMirror core packages used directly: include "@codemirror/view" and
"@codemirror/commands" in package.json dependencies (use versions compatible
with the existing codemirror package, e.g., same major 6 range) so imports of
`@codemirror/view` and `@codemirror/commands` are not only transitive; after
updating package.json run the package manager to update lockfile (npm/yarn/pnpm)
to ensure deterministic installs.
🧹 Nitpick comments (3)
src/app/layout.tsx (1)

4-4: Prefer the project Toaster wrapper for consistency.

There is already a local wrapper (src/components/ui/sonner.tsx); importing from there keeps toast styling/theme behavior centralized.

Proposed fix
-import { Toaster } from "sonner";
+import { Toaster } from "@/components/ui/sonner";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/layout.tsx` at line 4, Replace the direct import of Toaster from the
external "sonner" package with the project's local Toaster wrapper to preserve
centralized styling and theme behavior: in src/app/layout.tsx remove the import
statement importing Toaster from "sonner" and instead import the Toaster export
from the local wrapper module (the file that defines the project wrapper, e.g.,
src/components/ui/sonner.tsx); ensure any usage of the Toaster component in
layout.tsx continues to reference the same symbol name (Toaster) so no further
JSX changes are required.
src/app/api/suggestion/route.ts (1)

6-6: Remove stale commented import.

This leftover debug/commented import adds noise and should be cleaned up.

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

In `@src/app/api/suggestion/route.ts` at line 6, Remove the stale commented import
line "// import { google } from "@ai-sdk/google";" from the suggestion route
file (route.ts) to clean up dead code and noise; simply delete that commented
import so only active imports remain in the file.
src/features/editor/extensions/suggestion/index.ts (1)

60-67: Remove unused generateFakeSuggestion.

This function is currently dead code in this module.

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

In `@src/features/editor/extensions/suggestion/index.ts` around lines 60 - 67,
Remove the dead helper function generateFakeSuggestion from this module: locate
the function declaration named generateFakeSuggestion (the const
generateFakeSuggestion = (textBeforeCursor: string): string | null => { ... })
and delete it along with any related unused imports or types; run a quick search
for generateFakeSuggestion to ensure nothing else references it and remove or
refactor those references if found, then run the TypeScript build/lint to
confirm no unused-symbol errors remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/api/quick-edit/route.ts`:
- Around line 45-63: The request body is only being checked for truthiness,
allowing non-string or malformed values to slip into prompt construction; update
the POST handler in route.ts to parse and normalize the body once (e.g., const
body = await request.json()), then validate that selectedCode, fullCode (if
used), and instruction are strings and non-empty after trimming, returning a 400
NextResponse.json with clear error messages when any field is missing or wrong
type; apply the same strict string/type checks and normalization logic for the
later validation block referenced around lines 94-97 so all prompt inputs are
sanitized before use.
- Around line 47-49: The userId authorization check in the API route returns a
400 status for unauthorized requests; update the early-return in route.ts (the
userId check in the handler that calls NextResponse.json) to return an
authentication-appropriate status (use 401 Unauthorized) instead of 400 so it is
semantically correct and consistent with the other AI route.
- Around line 65-90: The URL scraping block currently takes all matches from
URL_REGEX, scrapes them in parallel via firecrawl.scrapeUrl, and joins results
into documentationContext; change this to (1) deduplicate URLs (de-dup the urls
array), (2) validate each URL against a whitelist or safe URL check and ignore
invalid/externally disallowed hosts, (3) enforce a hard cap (e.g., MAX_URLS
constant) on the number of URLs processed and only take the first N after
dedupe/validation, and (4) limit concurrency instead of Promise.all over all
URLs (use a small worker/pool or sequential processing) and handle
timeouts/errors robustly before building documentationContext; update references
to URL_REGEX, urls, firecrawl.scrapeUrl, scrapedResults, validResults, and
documentationContext accordingly.

In `@src/app/api/suggestion/route.ts`:
- Around line 54-77: The handler reads request.json() into variables and
interpolates them into SUGGESTION_PROMPT without validating required fields
(only code is checked), which can cause runtime errors (e.g., calling
lineNumber.toString()). Add validation after the await request.json() for
required/optional fields (fileName, code, currentLine, previousLines,
textBeforeCursor, textAfterCursor, nextLines, lineNumber), returning
NextResponse.json({ error: ... }, { status: 400 }) for missing/invalid values;
coerce or default optional strings (previousLines, nextLines) to "" and ensure
lineNumber is a number (or call toString only after verifying) before using it
in the SUGGESTION_PROMPT.replace chain so SUGGESTION_PROMPT interpolation is
safe.

In `@src/app/globals.css`:
- Line 122: The Biome CSS parser isn't configured to understand Tailwind
directives (so lines like "@apply bg-background text-foreground" in globals.css
fail); update biome.json's css.parser to include "tailwindDirectives": true (and
keep css.linter.enabled true and css.parser.cssModules as needed) so the parser
accepts Tailwind directives; modify the css.parser section in biome.json
accordingly and save to enable proper linting/parsing of
`@apply/`@theme/@custom-variant usages.

In `@src/app/layout.tsx`:
- Around line 33-36: The Toaster is currently rendered inside Providers'
auth-gated children slot so it isn't mounted for unauthenticated/loading users;
move the <Toaster /> out of the auth-protected children render path—either
render <Toaster /> as a sibling to {children} but still inside <Providers>, or
place it inside <Providers> but outside the <Authenticated> wrapper (identify
the Providers component and where {children} is passed/rendered and relocate the
Toaster accordingly) so the toast host is always mounted app-wide.

In `@src/features/editor/components/editor-view.tsx`:
- Around line 22-29: The cleanup effect currently depends on activeTabId which
clears timeoutRef.current on tab switches and drops pending debounced autosaves;
change the effect to run cleanup only on unmount (empty dependency array) so
timeouts persist across tab switches, or alternatively, on activeTabId change
flush the pending save instead of clearing it; update the useEffect that
references timeoutRef.current to remove activeTabId from its deps (use []), or
replace the cleanup to call the debounced flush function (e.g.,
flushDebouncedSave or the save handler) when activeTabId changes so edits aren't
lost.

In `@src/features/editor/extensions/quick-edit/index.ts`:
- Around line 108-113: The submission currently captures selection text
(selection, selectedCode, fullCode) before async work and then uses those
pre-await offsets when applying the replacement, which can target stale ranges
and also drops valid empty-string edits by checking editedCode truthiness; fix
by storing the numeric positions (selection.from and selection.to) immediately
and use transaction.mapping (or re-resolve the positions from
editorView.state.doc using a current selection/pos mapping) when applying the
edit so you apply to the correct, current range, and change the edited check to
test for editedCode !== undefined (or editedCode !== null) instead of if
(editedCode) so empty strings are allowed; apply the same change to the second
occurrence handling around the block referenced by lines 128-138.
- Around line 18-20: The module-scope globals editorView and
currentAbortController can target the wrong editor when multiple instances
exist; remove these globals and make the state instance-scoped (e.g., attach
abort controller and view reference to the EditorView instance via a
WeakMap<EditorView, AbortController> or EditorView-specific plugin state) so
each editor instance has its own AbortController and view pointer; update all
usages (handlers like submit/cancel and functions that currently read editorView
or currentAbortController) to look up the instance-specific values (by the
EditorView passed into handlers or via the WeakMap/plugin state) so
cancel/submit/apply edits always operate on the correct editor instance.

In `@src/features/editor/extensions/selection-tooltip.ts`:
- Around line 29-33: The "Add to Chat" button (addToChatButton) is rendered but
has no click handler; attach a handler that invokes the app's selection-to-chat
flow (either by calling an existing callback like onAddToChat /
dispatchSelectionToChat or by dispatching a CustomEvent 'add-to-chat' with the
selected text) and make sure to preventDefault/stopPropagation; do the same for
the other actionable button mentioned (the one at lines 56-57) so both buttons
call the appropriate handler or emit the event and wire the parent component to
handle that event.
- Line 5: The module-level editorView variable and captureViewExtension
workaround are unsafe for multiple EditorView instances; update the Tooltip
create() implementation to use the create(view) parameter directly for event
handlers (e.g., the onclick handler) instead of reading or assigning editorView,
remove the editorView declaration and the captureViewExtension code, and ensure
any references to editorView (in selection-tooltip's create() and onclick) are
replaced with the local view parameter so handlers operate on the correct
EditorView instance.

In `@src/features/editor/extensions/suggestion/index.ts`:
- Around line 54-59: The three module-scoped mutable variables debounceTimer,
isWaitingForSuggestion, and currentAbortController must be made
per-editor-instance to avoid cross-instance interference; refactor so these are
stored per EditorView (e.g., in a WeakMap keyed by the editor instance or as
fields on an extension/plugin state) and update all uses (creation, cancelation,
and checks) to read/write via that per-instance storage instead of the
module-level debounceTimer/isWaitingForSuggestion/currentAbortController
identifiers (also update any helper functions that reference them to accept the
editor instance or retrieve the per-instance record).
- Around line 126-143: A race can let a slow earlier fetcher response overwrite
a newer suggestion; add sequencing by introducing a monotonically-incremented
request id (e.g., suggestionSeq/requestCounter) that you increment before
creating currentAbortController and calling fetcher, capture the id in the async
closure, and after awaiting fetcher check that the captured id matches the
latest request id; if it does not match, ignore the stale response and do not
dispatch setSuggestionEffect. Update/clear currentAbortController and
isWaitingForSuggestion only for the matching/latest request and keep using
existing symbols: debounceTimer, DEBOUNCE_DELAY, generatePayload,
currentAbortController, fetcher, isWaitingForSuggestion, and setSuggestionEffect
to locate and modify the code.

---

Outside diff comments:
In `@package.json`:
- Around line 11-60: Add explicit dependencies for the CodeMirror core packages
used directly: include "@codemirror/view" and "@codemirror/commands" in
package.json dependencies (use versions compatible with the existing codemirror
package, e.g., same major 6 range) so imports of `@codemirror/view` and
`@codemirror/commands` are not only transitive; after updating package.json run
the package manager to update lockfile (npm/yarn/pnpm) to ensure deterministic
installs.

---

Nitpick comments:
In `@src/app/api/suggestion/route.ts`:
- Line 6: Remove the stale commented import line "// import { google } from
"@ai-sdk/google";" from the suggestion route file (route.ts) to clean up dead
code and noise; simply delete that commented import so only active imports
remain in the file.

In `@src/app/layout.tsx`:
- Line 4: Replace the direct import of Toaster from the external "sonner"
package with the project's local Toaster wrapper to preserve centralized styling
and theme behavior: in src/app/layout.tsx remove the import statement importing
Toaster from "sonner" and instead import the Toaster export from the local
wrapper module (the file that defines the project wrapper, e.g.,
src/components/ui/sonner.tsx); ensure any usage of the Toaster component in
layout.tsx continues to reference the same symbol name (Toaster) so no further
JSX changes are required.

In `@src/features/editor/extensions/suggestion/index.ts`:
- Around line 60-67: Remove the dead helper function generateFakeSuggestion from
this module: locate the function declaration named generateFakeSuggestion (the
const generateFakeSuggestion = (textBeforeCursor: string): string | null => {
... }) and delete it along with any related unused imports or types; run a quick
search for generateFakeSuggestion to ensure nothing else references it and
remove or refactor those references if found, then run the TypeScript build/lint
to confirm no unused-symbol errors remain.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09d60a1 and 8e6761d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • package.json
  • src/app/api/quick-edit/route.ts
  • src/app/api/suggestion/route.ts
  • src/app/globals.css
  • src/app/layout.tsx
  • src/features/editor/components/code-editor.tsx
  • src/features/editor/components/editor-view.tsx
  • src/features/editor/extensions/quick-edit/fetcher.ts
  • src/features/editor/extensions/quick-edit/index.ts
  • src/features/editor/extensions/selection-tooltip.ts
  • src/features/editor/extensions/suggestion/fetcher.ts
  • src/features/editor/extensions/suggestion/index.ts

Comment on lines +45 to +63
const { selectedCode, fullCode, instruction } = await request.json();

if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 400 });
}

if (!selectedCode) {
return NextResponse.json(
{ error: "Selected code is required" },
{ status: 400 }
);
}

if (!instruction) {
return NextResponse.json(
{ error: "Instruction is required" },
{ status: 400 }
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the request body schema before prompt construction.

Current checks only test truthiness. Non-string payloads can pass and produce malformed prompt values at runtime. Parse and normalize the body once.

🛡️ Suggested fix
+const quickEditRequestSchema = z.object({
+  selectedCode: z.string().min(1),
+  fullCode: z.string().default(""),
+  instruction: z.string().min(1),
+});
+
 export async function POST(request: Request) {
   try {
     const { userId } = await auth();
-    const { selectedCode, fullCode, instruction } = await request.json();
+    const { selectedCode, fullCode, instruction } =
+      quickEditRequestSchema.parse(await request.json());
@@
-    if (!selectedCode) {
-      return NextResponse.json(
-        { error: "Selected code is required" },
-        { status: 400 }
-      );
-    }
-
-    if (!instruction) {
-      return NextResponse.json(
-        { error: "Instruction is required" },
-        { status: 400 }
-      );
-    }

Also applies to: 94-97

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

In `@src/app/api/quick-edit/route.ts` around lines 45 - 63, The request body is
only being checked for truthiness, allowing non-string or malformed values to
slip into prompt construction; update the POST handler in route.ts to parse and
normalize the body once (e.g., const body = await request.json()), then validate
that selectedCode, fullCode (if used), and instruction are strings and non-empty
after trimming, returning a 400 NextResponse.json with clear error messages when
any field is missing or wrong type; apply the same strict string/type checks and
normalization logic for the later validation block referenced around lines 94-97
so all prompt inputs are sanitized before use.

Comment on lines +47 to +49
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 400 });
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use an auth-appropriate status code for unauthorized access.

Returning 400 for unauthorized requests is semantically incorrect and inconsistent with your other AI route.

🔧 Suggested fix
-      return NextResponse.json({ error: "Unauthorized" }, { status: 400 });
+      return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
📝 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
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 400 });
}
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/quick-edit/route.ts` around lines 47 - 49, The userId
authorization check in the API route returns a 400 status for unauthorized
requests; update the early-return in route.ts (the userId check in the handler
that calls NextResponse.json) to return an authentication-appropriate status
(use 401 Unauthorized) instead of 400 so it is semantically correct and
consistent with the other AI route.

Comment on lines +65 to +90
const urls: string[] = instruction.match(URL_REGEX) || [];
let documentationContext = "";

if (urls.length > 0) {
const scrapedResults = await Promise.all(
urls.map(async (url) => {
try {
const result = await firecrawl.scrapeUrl(url, {
formats: ["markdown"],
});

if (result.success && result.markdown) {
return `<doc url="${url}">\n${result.markdown}\n</doc>`;
}

return null;
} catch {
return null;
}
})
);

const validResults = scrapedResults.filter(Boolean);

if (validResults.length > 0) {
documentationContext = `<documentation>\n${validResults.join("\n\n")}\n</documentation>`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's view the full file to understand context
wc -l src/app/api/quick-edit/route.ts

Repository: ryansoe/kairos

Length of output: 93


🏁 Script executed:

# View the relevant section and surrounding code
sed -n '1,100p' src/app/api/quick-edit/route.ts

Repository: ryansoe/kairos

Length of output: 2776


🏁 Script executed:

# Search for URL_REGEX definition and usage
rg -n 'URL_REGEX|const.*REGEX' src/app/api/quick-edit/route.ts

Repository: ryansoe/kairos

Length of output: 170


🏁 Script executed:

# Check for any existing URL validation or rate limiting in the file
rg -n 'filter|slice|Set|validate|limit|rate' src/app/api/quick-edit/route.ts

Repository: ryansoe/kairos

Length of output: 259


🏁 Script executed:

# Look for firecrawl configuration and limits
rg -n 'firecrawl' src/app/api/quick-edit/route.ts -A 5 -B 2

Repository: ryansoe/kairos

Length of output: 608


Enforce URL limits and deduplication to prevent abuse of web scraping.

User-supplied instructions can include unlimited URLs that are scraped in parallel without deduplication or validation, allowing a single instruction to trigger dozens of concurrent outbound requests and excessive prompt bloat.

Suggested hardening
-    const urls: string[] = instruction.match(URL_REGEX) || [];
+    const urls = [...new Set(instruction.match(URL_REGEX) || [])]
+      .slice(0, 3)
+      .filter((raw) => {
+        try {
+          const u = new URL(raw);
+          return u.protocol === "https:" || u.protocol === "http:";
+        } catch {
+          return false;
+        }
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/quick-edit/route.ts` around lines 65 - 90, The URL scraping block
currently takes all matches from URL_REGEX, scrapes them in parallel via
firecrawl.scrapeUrl, and joins results into documentationContext; change this to
(1) deduplicate URLs (de-dup the urls array), (2) validate each URL against a
whitelist or safe URL check and ignore invalid/externally disallowed hosts, (3)
enforce a hard cap (e.g., MAX_URLS constant) on the number of URLs processed and
only take the first N after dedupe/validation, and (4) limit concurrency instead
of Promise.all over all URLs (use a small worker/pool or sequential processing)
and handle timeouts/errors robustly before building documentationContext; update
references to URL_REGEX, urls, firecrawl.scrapeUrl, scrapedResults,
validResults, and documentationContext accordingly.

Comment on lines +54 to +77
const {
fileName,
code,
currentLine,
previousLines,
textBeforeCursor,
textAfterCursor,
nextLines,
lineNumber,
} = await request.json();

if (!code) {
return NextResponse.json({ error: "Code is required" }, { status: 400 });
}

const prompt = SUGGESTION_PROMPT.replace("{fileName}", fileName)
.replace("{code}", code)
.replace("{currentLine}", currentLine)
.replace("{previousLines}", previousLines || "")
.replace("{textBeforeCursor}", textBeforeCursor)
.replace("{textAfterCursor}", textAfterCursor)
.replace("{nextLines}", nextLines || "")
.replace("{lineNumber}", lineNumber.toString());

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate all request fields before prompt interpolation.

Only code is checked, but lineNumber, cursor context, and line blocks are used directly. Invalid/missing fields can trigger runtime failures (e.g., lineNumber.toString()).

✅ Suggested fix
+const suggestionRequestSchema = z.object({
+  fileName: z.string(),
+  code: z.string().min(1),
+  currentLine: z.string(),
+  previousLines: z.string().default(""),
+  textBeforeCursor: z.string(),
+  textAfterCursor: z.string(),
+  nextLines: z.string().default(""),
+  lineNumber: z.number(),
+});
+
 export async function POST(request: Request) {
   try {
@@
-    const {
+    const {
       fileName,
       code,
       currentLine,
       previousLines,
       textBeforeCursor,
       textAfterCursor,
       nextLines,
       lineNumber,
-    } = await request.json();
-
-    if (!code) {
-      return NextResponse.json({ error: "Code is required" }, { status: 400 });
-    }
+    } = suggestionRequestSchema.parse(await request.json());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/suggestion/route.ts` around lines 54 - 77, The handler reads
request.json() into variables and interpolates them into SUGGESTION_PROMPT
without validating required fields (only code is checked), which can cause
runtime errors (e.g., calling lineNumber.toString()). Add validation after the
await request.json() for required/optional fields (fileName, code, currentLine,
previousLines, textBeforeCursor, textAfterCursor, nextLines, lineNumber),
returning NextResponse.json({ error: ... }, { status: 400 }) for missing/invalid
values; coerce or default optional strings (previousLines, nextLines) to "" and
ensure lineNumber is a number (or call toString only after verifying) before
using it in the SUGGESTION_PROMPT.replace chain so SUGGESTION_PROMPT
interpolation is safe.

}
body {
@apply bg-background text-foreground select-none;
@apply bg-background text-foreground;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate Biome config =="
configs="$(fd -HI 'biome\.jsonc?$' || true)"
if [ -z "$configs" ]; then
  echo "No biome.json/biome.jsonc found."
else
  echo "$configs"
  echo "== Full Biome config content =="
  echo "$configs" | xargs cat
fi

echo ""
echo "== Tailwind directives in globals.css =="
rg -n '@apply|@theme|@custom-variant' src/app/globals.css || echo "No Tailwind directives found"

echo ""
echo "== Check globals.css around line 122 =="
sed -n '115,130p' src/app/globals.css

Repository: ryansoe/kairos

Length of output: 2534


🌐 Web query:

Biome CSS parser Tailwind @apply @theme support configuration

💡 Result:

Biome’s CSS parser can parse Tailwind-specific directives like @apply, @theme, and @utility, but it’s opt-in.

Enable Tailwind directives parsing

Add this to your biome.json:

{
  "css": {
    "parser": {
      "tailwindDirectives": true
    }
  }
}

This enables parsing of Tailwind syntax (including @theme, @utility, @apply). [1][2][3]

Optional: enable CSS formatting (separate switch)

{
  "css": {
    "formatter": {
      "enabled": true
    }
  }
}

(css.formatter.enabled is independent and defaults to false.) [1]

Note

This setting helps Biome accept/format Tailwind syntax, but it does not replace Tailwind’s build step (it won’t “apply” utilities into generated CSS by itself). [2][3]

Sources: [1] Biome configuration reference, [2] Biome v2.3 blog (Tailwind v4 support), [3] Biome changelog entry for 2.3.0 (tailwindDirectives option).


Add tailwindDirectives: true to Biome's CSS parser configuration.

The Biome config lacks explicit Tailwind support. Add this to biome.json:

"css": {
  "linter": {
    "enabled": true
  },
  "parser": {
    "cssModules": true,
    "tailwindDirectives": true
  }
}

Without this setting, Biome's CSS linter cannot properly parse the Tailwind directives (@apply, @theme, @custom-variant) used throughout globals.css, including line 122.

🧰 Tools
🪛 Biome (2.4.4)

[error] 122-122: Tailwind-specific syntax is disabled.

(parse)

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

In `@src/app/globals.css` at line 122, The Biome CSS parser isn't configured to
understand Tailwind directives (so lines like "@apply bg-background
text-foreground" in globals.css fail); update biome.json's css.parser to include
"tailwindDirectives": true (and keep css.linter.enabled true and
css.parser.cssModules as needed) so the parser accepts Tailwind directives;
modify the css.parser section in biome.json accordingly and save to enable
proper linting/parsing of `@apply/`@theme/@custom-variant usages.

Comment on lines +108 to +113
const selection = editorView.state.selection.main;
const selectedCode = editorView.state.doc.sliceString(
selection.from,
selection.to
);
const fullCode = editorView.state.doc.toString();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Submit path uses stale selection and drops valid empty-string edits.

The replacement uses pre-await offsets, so in-flight document changes can make the apply step target stale ranges. Also, if (editedCode) rejects legitimate "" results.

🔧 Suggested fix
           const selection = editorView.state.selection.main;
+          const from = selection.from;
+          const to = selection.to;
           const selectedCode = editorView.state.doc.sliceString(
-            selection.from,
-            selection.to
+            from,
+            to
           );
+          const baseDoc = editorView.state.doc;
           const fullCode = editorView.state.doc.toString();
           ...
-          if (editedCode) {
+          if (editedCode !== null) {
+            if (editorView.state.doc !== baseDoc) {
+              submitButton.disabled = false;
+              submitButton.textContent = "Submit";
+              return;
+            }
             editorView.dispatch({
               changes: {
-                from: selection.from,
-                to: selection.to,
+                from,
+                to,
                 insert: editedCode,
               },
-              selection: { anchor: selection.from + editedCode.length },
+              selection: { anchor: from + editedCode.length },
               effects: showQuickEditEffect.of(false),
             });

Also applies to: 128-138

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

In `@src/features/editor/extensions/quick-edit/index.ts` around lines 108 - 113,
The submission currently captures selection text (selection, selectedCode,
fullCode) before async work and then uses those pre-await offsets when applying
the replacement, which can target stale ranges and also drops valid empty-string
edits by checking editedCode truthiness; fix by storing the numeric positions
(selection.from and selection.to) immediately and use transaction.mapping (or
re-resolve the positions from editorView.state.doc using a current selection/pos
mapping) when applying the edit so you apply to the correct, current range, and
change the edited check to test for editedCode !== undefined (or editedCode !==
null) instead of if (editedCode) so empty strings are allowed; apply the same
change to the second occurrence handling around the block referenced by lines
128-138.

import { StateField, EditorState } from "@codemirror/state";
import { showQuickEditEffect, quickEditState } from "./quick-edit";

let editorView: EditorView | null = null;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's see the full content of the file to understand the structure
cat -n src/features/editor/extensions/selection-tooltip.ts

Repository: ryansoe/kairos

Length of output: 3496


🏁 Script executed:

# Search for how create() method signature works in Codemirror contexts
rg -B2 -A5 'create\s*\(' src/features/editor/extensions/selection-tooltip.ts

Repository: ryansoe/kairos

Length of output: 627


🏁 Script executed:

# Check how selectionTooltip is used in the codebase
rg -B2 -A2 'selectionTooltip' src/features/editor/

Repository: ryansoe/kairos

Length of output: 1680


🌐 Web query:

Codemirror Tooltip interface create method signature view parameter

💡 Result:

In CodeMirror 6, the Tooltip interface’s create method signature is:

create(view: EditorView): TooltipView

So the view parameter is the EditorView instance the tooltip is being created for (use it to read state, coords, dispatch, access DOM, etc.). [1][3]

Sources: CodeMirror reference manual (Tooltips → interface Tooltip) [1], and @codemirror/view type docs showing the same signature [3].


Use the view parameter from create() instead of module-level editorView.

The module-level editorView is unsafe with multiple editor instances and can reference a stale or destroyed view. Codemirror's Tooltip create() method receives the current EditorView as a parameter—use it directly in the onclick handler and remove the captureViewExtension workaround.

Suggested fix
-import { Tooltip, showTooltip, EditorView } from "@codemirror/view";
+import { Tooltip, showTooltip, EditorView } from "@codemirror/view";
 import { StateField, EditorState } from "@codemirror/state";
 import { showQuickEditEffect, quickEditState } from "./quick-edit";
-
-let editorView: EditorView | null = null;
@@
-      create() {
+      create(view) {
@@
-        quickEditButton.onclick = () => {
-          if (editorView) {
-            editorView.dispatch({
-              effects: showQuickEditEffect.of(true),
-            });
-          }
-        };
+        quickEditButton.onclick = () => {
+          view.dispatch({
+            effects: showQuickEditEffect.of(true),
+          });
+        };
@@
-const captureViewExtension = EditorView.updateListener.of((update) => {
-  editorView = update.view;
-});
-
 export const selectionTooltip = () => [
   selectionTooltipField,
-  captureViewExtension,
 ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/editor/extensions/selection-tooltip.ts` at line 5, The
module-level editorView variable and captureViewExtension workaround are unsafe
for multiple EditorView instances; update the Tooltip create() implementation to
use the create(view) parameter directly for event handlers (e.g., the onclick
handler) instead of reading or assigning editorView, remove the editorView
declaration and the captureViewExtension code, and ensure any references to
editorView (in selection-tooltip's create() and onclick) are replaced with the
local view parameter so handlers operate on the correct EditorView instance.

Comment on lines +29 to +33
const addToChatButton = document.createElement("button");
addToChatButton.textContent = "Add to Chat";
addToChatButton.className =
"font-sans p-1 px-2 hover:bg-foreground/10 rounded-sm";

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add to Chat is currently a no-op action.

The button is rendered as an actionable control but has no handler, which is a broken interaction.

I can help wire this to a state effect/callback and draft the companion issue if you want.

Also applies to: 56-57

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

In `@src/features/editor/extensions/selection-tooltip.ts` around lines 29 - 33,
The "Add to Chat" button (addToChatButton) is rendered but has no click handler;
attach a handler that invokes the app's selection-to-chat flow (either by
calling an existing callback like onAddToChat / dispatchSelectionToChat or by
dispatching a CustomEvent 'add-to-chat' with the selected text) and make sure to
preventDefault/stopPropagation; do the same for the other actionable button
mentioned (the one at lines 56-57) so both buttons call the appropriate handler
or emit the event and wire the parent component to handle that event.

Comment on lines +54 to +59
let debounceTimer: number | null = null;
let isWaitingForSuggestion = false;
const DEBOUNCE_DELAY = 300;

let currentAbortController: AbortController | null = null;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Isolate suggestion runtime state per editor instance.

debounceTimer, isWaitingForSuggestion, and currentAbortController are module-scoped, so separate editor instances can cancel or hide each other’s suggestion flow.

🔧 Suggested fix
-let debounceTimer: number | null = null;
-let isWaitingForSuggestion = false;
-let currentAbortController: AbortController | null = null;
+type SuggestionRuntime = {
+  debounceTimer: number | null;
+  isWaitingForSuggestion: boolean;
+  currentAbortController: AbortController | null;
+};

-const createDebouncePlugin = (fileName: string) => {
+const createDebouncePlugin = (fileName: string, runtime: SuggestionRuntime) => {
   return ViewPlugin.fromClass(
     class {
       triggerSuggestion(view: EditorView) {
-        if (debounceTimer !== null) clearTimeout(debounceTimer);
-        if (currentAbortController !== null) currentAbortController.abort();
-        isWaitingForSuggestion = true;
+        if (runtime.debounceTimer !== null) clearTimeout(runtime.debounceTimer);
+        if (runtime.currentAbortController !== null) runtime.currentAbortController.abort();
+        runtime.isWaitingForSuggestion = true;
         ...
       }
     }
   );
 };

-const renderPlugin = ViewPlugin.fromClass(...)
+const createRenderPlugin = (runtime: SuggestionRuntime) => ViewPlugin.fromClass(...)

 export const suggestion = (fileName: string) => [
-  suggestionState,
-  createDebouncePlugin(fileName),
-  renderPlugin,
-  acceptSuggestionKeymap,
-];
+  const runtime: SuggestionRuntime = {
+    debounceTimer: null,
+    isWaitingForSuggestion: false,
+    currentAbortController: null,
+  };
+  return [
+    suggestionState,
+    createDebouncePlugin(fileName, runtime),
+    createRenderPlugin(runtime),
+    acceptSuggestionKeymap,
+  ];
+};

Also applies to: 102-157, 159-207, 229-234

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

In `@src/features/editor/extensions/suggestion/index.ts` around lines 54 - 59, The
three module-scoped mutable variables debounceTimer, isWaitingForSuggestion, and
currentAbortController must be made per-editor-instance to avoid cross-instance
interference; refactor so these are stored per EditorView (e.g., in a WeakMap
keyed by the editor instance or as fields on an extension/plugin state) and
update all uses (creation, cancelation, and checks) to read/write via that
per-instance storage instead of the module-level
debounceTimer/isWaitingForSuggestion/currentAbortController identifiers (also
update any helper functions that reference them to accept the editor instance or
retrieve the per-instance record).

Comment on lines +126 to +143
debounceTimer = window.setTimeout(async () => {
const payload = generatePayload(view, fileName);
if (!payload) {
isWaitingForSuggestion = false;
view.dispatch({ effects: setSuggestionEffect.of(null) });
return;
}
currentAbortController = new AbortController();
const suggestion = await fetcher(
payload,
currentAbortController.signal
);

isWaitingForSuggestion = false;
view.dispatch({
effects: setSuggestionEffect.of(suggestion),
});
}, DEBOUNCE_DELAY);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against stale async completions before dispatch.

A slower earlier request can still resolve after a newer trigger and overwrite fresher suggestion text. Add request sequencing and ignore out-of-date responses.

🔧 Suggested fix
 class {
+  private requestSeq = 0;

   triggerSuggestion(view: EditorView) {
+    const requestId = ++this.requestSeq;
     ...
     debounceTimer = window.setTimeout(async () => {
       ...
       const suggestion = await fetcher(payload, currentAbortController.signal);
+      if (requestId !== this.requestSeq) return;

       view.dispatch({
         effects: setSuggestionEffect.of(suggestion),
       });
     }, DEBOUNCE_DELAY);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/editor/extensions/suggestion/index.ts` around lines 126 - 143, A
race can let a slow earlier fetcher response overwrite a newer suggestion; add
sequencing by introducing a monotonically-incremented request id (e.g.,
suggestionSeq/requestCounter) that you increment before creating
currentAbortController and calling fetcher, capture the id in the async closure,
and after awaiting fetcher check that the captured id matches the latest request
id; if it does not match, ignore the stale response and do not dispatch
setSuggestionEffect. Update/clear currentAbortController and
isWaitingForSuggestion only for the matching/latest request and keep using
existing symbols: debounceTimer, DEBOUNCE_DELAY, generatePayload,
currentAbortController, fetcher, isWaitingForSuggestion, and setSuggestionEffect
to locate and modify the code.

@ryansoe ryansoe merged commit 01adacf into main Mar 2, 2026
16 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