Conversation
📝 WalkthroughWalkthroughThis 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 | 🟠 MajorAdd missing CodeMirror core package declarations to dependencies.
The code imports
@codemirror/viewand@codemirror/commandsdirectly, 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 projectToasterwrapper 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 unusedgenerateFakeSuggestion.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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (12)
package.jsonsrc/app/api/quick-edit/route.tssrc/app/api/suggestion/route.tssrc/app/globals.csssrc/app/layout.tsxsrc/features/editor/components/code-editor.tsxsrc/features/editor/components/editor-view.tsxsrc/features/editor/extensions/quick-edit/fetcher.tssrc/features/editor/extensions/quick-edit/index.tssrc/features/editor/extensions/selection-tooltip.tssrc/features/editor/extensions/suggestion/fetcher.tssrc/features/editor/extensions/suggestion/index.ts
| 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 } | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| if (!userId) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 400 }); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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>`; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's view the full file to understand context
wc -l src/app/api/quick-edit/route.tsRepository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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 2Repository: 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.
| 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()); | ||
|
|
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
🧩 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.cssRepository: 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.
| const selection = editorView.state.selection.main; | ||
| const selectedCode = editorView.state.doc.sliceString( | ||
| selection.from, | ||
| selection.to | ||
| ); | ||
| const fullCode = editorView.state.doc.toString(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
🧩 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.tsRepository: 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.tsRepository: 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): TooltipViewSo 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.
| const addToChatButton = document.createElement("button"); | ||
| addToChatButton.textContent = "Add to Chat"; | ||
| addToChatButton.className = | ||
| "font-sans p-1 px-2 hover:bg-foreground/10 rounded-sm"; | ||
|
|
There was a problem hiding this comment.
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.
| let debounceTimer: number | null = null; | ||
| let isWaitingForSuggestion = false; | ||
| const DEBOUNCE_DELAY = 300; | ||
|
|
||
| let currentAbortController: AbortController | null = null; | ||
|
|
There was a problem hiding this comment.
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).
| 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); |
There was a problem hiding this comment.
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.
Summary by CodeRabbit
Release Notes
New Features
Improvements