From 46a072c5c7a75acab14e5b597d30f3fe6de04e32 Mon Sep 17 00:00:00 2001 From: Shiva Jyoti Date: Fri, 12 Jun 2026 18:14:30 +0530 Subject: [PATCH 1/5] =?UTF-8?q?fix(a11y):=20add=20dynamic=20aria-label=20t?= =?UTF-8?q?o=20copy=20button=20in=20hero-code.tsx=20=E2=80=94=20closes=20#?= =?UTF-8?q?463=20(#487)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Shiva --- modules/home/hero-code.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/home/hero-code.tsx b/modules/home/hero-code.tsx index d5d1bcd1..007b2ba0 100644 --- a/modules/home/hero-code.tsx +++ b/modules/home/hero-code.tsx @@ -59,6 +59,7 @@ export function HeroCodeDemo() {
+
-

Asme

+

Editron

@@ -138,7 +139,7 @@ function FloatingPaths({ position }: { position: number }) { } -${189 + i * 6} -${312 - i * 5 * position} ${216 - i * 6} ${152 - i * 5 * position } ${343 - i * 6}C${616 - i * 5 * position} ${470 - i * 6} ${684 - i * 5 * position } ${875 - i * 6} ${684 - i * 5 * position} ${875 - i * 6}`, - color: `rgba(15,23,42,${0.1 + i * 0.03})`, + color: `currentcolor`, width: 0.5 + i * 0.03, })); diff --git a/components/ui/editron-auth-page.tsx b/components/ui/editron-auth-page.tsx index 2dc61d6c..570e0ee8 100644 --- a/components/ui/editron-auth-page.tsx +++ b/components/ui/editron-auth-page.tsx @@ -21,7 +21,7 @@ export function EditronAuthPage({ onGoogleSignIn, onGithubSignIn }: EditronAuthP return (
{/* Left Side - Branding & Testimonial */} -
+
Editron Logo @@ -45,7 +45,7 @@ export function EditronAuthPage({ onGoogleSignIn, onGithubSignIn }: EditronAuthP
{/* Right Side - Auth Form */} -
+
{/* Background Gradients with Red Accent */}
{/* Back to Home Button */} - {/* Auth Form Container */} -
+
{/* Mobile Logo */}
@@ -77,11 +77,11 @@ export function EditronAuthPage({ onGoogleSignIn, onGithubSignIn }: EditronAuthP {/* Header */}
-

+

Welcome to Editron

- Sign in to start coding with intelligence + Start building faster with AI-powered development

{/* Status Badge */} @@ -100,7 +100,7 @@ export function EditronAuthPage({ onGoogleSignIn, onGithubSignIn }: EditronAuthP
)} - {messages.map((msg) => { - const extended = msg as unknown as ExtendedMessage; - const rawParts: MessagePart[] = extended.parts ?? []; - - // AI SDK v3 stores user text in parts[].type=="text" - // Only genuine user messages have text parts - const textParts = rawParts.filter((p) => (p.type ?? "") === "text"); - const textContent: string = ( - textParts.map((p) => p.text ?? "").join("") || - extended.content || - "" - ); - - // v3 tool parts have type starting with "tool-" (e.g. "tool-read_file") - const toolParts: MessagePart[] = rawParts.filter( - (p) => (p.type ?? "").startsWith("tool-") - ); - - // Skip SDK-injected synthetic messages (no real text parts, no tool parts) - const isGenuineUser = msg.role === "user" && textParts.length > 0; - - return ( -
- {isGenuineUser && ( -
-
- {textContent} -
-
- -
-
- )} - {msg.role === "assistant" && ( -
-
- -
-
- {textContent && ( -
- {textContent} -
- )} - {toolParts.map((ti) => { - // In v3, tool name comes from the type suffix or toolName property - const tiName = (ti.toolName as string | undefined) ?? (ti.type as string)?.split("-").slice(1).join("-") ?? "tool"; - // Path arg lives in ti.input.path in v3 - const tiPath = (ti.input as Record | undefined)?.path as string | undefined; - const tiDone = ti.state === "output-available" || ti.state === "result"; - return ( -
-
- -
- - {tiName}({tiPath ? tiPath.split("/").pop() : ""}) {tiDone ? "✓" : } - -
- ); - })} -
-
- )} -
- ); - })} + {messages.map((msg) => ( + 0 && messages[messages.length - 1].role !== "assistant"} + /> + ))} {isLoading && messages.length > 0 && messages[messages.length - 1].role !== "assistant" && (
@@ -507,4 +316,4 @@ export default function AIChatPanel({ ); -} +} \ No newline at end of file diff --git a/modules/playground/components/chat-message.tsx b/modules/playground/components/chat-message.tsx new file mode 100644 index 00000000..95b2bd06 --- /dev/null +++ b/modules/playground/components/chat-message.tsx @@ -0,0 +1,108 @@ +/** + * @fileoverview Chat message rendering component for AI conversations. + * @module components/chat-message + * @description Renders individual chat messages with support for: + * - User messages with avatar + * - AI assistant messages with bot avatar + * - Tool invocation display with status indicators + * - Loading states for streaming responses + * - Message parts (text, tool-invocation) from AI SDK v3 + * @param {ChatMessageProps} props - Message data and loading state + * @returns {JSX.Element} Rendered message bubble with appropriate styling + */ + +"use client"; + +import React from "react"; +import { Bot, User, Wrench, Loader2 } from "lucide-react"; + +interface MessagePart { + type?: string; + text?: string; + toolCallId?: string; + toolName?: string; + state?: string; + input?: Record; + args?: Record; + [key: string]: unknown; +} + +interface ExtendedMessage { + parts?: MessagePart[]; + content?: string; + role?: string; + id?: string; +} + +interface ChatMessageProps { + message: ExtendedMessage & { role?: string; id?: string }; + isLoading?: boolean; +} + +export function ChatMessage({ message, isLoading }: ChatMessageProps) { + const rawParts: MessagePart[] = message.parts ?? []; + + // AI SDK v3 stores user text in parts[].type=="text" + const textParts = rawParts.filter((p) => (p.type ?? "") === "text"); + const textContent: string = ( + textParts.map((p) => p.text ?? "").join("") || + message.content || + "" + ); + + // v3 tool parts have type starting with "tool-" (e.g. "tool-read_file") + const toolParts: MessagePart[] = rawParts.filter( + (p) => (p.type ?? "").startsWith("tool-") + ); + + // Skip SDK-injected synthetic messages (no real text parts, no tool parts) + const isGenuineUser = message.role === "user" && textParts.length > 0; + + return ( +
+ {isGenuineUser && ( +
+
+ {textContent} +
+
+ +
+
+ )} + {message.role === "assistant" && ( +
+
+ +
+
+ {textContent && ( +
+ {textContent} +
+ )} + {toolParts.map((ti, idx) => { + const tiName = (ti.toolName as string | undefined) ?? + (ti.type as string)?.split("-").slice(1).join("-") ?? "tool"; + const tiPath = (ti.input as Record | undefined)?.path as string | undefined; + const tiDone = ti.state === "output-available" || ti.state === "result"; + // Add index fallback for undefined toolCallId to fix React key issue + const key = ti.toolCallId ?? `tool-${idx}`; + return ( +
+
+ +
+ + {tiName}({tiPath ? tiPath.split("/").pop() : ""}) {tiDone ? "✓" : } + +
+ ); + })} +
+
+ )} + {/* REMOVED: Duplicate "Thinking..." indicator - AIChatPanel handles this at list level */} +
+ ); +} \ No newline at end of file diff --git a/modules/playground/hooks/useAITools.ts b/modules/playground/hooks/useAITools.ts new file mode 100644 index 00000000..91b6b679 --- /dev/null +++ b/modules/playground/hooks/useAITools.ts @@ -0,0 +1,295 @@ +/** + * @fileoverview Custom hook for handling AI tool execution logic. + * @module hooks/useAITools + * @description Extracts and manages all client-side tool execution logic from AI chat interactions. + * Handles four main tool types: + * - read_file: Reads file content from template data + * - edit_file: Updates a single file with new content + * - edit_multiple_files: Batch updates multiple files + * - delete_file: Removes a file from the project + * @feature Prevents duplicate tool execution using processedToolCallIds ref + * @feature Checks for unresolved tools to block message sending + * @param {UseAIToolsProps} props - Configuration and state dependencies + * @returns {Object} hasUnresolvedTools - Function to check pending tool calls + */ + +"use client"; + +import { useEffect, useRef, useCallback } from "react"; +import { toast } from "sonner"; +import type { TemplateFolder } from "@/modules/playground/lib/path-to-json"; +import { + addOrUpdateFile, + deleteFileByPath, + findFileByPath, +} from "@/modules/playground/hooks/useAI"; + +interface MessagePart { + type?: string; + text?: string; + toolCallId?: string; + toolName?: string; + state?: string; + input?: Record; + toolInvocation?: Record; + args?: Record; + [key: string]: unknown; +} + +interface ExtendedMessage { + parts?: MessagePart[]; + content?: string; + role?: string; + id?: string; +} + +interface UseAIToolsProps { + messages: unknown[]; + templateData: TemplateFolder | null; + openFiles: Array<{ + id?: string; + filename: string; + fileExtension?: string; + content?: string; + originalContent?: string; + hasUnsavedChanges?: boolean; + }>; + setTemplateData: (data: TemplateFolder) => void; + setOpenFiles: (files: Array<{ + id?: string; + filename: string; + fileExtension?: string; + content?: string; + originalContent?: string; + hasUnsavedChanges?: boolean; + }>) => void; + saveTemplateData: (data: TemplateFolder) => Promise; + addToolResult: (result: { toolCallId: string; tool: string; output: string }) => void; +} + +/** + * Helper function to normalize and compare file paths + * Ensures accurate path matching across different directory structures + */ +function normalizePath(path: string): string { + return path.replace(/\\/g, "/").replace(/^\/+/, "").toLowerCase(); +} + +function isPathMatch(path1: string, path2: string): boolean { + const normalized1 = normalizePath(path1); + const normalized2 = normalizePath(path2); + return normalized1 === normalized2; +} + +export function useAITools({ + messages, + templateData, + openFiles, + setTemplateData, + setOpenFiles, + saveTemplateData, + addToolResult, +}: UseAIToolsProps) { + // Track which tool calls we've already executed to prevent double-execution + const processedToolCallIds = useRef(new Set()); + + // Check if the most recent tool hasn't finished to prevent sending messages + const hasUnresolvedTools = useCallback(() => { + const lastMessage = messages[messages.length - 1] as ExtendedMessage | undefined; + if (lastMessage?.role !== "assistant") return false; + + const parts = (lastMessage as unknown as { parts?: unknown[] })?.parts ?? []; + return Array.isArray(parts) && parts.some((rawP: unknown) => { + if (!rawP || typeof rawP !== "object") return false; + const p = rawP as MessagePart; + const isTool = p.type === "tool-invocation" || + (typeof p.type === "string" && p.type.startsWith("tool-")); + const isUnresolved = !p.state || + (p.state !== "result" && p.state !== "output-available"); + const hasCall = p.toolInvocation && + typeof p.toolInvocation === "object" && + (p.toolInvocation as Record).state === "call"; + return isTool && isUnresolved && hasCall; + }); + }, [messages]); + + // Handle incoming client-side tool calls + useEffect(() => { + const lastMessage = messages[messages.length - 1] as ExtendedMessage | undefined; + if (lastMessage?.role !== "assistant") return; + + const rawParts: unknown[] = (lastMessage as unknown as { parts?: unknown[] }).parts ?? []; + + for (const rawPart of rawParts) { + const part = rawPart as Record; + const partType = part.type as string | undefined; + + // v3 static tool parts: type starts with "tool-" (e.g. "tool-read_file") + if (!partType?.startsWith("tool-")) continue; + + // Guard against re-execution: skip if already processed + const toolCallId = part.toolCallId as string | undefined; + if (!toolCallId) continue; + if (processedToolCallIds.current.has(toolCallId)) continue; + + // Only execute when input is fully available (not still streaming) + const state = part.state as string | undefined; + if (state === "output-available" || state === "output-streaming") continue; + if (state === "input-streaming") continue; + + const toolName = (part.toolName as string | undefined) ?? + partType.split("-").slice(1).join("-"); + const args = (part.input as Record | undefined) ?? + (part.args as Record | undefined) ?? {}; + + if (!toolCallId || !toolName) continue; + + let result: string; + + // Use an IIFE (Immediately Invoked Function Expression) to handle async operations + (async () => { + try { + if (toolName === "read_file") { + const { path } = args as { path?: string }; + if (!path || typeof path !== "string") { + result = `Error: read_file requires a "path" argument (e.g. "src/App.tsx")`; + } else { + const file = findFileByPath(templateData?.items || [], path); + result = (file && "content" in file && file.content !== undefined) + ? file.content + : `Error: File "${path}" not found`; + } + } else if (toolName === "edit_file") { + const { path, content } = args as { path?: string; content?: string }; + if (!path || typeof path !== "string") { + result = `Error: edit_file requires a "path" argument (e.g. "README.md")`; + } else if (content === undefined || content === null) { + result = `Error: edit_file requires a "content" argument with the full file contents`; + } else if (!templateData) { + result = `Error: Template data not loaded`; + } else { + try { + const updatedItems = addOrUpdateFile(templateData.items, path, content as string); + const updatedTemplate = { ...templateData, items: updatedItems }; + setTemplateData(updatedTemplate); + + // Use normalized path comparison instead of endsWith + const normalizedEditPath = normalizePath(path); + const updatedOpenFiles = openFiles.map((f) => { + const ext = f.fileExtension ? `.${f.fileExtension}` : ""; + const fullName = `${f.filename}${ext}`; + const normalizedFullName = normalizePath(fullName); + if (isPathMatch(normalizedEditPath, normalizedFullName)) { + return { + ...f, + content: content as string, + hasUnsavedChanges: true + }; + } + return f; + }); + + setOpenFiles(updatedOpenFiles); + await saveTemplateData(updatedTemplate); + toast.success(`AI updated ${path}`); + result = `Successfully updated ${path}`; + } catch (saveError) { + console.error("Failed to save template data:", saveError); + toast.error(`Failed to save changes to ${path}`); + result = `Error: Failed to save changes to ${path}`; + } + } + } else if (toolName === "edit_multiple_files") { + const { changes } = args as { changes?: { path: string; content: string }[] }; + if (!changes || !Array.isArray(changes) || changes.length === 0) { + result = `Error: edit_multiple_files requires a "changes" array with at least one {path, content} entry`; + } else if (!templateData) { + result = `Error: Template data not loaded`; + } else { + try { + let currentItems = templateData.items; + let currentOpenFiles = [...openFiles]; + + // Build a map of normalized paths for efficient comparison + const normalizedChanges = changes.map(change => ({ + ...change, + normalizedPath: normalizePath(change.path) + })); + + for (const change of normalizedChanges) { + currentItems = addOrUpdateFile(currentItems, change.path, change.content); + currentOpenFiles = currentOpenFiles.map((f) => { + const ext = f.fileExtension ? `.${f.fileExtension}` : ""; + const fullName = `${f.filename}${ext}`; + const normalizedFullName = normalizePath(fullName); + if (isPathMatch(change.normalizedPath, normalizedFullName)) { + return { ...f, content: change.content, hasUnsavedChanges: true }; + } + return f; + }); + } + + const updatedTemplate = { ...templateData, items: currentItems }; + setTemplateData(updatedTemplate); + setOpenFiles(currentOpenFiles); + await saveTemplateData(updatedTemplate); + toast.success(`AI scaffolded ${changes.length} files`); + result = `Successfully updated ${changes.length} files`; + } catch (saveError) { + console.error("Failed to save template data:", saveError); + toast.error(`Failed to save changes`); + result = `Error: Failed to save changes`; + } + } + } else if (toolName === "delete_file") { + const { path } = args as { path?: string }; + if (!path || typeof path !== "string") { + result = `Error: delete_file requires a "path" argument`; + } else if (!templateData) { + result = `Error: Template data not loaded`; + } else { + try { + const updatedItems = deleteFileByPath(templateData.items, path); + const updatedTemplate = { ...templateData, items: updatedItems }; + setTemplateData(updatedTemplate); + + // Use normalized path comparison for deletion + const normalizedDeletePath = normalizePath(path); + const updatedOpenFiles = openFiles.filter((f) => { + const ext = f.fileExtension ? `.${f.fileExtension}` : ""; + const fullName = `${f.filename}${ext}`; + const normalizedFullName = normalizePath(fullName); + return !isPathMatch(normalizedDeletePath, normalizedFullName); + }); + + setOpenFiles(updatedOpenFiles); + await saveTemplateData(updatedTemplate); + toast.success(`AI deleted ${path}`); + result = `Successfully deleted ${path}`; + } catch (saveError) { + console.error("Failed to save template data:", saveError); + toast.error(`Failed to delete ${path}`); + result = `Error: Failed to delete ${path}`; + } + } + } else { + result = `Error: Unknown tool ${toolName}`; + } + } catch (err: unknown) { + result = `Error: ${err instanceof Error ? err.message : String(err)}`; + } + + // Mark as processed BEFORE calling addToolResult to prevent re-execution on re-render + processedToolCallIds.current.add(toolCallId); + + addToolResult({ + toolCallId, + tool: toolName, + output: result, + }); + })(); + } + }, [messages, templateData, openFiles, setTemplateData, setOpenFiles, saveTemplateData, addToolResult]); + + return { hasUnresolvedTools }; +} \ No newline at end of file From 4ffe26f05157c15b98334d19add664ebbafb240c Mon Sep 17 00:00:00 2001 From: Ravindi Fernando <140165006+RavindiFernando@users.noreply.github.com> Date: Sat, 4 Jul 2026 12:58:47 +0530 Subject: [PATCH 4/5] fix(a11y): add aria-label to icon-only buttons in EnvManager (#503) Signed-off-by: RavindiFernando --- modules/playground/components/env-manager.tsx | 3 ++- tests/env-manager.test.tsx | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/playground/components/env-manager.tsx b/modules/playground/components/env-manager.tsx index bc41e6cd..48b5e1a0 100644 --- a/modules/playground/components/env-manager.tsx +++ b/modules/playground/components/env-manager.tsx @@ -154,7 +154,7 @@ export function EnvManager({ Environment Variables
- @@ -216,6 +216,7 @@ export function EnvManager({ variant="ghost" className="h-8 w-8 text-muted-foreground hover:text-red-500 shrink-0" onClick={() => handleRemoveVar(idx)} + aria-label={`Remove variable ${idx + 1}`} > diff --git a/tests/env-manager.test.tsx b/tests/env-manager.test.tsx index 7f53bca9..49516c92 100644 --- a/tests/env-manager.test.tsx +++ b/tests/env-manager.test.tsx @@ -11,8 +11,8 @@ vi.mock("@/components/ui/sidebar", () => ({ })); vi.mock("@/components/ui/button", () => ({ - Button: ({ children, onClick, disabled, className, title }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; className?: string; title?: string }) => ( - ), @@ -76,7 +76,7 @@ describe("EnvManager Component", () => { /> ); - const addButton = screen.getByTitle("Add Variable"); + const addButton = screen.getByRole("button", { name: "Add Variable" }); fireEvent.click(addButton); const keyInput = screen.getByPlaceholderText("API_KEY") as HTMLInputElement; @@ -102,7 +102,7 @@ describe("EnvManager Component", () => { ); // Add first variable - const addButton = screen.getByTitle("Add Variable"); + const addButton = screen.getByRole("button", { name: "Add Variable" }); fireEvent.click(addButton); const inputs = screen.getAllByPlaceholderText("API_KEY"); fireEvent.change(inputs[0], { target: { value: "PORT" } }); @@ -129,7 +129,7 @@ describe("EnvManager Component", () => { ); // Add a variable (will start empty) - const addButton = screen.getByTitle("Add Variable"); + const addButton = screen.getByRole("button", { name: "Add Variable" }); fireEvent.click(addButton); expect(screen.getByText("⚠️ All keys must be filled.")).toBeDefined(); From 7e4d2f21822107d09ab59915c111412c05715451 Mon Sep 17 00:00:00 2001 From: Rakshith Date: Sat, 15 Aug 2026 23:33:43 +0530 Subject: [PATCH 5/5] docs: describe security protocols for package releases