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).
- POST /api/chat
- - GET /api/assets/search
+ - GET /api/proposals
- POST /api/proposals/[id]/apply
+ - GET /api/assets/search
+ - POST /api/assets/generate3d
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()