-
Notifications
You must be signed in to change notification settings - Fork 0
feat: ai features #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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 }); | ||
| } | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 |
||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 |
||
| } | ||
| } | ||
|
|
||
| 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 } | ||
| ); | ||
| } | ||
| } | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate all request fields before prompt interpolation. Only ✅ 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 |
||
| 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 } | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,7 +119,7 @@ | |
| @apply border-border outline-ring/50; | ||
| } | ||
| body { | ||
| @apply bg-background text-foreground select-none; | ||
| @apply bg-background text-foreground; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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:
💡 Result: Biome’s CSS parser can parse Tailwind-specific directives like Enable Tailwind directives parsingAdd this to your {
"css": {
"parser": {
"tailwindDirectives": true
}
}
}This enables parsing of Tailwind syntax (including Optional: enable CSS formatting (separate switch){
"css": {
"formatter": {
"enabled": true
}
}
}( NoteThis 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 The Biome config lacks explicit Tailwind support. Add this to "css": {
"linter": {
"enabled": true
},
"parser": {
"cssModules": true,
"tailwindDirectives": true
}
}Without this setting, Biome's CSS linter cannot properly parse the Tailwind directives ( 🧰 Tools🪛 Biome (2.4.4)[error] 122-122: Tailwind-specific syntax is disabled. (parse) 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
|
|
||
| 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"; | ||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because Proposed fix- <Providers>
- {children}
- <Toaster />
- </Providers>
+ <Providers>{children}</Providers>
+ <Toaster />📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| </body> | ||||||||||||||
| </html> | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid dropping pending autosave on tab switch. Cleanup tied to 💡 Suggested fix useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
}
};
- }, [activeTabId]);
+ }, []);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||
| <div className="h-full flex flex-col"> | ||||||||||||||||||||||||||||||||||||
| <div className="flex items-center"> | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| 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; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
📝 Committable suggestion
🤖 Prompt for AI Agents