diff --git a/.gitignore b/.gitignore index 0a0cfdd..425d83a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ warp/apps/web/.env.local warp/apps/web/.env.* # Local data store +warp/apps/web/data/ data/ # macOS diff --git a/Warp.md b/Warp.md index ee7858e..29b9452 100644 --- a/Warp.md +++ b/Warp.md @@ -7,7 +7,7 @@ ## 0) What this is * **Studio Plugin (Luau)**: docked chat + proposals, reads active editor state, previews diffs, applies edits inside **ChangeHistoryService**. -* **Next.js Backend (TypeScript)**: `/api/chat`, `/api/stream`, `/api/proposals/:id/apply`, `/api/assets/search`, plus an orchestrator that exposes tools to the LLM. +* **Next.js Backend (TypeScript)**: `/api/chat`, `/api/proposals/:id/apply`, `/api/proposals`, `/api/assets/search`, plus an orchestrator that exposes tools to the LLM. * **LLM Tool‑Calling**: one‑tool‑per‑message, approval‑first workflow (like Cline). The model proposes **proposals** (edits/object ops/asset ops); only the plugin performs writes after user approval. --- @@ -276,8 +276,7 @@ end ## 6) Streaming & transport -* **Plugin**: default to **short polling** or **long‑polling** `/api/stream?cursor=…`. If SSE is available and reliable in your environment, the plugin can use it; otherwise keep long‑poll. -* **Web dashboard**: use SSE for token‑level streaming. +* Streaming: not implemented in this increment. Use request/response for now. * Limit concurrent plugin HTTP requests (≤2) to avoid Studio’s in‑flight cap. --- @@ -328,7 +327,7 @@ flowchart TD subgraph Studio Plugin (Luau) A[User asks] --> B[Collect context\nGetEditorSource + Selection] B --> C[/api/chat] - J[Stream/Long-poll /api/stream] --> K[Render partial tokens] + %% Streaming omitted in this increment C -->|proposals| D[Diff/Operations UI] D --> E{Approve?} E -- Yes: edits --> F[TryBeginRecording\nUpdateSourceAsync\nFinishRecording] @@ -460,7 +459,7 @@ flowchart LR end subgraph Backend["Next.js Orchestrator (Vercel)"] - ROUTES[/api/chat • /api/stream* • /api/assets/search • /api/proposals/:id/apply/] + ROUTES[/api/chat • /api/assets/search • /api/proposals/:id/apply/ • /api/proposals] ORCH[Tool‑Calling Orchestrator] PROVIDERS[OpenRouter / OpenAI / Anthropic] STORE[(DB • Cache • Audit Logs)] @@ -480,7 +479,7 @@ flowchart LR GEN3D --> GPU --> OPENCLOUD --> ORCH ROUTES --> CATALOG - %% Notes: * Studio plugins cannot use SSE/WebSockets. Use short/long‑polling. + %% Notes: Streaming/SSE not implemented in this increment. ``` **Key points** @@ -849,117 +848,15 @@ warp/ - Docked chat UI, context capture, HTTP permission flow — implemented - `/api/chat` round-trip producing `edit` and `object_op` proposals — implemented -- Diff preview + one-click **Apply** (single undo step) — full unified diff renderer implemented; shows context, additions/deletions, multiple hunks, with Open/Close Diff toggle -- Asset search + insert flow — implemented (server stub + plugin Browse + Insert) +- Diff preview + one-click **Apply** (single undo step) — basic unified diff snippet attached by server; minimal preview in plugin +- Asset search API — implemented (requires CATALOG_API_URL); plugin UI not implemented in this increment - Audit log of proposals and outcomes — implemented with file-backed JSON durability and apply acknowledgements --- -## Implementation Progress — 2025-08-31 (updated @ 21:06 UTC) - -Recent updates - -- Added zod-based validation of provider tool-call arguments before mapping to proposals -- Added Plan/Act scaffold: executes context tools locally and performs a second provider call for the next actionable tool -- Switched proposals store to file-backed JSON durability (apps/web/data/proposals.json) -- Introduced Catalog provider interface (env CATALOG_API_URL) for real search; stub remains fallback - -Decisions captured - -- AI name: Vector (assistant name in UI and comments) -- LLM provider: OpenRouter, model: moonshotai/kimi-k2:free -- Package manager: npm -- Hosting: Local dev at http://127.0.0.1:3000 for now. We will move to Vercel (or similar) later. -- Domain permissions: local-only for now; will add hosted domain later when deploying. -- Plugin install workflow: Rojo during development → export .rbxm for small tester group → publish to Marketplace for public release. -- System prompt: refined Vector prompt (one-tool-per-message, proposal-first, minimal diffs). - -Working now - -- Web (Next.js + npm) - - Local app scaffold (Next 14) with npm scripts; landing page at /. - - API routes wired: /api/chat persists proposals to a file-backed store for auditing; /api/proposals/[id]/apply records an audit event; /api/proposals (GET) lists stored proposals; /api/assets/search uses a provider interface (real via CATALOG_API_URL or stub); /api/assets/generate3d returns a stub jobId. - - Provider tool-call parsing (OpenRouter) behind flags: when VECTOR_USE_OPENROUTER=1, model outputs exactly one XML-like tool call which is parsed and mapped to proposals. Arguments are validated with zod before mapping. Falls back to safe proposals if parsing fails. - - Plan/Act scaffold behind VECTOR_PLAN_ACT=1: if the first tool is a context tool (get_active_script, list_selection, list_open_documents), we execute it locally from the provided context, store the result in a short session, and issue a second provider call using that result to get the next actionable tool. - - Provider adapters present: openrouter.ts (call path), openai.ts (stub). -- Plugin (Vector) - - Dock UI with input + Send. Sends context (active script + selection) to /api/chat and renders proposals. - - Approve/Reject per proposal; applies: - - edit proposals that insert text via rangeEDITS (merged, UpdateSourceAsync with undo) - - object_op rename_instance (ChangeHistoryService wrapped) - - Added "Apply & Open" for edit proposals (opens the script after a successful apply). - - Asset search flow: Browse button fetches /api/assets/search results → Inline list with Insert buttons → inserts via InsertService. - - Reports apply results back to /api/proposals/:id/apply for auditing. - - Simple diff preview snippet (shows inserted text). - -Configured locally - -- Environment - - warp/apps/web/.env present with OPENROUTER_API_KEY (provided) and OPENROUTER_MODEL=moonshotai/kimi-k2:free (default). - - VECTOR_USE_OPENROUTER=0 by default; set to 1 to enable provider-driven tool-call parsing. - - VECTOR_PLAN_ACT=0 by default; set to 1 to enable a second provider call after context tools (Plan/Act scaffold). -- CATALOG_API_URL optional: when set, /api/assets/search calls this URL to fetch normalized results; otherwise it uses a stub list. - - Data directory: proposals are persisted to apps/web/data/proposals.json (auto-created on write). - - Do not commit .env to version control. - -Not yet implemented (next milestones) - -- Provider-driven tool-call execution loop (multi-turn Plan/Act with context tools) and stricter validation/guardrails -- Robust diff merging on server (multi-edit merging) and generalized path→Instance resolution -- Asset Catalog integration (real Catalog API instead of stub) and GPU 3D generation → Open Cloud upload -- Rich plugin UI: full diff preview (line-by-line, deletions/edits), improved status/streaming, thumbnails for assets -- Persistence: move from file-backed JSON to a durable DB/schema (e.g., SQLite/Prisma), richer auditing and listing endpoints (filter by projectId) -- Packaging/deploy: Vercel hosting, domain allowlisting in Studio - -Needs from you - -- Approvals to proceed: - - Wire plugin context capture to POST /api/chat and display proposals in the Vector dock. - - Implement diff preview UI with Approve/Reject and undo semantics. - - Enable the provider-backed path (OpenRouter) and tool-call parsing after initial local verification. -- Later (not blocking M0/M1): - - Roblox Open Cloud credentials for 3D generation and asset uploads (generate3d flows). - - Catalog API integration details/keys if we query Roblox web APIs server‑side (optional). - -Recommended next steps (proposed M0 path) - -1) Verify npm install in warp/apps/web and run the local dev server (npm run dev) -2) Wire the plugin to collect context (active script + selection) and POST to /api/chat -3) Implement diff preview data shaping on server (no writes), then renderable in plugin UI -4) Integrate OpenRouter provider call path behind a feature flag and progressively build tool-call parsing - -Note on cline_openai.md - -- Populated with a concise Vector system prompt (Cline‑style). We will refine it as the tool registry and execution loop evolve. - -### How to run locally - -- Web (from a new terminal): - - cd warp/apps/web - - npm install - - Ensure your .env or .env.local contains OPENROUTER_API_KEY and OPENROUTER_MODEL=moonshotai/kimi-k2:free - - Optional: set VECTOR_USE_OPENROUTER=1 to attach provider notes - - npm run dev -- In Roblox Studio: - - Load the plugin (via Rojo or by placing the plugin folder) - - Click the Vector toolbar button to open the dock - - Click "Vector Settings" to enter your provider info (OpenRouter-compatible). Use Base URL `https://openrouter.ai/api/v1`, paste your API Key, and choose a Model ID (e.g., `moonshotai/kimi-k2:free`). Saved locally via plugin settings. - - Type a message, click Send - - Approve/Reject proposals; edits and rename ops will apply as described - -### Notes and next steps - -- Known limitations in this increment: - - Diff renderer falls back to a simplified mode for very large files; multi-edit diffs are supported but may be slower on very large scripts - - Edits merging handles the current insertion case well, but not complex multi‑edit scenarios yet - - Asset search uses stubbed backend data and shows a text list (no thumbnails yet) - - Plan/Act is a single additional step (context tool then actionable tool); a full multi-turn loop remains -- Recommended follow‑ups: - - Optimize diff computation for very large scripts; consider server-side precomputation for proposals with many edits - - Add real Catalog integration on the server and show thumbnails in the plugin UI - - Persist proposals/audit records in a durable store and add filtering endpoints - - Move to Vercel and add the hosted domain to Studio’s allowed list - - Expand provider tool‑call parsing into a full multi-turn Plan/Act execution loop with guardrails and retries +## Implementation Status + +Moved to docs/IMPLEMENTATION_STATUS.md for clarity and maintenance. --- diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md new file mode 100644 index 0000000..ae56bb1 --- /dev/null +++ b/docs/IMPLEMENTATION_STATUS.md @@ -0,0 +1,99 @@ +# Vector Implementation Status (Spec) + +This document summarizes what’s implemented, what’s partial/dummy, what’s not implemented, and what inputs/decisions are needed. + +Scope: warp/apps/web (Next.js) and warp/plugin (Roblox Studio plugin). + +Implemented + +- Plugin (Luau) + - Dock UI with chat input and proposal list + - Context capture: active script (path + editor source) and selection + - Send to backend: POST /api/chat with context and optional provider settings + - Proposal rendering: edit/object_op/asset_op cards + - Approve/Reject per proposal + - Apply edit proposals (rangeEDITS) with ChangeHistoryService and ScriptEditorService:UpdateSourceAsync + - Apply object_op rename_instance (ChangeHistoryService wrapped) + - Diff preview: simple unified diff snippet (on-demand Open Diff) + - Asset search UI: Browse button calling /api/assets/search and inline “Insert” using InsertService + - “Apply & Open” for edit proposals + - Apply result reporting: POST /api/proposals/:id/apply + - Settings popup (internal testing): configure OpenRouter Base URL, API Key, and Model, with Kimi K2 preset and Test button + +- Backend (Next.js, TypeScript) + - Routes + - POST /api/chat: validates input, runs orchestrator, returns proposals, persists for audit + - GET /api/proposals: list stored proposals + - POST /api/proposals/[id]/apply: mark proposal applied with event payload + - GET /api/assets/search: calls external CATALOG_API_URL provider; returns normalized results + - POST /api/assets/generate3d: returns stub jobId + - Orchestrator + - Provider: OpenRouter adapter; reads API key/model from per-request provider or env + - Tool-call parsing: expects one XML-like tool call; zod-validated args; maps to proposals + - Fallback behavior when provider parsing fails: safe edit/object/asset proposals + - Plan/Act scaffold: if the first tool is a context tool and flags enabled, executes locally and performs one follow-up provider call + - Persistence + - File-backed JSON datastore for proposals and audit under warp/apps/web/data/proposals.json + +Half-implemented (placeholder/dummy) + +- Diff preview + - Minimal unified diff preview (single simplified render; line-by-line with limited context). Full multi-hunk, highlighted, and large-file optimized renderer pending. +- Plan/Act execution loop + - Single extra step only (context tool → one additional provider call). Full multi-turn loop with guardrails and retries pending. +- 3D generation route + - /api/assets/generate3d returns a stub jobId; no GPU job enqueueing or Open Cloud upload yet. +- Provider error handling + - Errors surfaced when provider is configured; basic bubbling to plugin UI is present, with minimal categorization. + +Not implemented + +- Full provider-driven execution loop + - Multi-turn Plan/Act with robust validation, retries, and proper context tool chaining. +- Robust diff merging on server + - Multi-edit merging, conflict resolution, and path→Instance resolution generalization. +- Real Catalog integration service + - Current backend requires an external CATALOG_API_URL service. No built-in Roblox web API integration or thumbnails yet. +- Durable database + - Migrate from file-backed JSON to SQLite/Prisma (or similar) with richer audit and filtering endpoints (e.g., filter by projectId). +- Packaging and deploy + - Vercel deployment, domain allowlisting in Studio, production environment plumbing. +- Streaming/transport + - SSE/long-polling for plugin status updates is not implemented. + +What you need to provide / decide + +- CATALOG_API_URL + - A server endpoint that returns normalized results [{ id, name, creator, type, thumbnailUrl? }]; required for /api/assets/search. +- Provider credentials path + - Choose either: enter credentials in the plugin Settings (recommended for dev) or set OPENROUTER_API_KEY (+ optional OPENROUTER_MODEL) in warp/apps/web/.env(.local) and set VECTOR_USE_OPENROUTER=1. +- Persistence and deploy + - Decide on DB (e.g., SQLite/Prisma) and hosting target (e.g., Vercel) for moving beyond local-only. +- Roblox Open Cloud credentials (later) + - Needed for 3D generation uploads and asset insertion flows. + +File map (key references) + +- Backend + - /warp/apps/web/app/api/chat/route.ts + - /warp/apps/web/app/api/proposals/route.ts + - /warp/apps/web/app/api/proposals/[id]/apply/route.ts + - /warp/apps/web/app/api/assets/search/route.ts + - /warp/apps/web/app/api/assets/generate3d/route.ts + - /warp/apps/web/lib/orchestrator/index.ts + - /warp/apps/web/lib/orchestrator/providers/openrouter.ts + - /warp/apps/web/lib/tools/schemas.ts + - /warp/apps/web/lib/diff/rangeEdits.ts + - /warp/apps/web/lib/store/proposals.ts + - /warp/apps/web/lib/store/persist.ts + +- Plugin + - /warp/plugin/src/main.server.lua (dock UI, settings popup, proposals, apply flows) + - /warp/plugin/src/net/http.lua (HTTP helper) + +Notes + +- Localhost default: http://127.0.0.1:3000 +- Plugin stores provider credentials locally via plugin:SetSetting; not committed. +- .gitignore excludes env files and warp/apps/web/data/ (local datastore). + diff --git a/warp/apps/web/app/api/assets/search/route.ts b/warp/apps/web/app/api/assets/search/route.ts index a176828..3694b17 100644 --- a/warp/apps/web/app/api/assets/search/route.ts +++ b/warp/apps/web/app/api/assets/search/route.ts @@ -6,7 +6,13 @@ export async function GET(req: Request) { const { searchParams } = new URL(req.url) const query = searchParams.get('query') || searchParams.get('q') || '' const limit = Math.max(1, Math.min(50, Number(searchParams.get('limit') || '8'))) - - const results = await searchRobloxCatalog(query, limit) - return Response.json({ results, query, limit }) + try { + const results = await searchRobloxCatalog(query, limit) + return Response.json({ results, query, limit }) + } catch (err: any) { + return new Response(JSON.stringify({ error: err?.message || 'Catalog provider error', query, limit }), { + status: 502, + headers: { 'content-type': 'application/json' }, + }) + } } diff --git a/warp/apps/web/app/api/chat/route.ts b/warp/apps/web/app/api/chat/route.ts index 9d8313f..d684e49 100644 --- a/warp/apps/web/app/api/chat/route.ts +++ b/warp/apps/web/app/api/chat/route.ts @@ -36,10 +36,12 @@ export async function POST(req: Request) { return Response.json({ proposals }) } catch (err: any) { - console.error('chat handler error', err) - return new Response( - JSON.stringify({ error: err?.message || 'Invalid request' }), - { status: 400, headers: { 'content-type': 'application/json' } }, - ) + const msg = err?.message || 'Unknown error' + const status = /invalid/i.test(msg) ? 400 : 500 + console.error('chat handler error', msg) + return new Response(JSON.stringify({ error: msg }), { + status, + headers: { 'content-type': 'application/json' }, + }) } } diff --git a/warp/apps/web/app/api/proposals/route.ts b/warp/apps/web/app/api/proposals/route.ts index a2d4830..ed8b4c4 100644 --- a/warp/apps/web/app/api/proposals/route.ts +++ b/warp/apps/web/app/api/proposals/route.ts @@ -1,8 +1,7 @@ export const runtime = 'nodejs' -import { listProposals } from "../../lib/store/proposals" +import { listProposals } from "../../../lib/store/proposals" export async function GET() { return Response.json({ proposals: listProposals() }) } - diff --git a/warp/apps/web/app/layout.tsx b/warp/apps/web/app/layout.tsx new file mode 100644 index 0000000..1bb911c --- /dev/null +++ b/warp/apps/web/app/layout.tsx @@ -0,0 +1,10 @@ +export const metadata = { title: 'Vector Web' } + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + diff --git a/warp/apps/web/app/page.tsx b/warp/apps/web/app/page.tsx index 0cc70c7..56381ba 100644 --- a/warp/apps/web/app/page.tsx +++ b/warp/apps/web/app/page.tsx @@ -5,8 +5,10 @@ export default function Page() {

Local API endpoints for the Roblox Studio copilot (Vector).

Running on localhost:3000 for development. We will move to Vercel later.

diff --git a/warp/apps/web/lib/catalog/search.ts b/warp/apps/web/lib/catalog/search.ts index 6f1b8dd..674150d 100644 --- a/warp/apps/web/lib/catalog/search.ts +++ b/warp/apps/web/lib/catalog/search.ts @@ -1,29 +1,18 @@ export type CatalogItem = { id: number; name: string; creator: string; type: string; thumbnailUrl?: string } export async function searchRobloxCatalog(query: string, limit: number): Promise { - // Real integration can be implemented by setting CATALOG_API_URL to a service that returns - // normalized results: [{ id, name, creator, type, thumbnailUrl }] const url = process.env.CATALOG_API_URL - if (url) { - try { - const res = await fetch(`${url}?query=${encodeURIComponent(query)}&limit=${limit}`) - if (res.ok) { - const js = (await res.json()) as { results?: CatalogItem[] } - if (Array.isArray(js?.results)) return js.results - } - } catch (e) { - // fall through to stub - } + if (!url) { + throw new Error('CATALOG_API_URL not configured') } - // Stub fallback - const samples: CatalogItem[] = [ - { id: 1111111, name: 'Simple Button', creator: 'Roblox', type: 'Model', thumbnailUrl: 'https://tr.rbxcdn.com/4f9b-150x150' }, - { id: 2222222, name: 'Sci-Fi Panel', creator: 'Builder', type: 'Model', thumbnailUrl: 'https://tr.rbxcdn.com/9c2a-150x150' }, - { id: 3333333, name: 'Wooden Crate', creator: 'AssetMaker', type: 'Model', thumbnailUrl: 'https://tr.rbxcdn.com/abcd-150x150' }, - { id: 4444444, name: 'Green Button', creator: 'CreatorX', type: 'Model', thumbnailUrl: 'https://tr.rbxcdn.com/ef12-150x150' }, - { id: 5555555, name: 'Keycard', creator: 'StudioUser', type: 'Model', thumbnailUrl: 'https://tr.rbxcdn.com/12ab-150x150' }, - { id: 6666666, name: 'Door', creator: 'Roblox', type: 'Model', thumbnailUrl: 'https://tr.rbxcdn.com/77aa-150x150' }, - ] - return samples + const res = await fetch(`${url}?query=${encodeURIComponent(query)}&limit=${limit}`) + if (!res.ok) { + const text = await res.text().catch(() => '') + throw new Error(`Catalog provider error ${res.status}: ${text}`) + } + const js = (await res.json()) as { results?: CatalogItem[] } + if (!Array.isArray(js?.results)) { + throw new Error('Invalid catalog response: missing results[]') + } + return js.results } - diff --git a/warp/apps/web/lib/diff/rangeEdits.ts b/warp/apps/web/lib/diff/rangeEdits.ts new file mode 100644 index 0000000..0f1cba6 --- /dev/null +++ b/warp/apps/web/lib/diff/rangeEdits.ts @@ -0,0 +1,42 @@ +export type EditPos = { line: number; character: number } +export type Edit = { start: EditPos; end: EditPos; text: string } + +function posToIndex(text: string, pos: EditPos): number { + const lines = text.split('\n') + const line = Math.max(0, Math.min(pos.line, lines.length)) + const prefix = lines.slice(0, line).join('\n') + const base = prefix.length + (line > 0 ? 1 : 0) + return base + Math.max(0, pos.character) +} + +export function applyRangeEdits(oldText: string, edits: Edit[]): string { + if (!Array.isArray(edits) || edits.length === 0) return oldText + const enriched = edits.map((e) => ({ sidx: posToIndex(oldText, e.start), eidx: posToIndex(oldText, e.end), text: e.text })) + enriched.sort((a, b) => b.sidx - a.sidx) + let next = oldText + for (const e of enriched) { + next = next.slice(0, e.sidx) + e.text + next.slice(e.eidx) + } + return next +} + +export function simpleUnifiedDiff(oldText: string, newText: string, path = 'file'): string { + const a = oldText.split('\n') + const b = newText.split('\n') + let i = 0 + while (i < a.length && i < b.length && a[i] === b[i]) i++ + let j = 0 + while (j < a.length - i && j < b.length - i && a[a.length - 1 - j] === b[b.length - 1 - j]) j++ + + const aStart = i + 1 + const bStart = i + 1 + const aCount = Math.max(0, a.length - i - j) + const bCount = Math.max(0, b.length - i - j) + + const header = [`--- a/${path}`, `+++ b/${path}`, `@@ -${aStart},${aCount} +${bStart},${bCount} @@`] + const lines: string[] = [] + for (let k = 0; k < aCount; k++) lines.push('-' + a[i + k]) + for (let k = 0; k < bCount; k++) lines.push('+' + b[i + k]) + return header.concat(lines).join('\n') +} + diff --git a/warp/apps/web/lib/orchestrator/index.ts b/warp/apps/web/lib/orchestrator/index.ts index 60564bd..3dcc24b 100644 --- a/warp/apps/web/lib/orchestrator/index.ts +++ b/warp/apps/web/lib/orchestrator/index.ts @@ -46,6 +46,7 @@ import { callOpenRouter } from './providers/openrouter' import { z } from 'zod' import { Tools } from '../tools/schemas' import { getSession, setLastTool } from '../store/sessions' +import { applyRangeEdits, simpleUnifiedDiff } from '../diff/rangeEdits' const SYSTEM_PROMPT = `You are Vector, a Roblox Studio copilot. Proposal-first. One tool per message. @@ -124,7 +125,10 @@ function mapToolToProposals(name: string, a: Record, input: ChatInp const path = ensurePath(input.context.activeScript?.path || null) const edits = toEditArray((a as any).edits) if (path && edits) { - proposals.push({ id: id('edit'), type: 'edit', path, notes: `Parsed from ${name}`, diff: { mode: 'rangeEDITS', edits } }) + const old = input.context.activeScript?.text || '' + const next = applyRangeEdits(old, edits) + const unified = simpleUnifiedDiff(old, next, path) + proposals.push({ id: id('edit'), type: 'edit', path, notes: `Parsed from ${name}`, diff: { mode: 'rangeEDITS', edits }, preview: { unified } } as any) return proposals } } @@ -185,9 +189,8 @@ export async function runLLM(input: ChatInput): Promise { const msg = input.message.trim() let providerContent: string | undefined - const useProvider = - (input.provider && input.provider.name === 'openrouter' && !!input.provider.apiKey) || - process.env.VECTOR_USE_OPENROUTER === '1' + const providerRequested = !!(input.provider && input.provider.name === 'openrouter' && !!input.provider.apiKey) + const useProvider = providerRequested || process.env.VECTOR_USE_OPENROUTER === '1' if (useProvider) { try { const resp = await callOpenRouter({ @@ -199,12 +202,18 @@ export async function runLLM(input: ChatInput): Promise { }) providerContent = resp.content || '' } catch (e: any) { + if (providerRequested) { + throw new Error(`Provider error: ${e?.message || 'unknown'}`) + } providerContent = undefined } } // If provider returned a tool-call, parse and map to proposals const tool = providerContent ? parseToolXML(providerContent) : null + if (providerRequested && !tool) { + throw new Error('Provider returned no parseable tool call') + } if (tool) { const name = tool.name as keyof typeof Tools let a: Record = tool.args || {} @@ -273,15 +282,18 @@ export async function runLLM(input: ChatInput): Promise { if (input.context.activeScript) { const path = input.context.activeScript.path const prefixComment = `-- Vector: ${sanitizeComment(msg)}\n` - return [ - { - id: id('edit'), - type: 'edit', - path, - notes: providerContent ? 'Provider response did not include a valid tool call; generated fallback edit.' : 'Insert a comment at the top as a placeholder for an edit.', - diff: { mode: 'rangeEDITS', edits: [{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 }, text: prefixComment }] }, - }, - ] + const edits = [{ start: { line: 0, character: 0 }, end: { line: 0, character: 0 }, text: prefixComment }] + const old = input.context.activeScript.text + const next = applyRangeEdits(old, edits) + const unified = simpleUnifiedDiff(old, next, path) + return [{ + id: id('edit'), + type: 'edit', + path, + notes: providerContent ? 'Provider response did not include a valid tool call; generated fallback edit.' : 'Insert a comment at the top as a placeholder for an edit.', + diff: { mode: 'rangeEDITS', edits }, + preview: { unified }, + } as any] } if (input.context.selection && input.context.selection.length > 0) { diff --git a/warp/apps/web/next-env.d.ts b/warp/apps/web/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/warp/apps/web/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/warp/apps/web/next.config.mjs b/warp/apps/web/next.config.mjs new file mode 100644 index 0000000..94be31c --- /dev/null +++ b/warp/apps/web/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +} + +export default nextConfig diff --git a/warp/apps/web/next.config.ts b/warp/apps/web/next.config.ts deleted file mode 100644 index c0d5f02..0000000 --- a/warp/apps/web/next.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NextConfig } from 'next' - -const nextConfig: NextConfig = { - reactStrictMode: true, -} - -export default nextConfig - diff --git a/warp/apps/web/package-lock.json b/warp/apps/web/package-lock.json new file mode 100644 index 0000000..a40ddca --- /dev/null +++ b/warp/apps/web/package-lock.json @@ -0,0 +1,508 @@ +{ + "name": "vector-warp-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vector-warp-web", + "version": "0.1.0", + "dependencies": { + "next": "14.2.5", + "react": "18.3.1", + "react-dom": "18.3.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.12.7", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "typescript": "^5.4.5" + } + }, + "node_modules/@next/env": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", + "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", + "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", + "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.5", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.5", + "@next/swc-darwin-x64": "14.2.5", + "@next/swc-linux-arm64-gnu": "14.2.5", + "@next/swc-linux-arm64-musl": "14.2.5", + "@next/swc-linux-x64-gnu": "14.2.5", + "@next/swc-linux-x64-musl": "14.2.5", + "@next/swc-win32-arm64-msvc": "14.2.5", + "@next/swc-win32-ia32-msvc": "14.2.5", + "@next/swc-win32-x64-msvc": "14.2.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/warp/apps/web/tsconfig.json b/warp/apps/web/tsconfig.json index a537707..4df5114 100644 --- a/warp/apps/web/tsconfig.json +++ b/warp/apps/web/tsconfig.json @@ -1,7 +1,34 @@ { - "extends": "next/core-web-vitals", "compilerOptions": { - "strict": true - } + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "esModuleInterop": true, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } - diff --git a/warp/plugin/src/main.server.lua b/warp/plugin/src/main.server.lua index 999b658..c6fb013 100644 --- a/warp/plugin/src/main.server.lua +++ b/warp/plugin/src/main.server.lua @@ -407,10 +407,16 @@ local function buildUI(gui) textBox.PlaceholderText = "Ask Vector…" textBox.Text = "" textBox.ClearTextOnFocus = false - textBox.Size = UDim2.new(1, -84, 1, -8) + textBox.Size = UDim2.new(1, -120, 1, -8) textBox.Position = UDim2.new(0, 4, 0, 4) textBox.Parent = inputRow + local settingsBtn = Instance.new("TextButton") + settingsBtn.Text = "⚙" + settingsBtn.Size = UDim2.new(0, 32, 1, -8) + settingsBtn.Position = UDim2.new(1, -116, 0, 4) + settingsBtn.Parent = inputRow + local sendBtn = Instance.new("TextButton") sendBtn.Text = "Send" sendBtn.Size = UDim2.new(0, 72, 1, -8) @@ -431,7 +437,7 @@ local function buildUI(gui) layout.Padding = UDim.new(0, 6) layout.Parent = list - return textBox, sendBtn, list + return textBox, sendBtn, list, settingsBtn end local function renderAssetResults(container, p, results) @@ -518,8 +524,10 @@ local function renderProposals(list, proposals) snippet.Size = UDim2.new(1, -8, 0, 28) snippet.Position = UDim2.new(0, 8, 0, 30) snippet.TextWrapped = true - if p.type == "edit" and p.diff and p.diff.edits and p.diff.edits[1] then - snippet.Text = "Insert: " .. string.sub(p.diff.edits[1].text or "", 1, 120) + if p.type == "edit" and p.preview and p.preview.unified then + snippet.Text = string.sub(p.preview.unified, 1, 300) + elseif p.type == "edit" and p.diff and p.diff.edits and p.diff.edits[1] then + snippet.Text = "Insert: " .. string.sub(p.diff.edits[1].text or "", 1, 200) elseif p.type == "object_op" and p.ops and p.ops[1] and p.ops[1].op == "rename_instance" then snippet.Text = "Rename → " .. tostring(p.ops[1].newName) else @@ -698,7 +706,12 @@ toggleButton.Click:Connect(function() local gui = plugin:CreateDockWidgetPluginGui("VectorDock", info) gui.Title = "Vector" - local input, sendBtn, list = buildUI(gui) + local input, sendBtn, list, settingsBtn = buildUI(gui) + + -- Open in-dock settings popup + settingsBtn.MouseButton1Click:Connect(function() + openSettings() + end) sendBtn.MouseButton1Click:Connect(function() local ctx = { @@ -706,26 +719,36 @@ toggleButton.Click:Connect(function() selection = getSelectionContext(), } local resp = sendChat("local", input.Text, ctx) - if not resp.Success then - local item = Instance.new("TextLabel") - item.Size = UDim2.new(1, -8, 0, 24) - item.Text = "HTTP error: " .. tostring(resp.StatusCode) - item.BackgroundTransparency = 1 - item.Parent = list - return - end - local ok, parsed = pcall(function() - return HttpService:JSONDecode(resp.Body) - end) - if not ok then - local item = Instance.new("TextLabel") - item.Size = UDim2.new(1, -8, 0, 24) - item.Text = "Invalid JSON from server" - item.BackgroundTransparency = 1 - item.Parent = list - return - end - renderProposals(list, parsed.proposals or {}) + if not resp.Success then + local item = Instance.new("TextLabel") + item.Size = UDim2.new(1, -8, 0, 48) + item.TextWrapped = true + item.Text = "HTTP " .. tostring(resp.StatusCode) .. ": " .. (resp.Body or "") + item.BackgroundTransparency = 1 + item.Parent = list + return + end + local ok, parsed = pcall(function() + return HttpService:JSONDecode(resp.Body) + end) + if not ok then + local item = Instance.new("TextLabel") + item.Size = UDim2.new(1, -8, 0, 24) + item.Text = "Invalid JSON from server" + item.BackgroundTransparency = 1 + item.Parent = list + return + end + if parsed.error then + local item = Instance.new("TextLabel") + item.Size = UDim2.new(1, -8, 0, 48) + item.TextWrapped = true + item.Text = "Error: " .. tostring(parsed.error) + item.BackgroundTransparency = 1 + item.Parent = list + return + end + renderProposals(list, parsed.proposals or {}) end) end) @@ -779,30 +802,86 @@ local function openSettings() return t end - local cfg = loadProviderSettings() - mkLabel("API Provider: OpenAI Compatible (OpenRouter)", 12) - mkLabel("Base URL", 42) - local baseInput = mkInput(62) - baseInput.Text = cfg.baseUrl - - mkLabel("API Key", 98) - local keyInput = mkInput(118) - keyInput.Text = cfg.apiKey - - mkLabel("Model ID", 154) - local modelInput = mkInput(174) - modelInput.Text = cfg.model - - local saveBtn = Instance.new("TextButton") - saveBtn.Text = "Done" - saveBtn.Size = UDim2.new(0, 96, 0, 28) - saveBtn.Position = UDim2.new(1, -108, 1, -40) - saveBtn.Parent = root - saveBtn.MouseButton1Click:Connect(function() - saveProviderSettings({ baseUrl = baseInput.Text, apiKey = keyInput.Text, model = modelInput.Text }) - gui.Enabled = false - gui:Destroy() - end) + local cfg = loadProviderSettings() + mkLabel("Provider (OpenAI-compatible)", 12) + local providerLbl = Instance.new("TextLabel") + providerLbl.Text = "OpenRouter (fixed for now)" + providerLbl.TextXAlignment = Enum.TextXAlignment.Left + providerLbl.BackgroundTransparency = 1 + providerLbl.Position = UDim2.new(0, 12, 0, 32) + providerLbl.Size = UDim2.new(1, -24, 0, 20) + providerLbl.Parent = root + + mkLabel("Base URL", 58) + local baseInput = mkInput(78) + baseInput.Text = cfg.baseUrl + + mkLabel("API Key", 114) + local keyInput = mkInput(134) + keyInput.Text = cfg.apiKey + + mkLabel("Model", 170) + local modelInput = mkInput(190) + modelInput.Text = cfg.model + + -- Quick preset for Kimi K2 + local presetBtn = Instance.new("TextButton") + presetBtn.Text = "Use Kimi K2 (recommended)" + presetBtn.Size = UDim2.new(0, 220, 0, 24) + presetBtn.Position = UDim2.new(0, 12, 0, 222) + presetBtn.Parent = root + presetBtn.MouseButton1Click:Connect(function() + modelInput.Text = "moonshotai/kimi-k2:free" + end) + + -- Internal testing note + local note = Instance.new("TextLabel") + note.Text = "Note: For internal testing only. Your API key is stored locally via plugin:SetSetting and not committed." + note.TextWrapped = true + note.TextXAlignment = Enum.TextXAlignment.Left + note.BackgroundTransparency = 1 + note.Position = UDim2.new(0, 12, 0, 252) + note.Size = UDim2.new(1, -24, 0, 40) + note.TextColor3 = Color3.fromRGB(180, 180, 180) + note.Parent = root + + -- Test button + local testBtn = Instance.new("TextButton") + testBtn.Text = "Test" + testBtn.Size = UDim2.new(0, 96, 0, 28) + testBtn.Position = UDim2.new(1, -216, 1, -40) + testBtn.Parent = root + testBtn.MouseButton1Click:Connect(function() + testBtn.Text = "Testing…" + local resp = sendChat("settings_test", "Ping from Settings", { activeScript = nil, selection = {} }) + if not resp.Success then + testBtn.Text = "HTTP " .. tostring(resp.StatusCode) + return + end + local ok, parsed = pcall(function() + return HttpService:JSONDecode(resp.Body) + end) + if not ok or parsed.error then + testBtn.Text = "Error" + else + testBtn.Text = "OK" + end + -- reset after short delay + task.delay(2, function() + testBtn.Text = "Test" + end) + end) + + local saveBtn = Instance.new("TextButton") + saveBtn.Text = "Done" + saveBtn.Size = UDim2.new(0, 96, 0, 28) + saveBtn.Position = UDim2.new(1, -108, 1, -40) + saveBtn.Parent = root + saveBtn.MouseButton1Click:Connect(function() + saveProviderSettings({ baseUrl = baseInput.Text, apiKey = keyInput.Text, model = modelInput.Text }) + gui.Enabled = false + gui:Destroy() + end) end settingsButton.Click:Connect(function()