Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 13 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@
"@base-ui/react": "^1.2.0",
"@clerk/nextjs": "^6.36.5",
"@clerk/themes": "^2.4.55",
"@codemirror/commands": "^6.10.2",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.39.15",
"@hookform/resolvers": "^5.2.2",
"@inngest/middleware-sentry": "^0.1.3",
"@mendable/firecrawl-js": "^1.21.1",
Expand All @@ -41,6 +39,7 @@
"embla-carousel-react": "^8.6.0",
"inngest": "^3.48.1",
"input-otp": "^1.4.2",
"ky": "^1.14.3",
"lucide-react": "^0.575.0",
"next": "16.1.1",
"next-themes": "^0.4.6",
Expand Down
113 changes: 113 additions & 0 deletions src/app/api/quick-edit/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { generateText, Output } from "ai";
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { anthropic } from "@ai-sdk/anthropic";
import { firecrawl } from "@/lib/firecrawl";

const quickEditSchema = z.object({
editedCode: z
.string()
.describe(
"The edited version of the selected code based on the instruction"
),
});

const URL_REGEX = /https?:\/\/[^\s)>\]]+/g;

const QUICK_EDIT_PROMPT = `You are a code editing assistant. Edit the selected code based on the user's instruction.

<context>
<selected_code>
{selectedCode}
</selected_code>
<full_code_context>
{fullCode}
</full_code_context>
</context>

{documentation}

<instruction>
{instruction}
</instruction>

<instructions>
Return ONLY the edited version of the selected code.
Maintain the same indentation level as the original.
Do not include any explanations or comments unless requested.
If the instruction is unclear or cannot be applied, return the original code unchanged.
</instructions>`;

export async function POST(request: Request) {
try {
const { userId } = await auth();
const { selectedCode, fullCode, instruction } = await request.json();

if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 400 });
}
Comment on lines +47 to +49
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.


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

if (!instruction) {
return NextResponse.json(
{ error: "Instruction is required" },
{ status: 400 }
);
}
Comment on lines +45 to +63
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.


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>`;
Comment on lines +65 to +90
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.

}
}

const prompt = QUICK_EDIT_PROMPT.replace("{selectedCode}", selectedCode)
.replace("{fullCode}", fullCode || "")
.replace("{instruction}", instruction)
.replace("{documentation}", documentationContext);

const { output } = await generateText({
model: anthropic("claude-haiku-4-5-20251001"),
output: Output.object({ schema: quickEditSchema }),
prompt,
});

return NextResponse.json({ editedCode: output.editedCode });
} catch (error) {
console.error("Edit error:", error);
return NextResponse.json(
{ error: "Failed to generate edit" },
{ status: 500 }
);
}
}
92 changes: 92 additions & 0 deletions src/app/api/suggestion/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { generateText, Output } from "ai";
import { NextResponse } from "next/server";
import { z } from "zod";
import { anthropic } from "@ai-sdk/anthropic";
import { auth } from "@clerk/nextjs/server";
// import { google } from "@ai-sdk/google";

const suggestionSchema = z.object({
suggestion: z
.string()
.describe(
"The code to insert at cursor, or empty string if no complete needed"
),
});

const SUGGESTION_PROMPT = `You are a code suggestion assistant.

<context>
<file_name>{fileName}</file_name>
<previous_lines>
{previousLines}
</previous_lines>
<current_line number="{lineNumber}">{currentLine}</current_line>
<before_cursor>{textBeforeCursor}</before_cursor>
<after_cursor>{textAfterCursor}</after_cursor>
<next_lines>
{nextLines}
</next_lines>
<full_code>
{code}
</full_code>
</context>

<instructions>
Follow these steps IN ORDER:

1. First, look at next_lines. If next_lines contains ANY code, check if it continues from where the cursor is. If it does, return empty string immediately - the code is already written.

2. Check if before_cursor ends with a complete statement (;, }, )). If yes, return empty string.

3. Only if steps 1 and 2 don't apply: suggest what should be typed at the cursor position, using context from full_code.

Your suggestion is inserted immediately after the cursor, so never suggest code that's already in the file.
</instructions>`;

export async function POST(request: Request) {
try {
const { userId } = await auth();

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

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());

Comment on lines +54 to +77
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.

const { output } = await generateText({
model: anthropic("claude-haiku-4-5-20251001"),
output: Output.object({ schema: suggestionSchema }),
prompt,
});

return NextResponse.json({ suggestion: output.suggestion });
} catch (error) {
console.error("Suggestion error: ", error);
return NextResponse.json(
{ error: "Failed to generate suggestion" },
{ status: 500 }
);
}
}
2 changes: 1 addition & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
@apply border-border outline-ring/50;
}
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.

}
}

Expand Down
6 changes: 5 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { IBM_Plex_Mono, Inter } from "next/font/google";

import { Toaster } from "sonner";
import { Providers } from "@/components/providers";

import "./globals.css";
Expand Down Expand Up @@ -29,7 +30,10 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body className={`${inter.variable} ${plexMono.variable} antialiased`}>
<Providers>{children}</Providers>
<Providers>
{children}
<Toaster />
</Providers>
Comment on lines +33 to +36
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

Toaster is currently mounted only for authenticated users.

Because Providers renders children inside <Authenticated>, the toast host at Line 35 won’t exist in unauthenticated/loading states. If you want app-wide toasts, render it outside the auth-gated children slot (or move it inside Providers but outside <Authenticated>).

Proposed fix
-        <Providers>
-          {children}
-          <Toaster />
-        </Providers>
+        <Providers>{children}</Providers>
+        <Toaster />
📝 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
<Providers>
{children}
<Toaster />
</Providers>
<Providers>{children}</Providers>
<Toaster />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/layout.tsx` around lines 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.

</body>
</html>
);
Expand Down
10 changes: 8 additions & 2 deletions src/features/editor/components/code-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { keymap } from "@codemirror/view";
import { minimap } from "../extensions/minimap";
import { indentationMarkers } from "@replit/codemirror-indentation-markers";
import { customSetup } from "../extensions/custom-setup";
import { suggestion } from "../extensions/suggestion";
import { quickEdit } from "../extensions/quick-edit";
import { selectionTooltip } from "../extensions/selection-tooltip";

interface Props {
fileName: string;
Expand Down Expand Up @@ -36,11 +39,14 @@ export const CodeEditor = ({
extensions: [
oneDark,
customTheme,
languageExtension,
customSetup,
languageExtension,
suggestion(fileName),
quickEdit(fileName),
selectionTooltip(),
keymap.of([indentWithTab]),
minimap(),
indentationMarkers(),
keymap.of([indentWithTab]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
onChange?.(update.state.doc.toString());
Expand Down
11 changes: 10 additions & 1 deletion src/features/editor/components/editor-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useFile } from "@/features/projects/hooks/use-files";
import Image from "next/image";
import { CodeEditor } from "./code-editor";
import { useUpdateFile } from "@/features/projects/hooks/use-files";
import { useRef } from "react";
import { useEffect, useRef } from "react";

const DEBOUNCE_MS = 1500;

Expand All @@ -19,6 +19,15 @@ export const EditorView = ({ projectId }: { projectId: Id<"projects"> }) => {
const isActiveFileBinary = activeFile && activeFile.storageId;
const isActiveFileText = activeFile && !activeFile.storageId;

// Clean-up pending debounced updates on unmount or file change
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [activeTabId]);
Comment on lines +22 to +29
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

Avoid dropping pending autosave on tab switch.

Cleanup tied to activeTabId cancels pending debounced writes during file switches, which can lose the latest edits typed within the debounce window.

💡 Suggested fix
   useEffect(() => {
     return () => {
       if (timeoutRef.current) {
         clearTimeout(timeoutRef.current);
+        timeoutRef.current = null;
       }
     };
-  }, [activeTabId]);
+  }, []);
📝 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
// Clean-up pending debounced updates on unmount or file change
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [activeTabId]);
// Clean-up pending debounced updates on unmount or file change
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/editor/components/editor-view.tsx` around lines 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.


return (
<div className="h-full flex flex-col">
<div className="flex items-center">
Expand Down
44 changes: 44 additions & 0 deletions src/features/editor/extensions/quick-edit/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import ky from "ky";
import { z } from "zod";
import { toast } from "sonner";

const editRequestSchema = z.object({
selectedCode: z.string(),
fullCode: z.string(),
instruction: z.string(),
});

const editResponseSchema = z.object({
editedCode: z.string(),
});

type EditRequest = z.infer<typeof editRequestSchema>;
type EditResponse = z.infer<typeof editResponseSchema>;

export const fetcher = async (
payload: EditRequest,
signal: AbortSignal
): Promise<string | null> => {
try {
const validatedPayload = editRequestSchema.parse(payload);

const response = await ky
.post("/api/quick-edit", {
json: validatedPayload,
signal,
timeout: 30_000,
retry: 0,
})
.json<EditResponse>();

const validatedResponse = editResponseSchema.parse(response);

return validatedResponse.editedCode || null;
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
return null;
}
toast.error("Failed to fetch AI quick edit");
return null;
}
};
Loading