From 8e4cb192625e0e40f763498221644493eec1a861 Mon Sep 17 00:00:00 2001 From: Arya Teja Rudraraju Date: Sat, 4 Apr 2026 20:11:50 -0700 Subject: [PATCH 1/7] Initialize task: New task --- .zenflow/tasks/new-task-7cad/plan.md | 67 ++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .zenflow/tasks/new-task-7cad/plan.md diff --git a/.zenflow/tasks/new-task-7cad/plan.md b/.zenflow/tasks/new-task-7cad/plan.md new file mode 100644 index 0000000..ac275b8 --- /dev/null +++ b/.zenflow/tasks/new-task-7cad/plan.md @@ -0,0 +1,67 @@ +# Spec and build + +## Configuration +- **Artifacts Path**: {@artifacts_path} → `.zenflow/tasks/{task_id}` + +--- + +## Agent Instructions + +Ask the user questions when anything is unclear or needs their input. This includes: +- Ambiguous or incomplete requirements +- Technical decisions that affect architecture or user experience +- Trade-offs that require business context + +Do not make assumptions on important decisions — get clarification first. + +--- + +## Workflow Steps + +### [ ] Step: Technical Specification + +Assess the task's difficulty, as underestimating it leads to poor outcomes. +- easy: Straightforward implementation, trivial bug fix or feature +- medium: Moderate complexity, some edge cases or caveats to consider +- hard: Complex logic, many caveats, architectural considerations, or high-risk changes + +Create a technical specification for the task that is appropriate for the complexity level: +- Review the existing codebase architecture and identify reusable components. +- Define the implementation approach based on established patterns in the project. +- Identify all source code files that will be created or modified. +- Define any necessary data model, API, or interface changes. +- Describe verification steps using the project's test and lint commands. + +Save the output to `{@artifacts_path}/spec.md` with: +- Technical context (language, dependencies) +- Implementation approach +- Source code structure changes +- Data model / API / interface changes +- Verification approach + +If the task is complex enough, create a detailed implementation plan based on `{@artifacts_path}/spec.md`: +- Break down the work into concrete tasks (incrementable, testable milestones) +- Each task should reference relevant contracts and include verification steps +- Replace the Implementation step below with the planned tasks + +Rule of thumb for step size: each step should represent a coherent unit of work (e.g., implement a component, add an API endpoint, write tests for a module). Avoid steps that are too granular (single function). + +Important: unit tests must be part of each implementation task, not separate tasks. Each task should implement the code and its tests together, if relevant. + +Save to `{@artifacts_path}/plan.md`. If the feature is trivial and doesn't warrant this breakdown, keep the Implementation step below as is. + +--- + +### [ ] Step: Implementation + +Implement the task according to the technical specification and general engineering best practices. + +1. Break the task into steps where possible. +2. Implement the required changes in the codebase +3. If relevant, write unit tests alongside each change. +4. Run relevant tests and linters in the end of each step. +5. Perform basic manual verification if applicable. +6. After completion, write a report to `{@artifacts_path}/report.md` describing: + - What was implemented + - How the solution was tested + - The biggest issues or challenges encountered From 31d85b190cf0e45d28df0ea212244cffbd13140b Mon Sep 17 00:00:00 2001 From: Arya Teja Rudraraju Date: Sun, 5 Apr 2026 02:11:22 -0700 Subject: [PATCH 2/7] Technical Specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Technical Specification step complete.** Here's what was produced: **Spec** (`.zenflow/tasks/new-task-7cad/spec.md`): - Assessed as **hard** — multiple interacting subsystems with cross-cutting concerns - Key finding: `lib/browser-engine.ts` **does not exist** (must be created, not "fixed") - `cleanResponse()` is missing `` block stripping entirely - Documented all new/modified files, interface changes, risk assessment, and verification approach **Plan** (`.zenflow/tasks/new-task-7cad/plan.md`) — broken into 6 implementation steps: 1. **Fix cleanResponse + create browser engine** — `@huggingface/transformers`, new `browser-engine.ts`, `` stripping 2. **Build WebContainer sandbox** — `@webcontainer/api`, singleton boot/exec, React hook with history 3. **Wire up UI** — mode selector, browser translate path, WebContainer execution 4. **Configure COOP/COEP headers** — SharedArrayBuffer headers without breaking Analytics 5. **Deploy to Vercel** — merge, env vars, Cloudflare DNS, smoke test 6. **Write report** --- .zenflow/tasks/new-task-7cad/plan.md | 92 +++++---- .zenflow/tasks/new-task-7cad/spec.md | 271 +++++++++++++++++++++++++++ 2 files changed, 328 insertions(+), 35 deletions(-) create mode 100644 .zenflow/tasks/new-task-7cad/spec.md diff --git a/.zenflow/tasks/new-task-7cad/plan.md b/.zenflow/tasks/new-task-7cad/plan.md index ac275b8..b1f74cd 100644 --- a/.zenflow/tasks/new-task-7cad/plan.md +++ b/.zenflow/tasks/new-task-7cad/plan.md @@ -18,50 +18,72 @@ Do not make assumptions on important decisions — get clarification first. ## Workflow Steps -### [ ] Step: Technical Specification +### [x] Step: Technical Specification + -Assess the task's difficulty, as underestimating it leads to poor outcomes. -- easy: Straightforward implementation, trivial bug fix or feature -- medium: Moderate complexity, some edge cases or caveats to consider -- hard: Complex logic, many caveats, architectural considerations, or high-risk changes +Assessed as **hard** — multiple interacting subsystems, new dependencies, cross-cutting header concerns. +Full spec saved to `.zenflow/tasks/new-task-7cad/spec.md`. -Create a technical specification for the task that is appropriate for the complexity level: -- Review the existing codebase architecture and identify reusable components. -- Define the implementation approach based on established patterns in the project. -- Identify all source code files that will be created or modified. -- Define any necessary data model, API, or interface changes. -- Describe verification steps using the project's test and lint commands. +--- + +### [ ] Step: Fix cleanResponse and create browser inference engine + +Create `lib/browser-engine.ts` (Transformers.js pipeline for in-browser WebGPU inference) and update `lib/clean-response.ts` to strip `` blocks from model output. + +- Install `@huggingface/transformers` +- Create `lib/browser-engine.ts` with singleton pipeline, lazy loading, progress callbacks, and status events +- Update `lib/clean-response.ts` to strip `...` blocks before other cleaning +- Verify: `npx tsc --noEmit && npm run lint && npm run build` + +--- + +### [ ] Step: Build WebContainer sandbox + +Create the in-browser sandbox using WebContainers to replace the Docker relay for demo use. + +- Install `@webcontainer/api` +- Create `lib/webcontainer-sandbox.ts` (boot, exec, teardown singleton) +- Create `hooks/use-webcontainer.ts` (React hook: boot-on-first-run, exec, history tracking) +- Update `types/sandbox.d.ts` to make `auditId` optional +- Verify: `npx tsc --noEmit && npm run lint && npm run build` -Save the output to `{@artifacts_path}/spec.md` with: -- Technical context (language, dependencies) -- Implementation approach -- Source code structure changes -- Data model / API / interface changes -- Verification approach +--- + +### [ ] Step: Wire up UI — mode selector, browser translate, sandbox execution + +Integrate browser inference and WebContainer sandbox into the main UI. -If the task is complex enough, create a detailed implementation plan based on `{@artifacts_path}/spec.md`: -- Break down the work into concrete tasks (incrementable, testable milestones) -- Each task should reference relevant contracts and include verification steps -- Replace the Implementation step below with the planned tasks +- Add inference mode selector (Cloud / Browser / Auto) to `components/shell-session.tsx` +- Update `hooks/use-translate.ts` to accept mode parameter and call browser engine directly in browser mode +- Replace `useSandbox()` with `useWebContainer()` in shell-session for WebContainer execution +- Update `components/execution-output.tsx` for optional auditId and command history display +- Verify: `npx tsc --noEmit && npm run lint && npm run build` + +--- -Rule of thumb for step size: each step should represent a coherent unit of work (e.g., implement a component, add an API endpoint, write tests for a module). Avoid steps that are too granular (single function). +### [ ] Step: Configure COOP/COEP headers and CSP updates -Important: unit tests must be part of each implementation task, not separate tasks. Each task should implement the code and its tests together, if relevant. +Add required headers for WebContainers (SharedArrayBuffer) without breaking Vercel Analytics. -Save to `{@artifacts_path}/plan.md`. If the feature is trivial and doesn't warrant this breakdown, keep the Implementation step below as is. +- Update `next.config.ts` with COOP/COEP headers (try `credentialless` first) +- Update CSP `connect-src` for WebContainer and Transformers.js origins +- Test that Vercel Analytics still loads; fall back to route-specific headers if broken +- Verify: `npx tsc --noEmit && npm run lint && npm run build` --- -### [ ] Step: Implementation +### [ ] Step: Deploy to Vercel and configure nl2shell.com domain + +Push to main, verify auto-deploy, configure domain and env vars. + +- Merge feature branch to main +- Set Vercel env vars: `HF_TOKEN`, `NEXT_PUBLIC_SANDBOX_ENABLED` +- Configure nl2shell.com CNAME → cname.vercel-dns.com in Cloudflare (DNS-only) +- Verify: `curl -I https://nl2shell.com` returns 200 with correct headers +- Manual smoke test: Cloud mode, Browser mode, Sandbox execution + +--- -Implement the task according to the technical specification and general engineering best practices. +### [ ] Step: Write implementation report -1. Break the task into steps where possible. -2. Implement the required changes in the codebase -3. If relevant, write unit tests alongside each change. -4. Run relevant tests and linters in the end of each step. -5. Perform basic manual verification if applicable. -6. After completion, write a report to `{@artifacts_path}/report.md` describing: - - What was implemented - - How the solution was tested - - The biggest issues or challenges encountered +- Write `{@artifacts_path}/report.md` describing what was implemented, how it was tested, and challenges encountered diff --git a/.zenflow/tasks/new-task-7cad/spec.md b/.zenflow/tasks/new-task-7cad/spec.md new file mode 100644 index 0000000..55fda81 --- /dev/null +++ b/.zenflow/tasks/new-task-7cad/spec.md @@ -0,0 +1,271 @@ +# Technical Specification: NL2Shell Web — Browser Inference, Sandbox, and Deployment + +**Difficulty:** Hard +**Rationale:** Multiple interacting subsystems (browser inference pipeline, WebContainer sandbox, COOP/COEP headers affecting third-party scripts, Vercel deployment), significant new code, and cross-cutting concerns (CSP headers, Vercel Analytics compatibility). + +--- + +## Technical Context + +- **Framework:** Next.js 16.1.6 (App Router), React 19.2, TypeScript (strict) +- **Styling:** Tailwind CSS 4, shadcn/ui components +- **Current inference:** Cloud-only via `@gradio/client` → HuggingFace Space `AryaYT/nl2shell-demo` +- **Current sandbox:** Docker relay server (`relay/`) — not deployed, requires Railway +- **Deployment target:** Vercel (project linked in `.vercel/project.json`) +- **Domain:** nl2shell.com (Cloudflare DNS) +- **No test framework configured** — no test files exist + +--- + +## Current State Analysis + +### What exists +| Component | Status | Location | +|-----------|--------|----------| +| Cloud translation API | Working | `app/api/translate/route.ts` | +| `useTranslate` hook | Working | `hooks/use-translate.ts` | +| Docker relay sandbox | Implemented, not deployed | `relay/`, `hooks/use-sandbox.ts`, `app/api/execute/route.ts` | +| `cleanResponse()` | Working for cloud mode | `lib/clean-response.ts` | +| Safety checks | Working (22 patterns) | `lib/safety.ts` | +| MCP server | Working | `app/api/mcp/route.ts` | +| Vercel Analytics | Configured in layout | `app/layout.tsx` | + +### What's missing +| Component | Status | Notes | +|-----------|--------|-------| +| `lib/browser-engine.ts` | **Does not exist** | SPEC says "fix" but file is absent — must create from scratch | +| `` block stripping | Missing in `cleanResponse()` | Current regex handles markdown fences, not `` tags | +| WebContainer sandbox | Not started | Replaces Docker relay for demo use case | +| COOP/COEP headers | Not configured | Required by WebContainers for SharedArrayBuffer | +| Mode selector UI | Not implemented | No Cloud/Browser/Auto toggle in current UI | +| Vercel deployment | Not done | Domain not configured | + +--- + +## Implementation Approach + +### Task 1: Browser Inference Engine (Create `lib/browser-engine.ts`) + +**New dependency:** `@huggingface/transformers` (Transformers.js v3) + +**Architecture:** +- Singleton pipeline pattern — load model once, reuse across calls +- `"use client"` module (WebGPU is browser-only) +- Chat-template messages format for Qwen3.5 instruction model +- System prompt matches the Gradio Space's prompt for consistency + +**Pipeline output shape** (Transformers.js `text-generation` with chat messages): +```typescript +// Returns: Array<{ generated_text: Array<{ role: string; content: string }> }> +// The assistant's response is the last message in generated_text +``` + +**Key design decisions:** +- Use `onnx-community/Qwen2.5-0.5B-Instruct` as initial model (smaller, faster for demo; SPEC's `Qwen3.5-0.8B-ONNX` can replace later when converted) +- Model ID configurable via constant for easy swap +- Progress callback for download/load status reporting to UI +- Lazy loading — pipeline only created on first `generate()` call + +**Interface:** +```typescript +export interface BrowserEngineStatus { + stage: "idle" | "downloading" | "loading" | "ready" | "generating" | "error"; + progress?: number; // 0-100 for download + error?: string; +} + +export function generate(query: string): Promise +export function getStatus(): BrowserEngineStatus +export function isReady(): boolean +export function onStatusChange(cb: (s: BrowserEngineStatus) => void): () => void +``` + +### Task 1b: Fix `cleanResponse()` for `` blocks + +The Qwen model family (especially instruction-tuned variants) often wraps reasoning in `...` tags before the actual answer. Current `cleanResponse()` only handles markdown fences. + +**Changes to `lib/clean-response.ts`:** +- Add `` block stripping as the FIRST operation (before markdown fence removal) +- Regex: `/^[\s\S]*?<\/think>\s*/` — matches `` at start, any content (including newlines), closing tag, trailing whitespace +- Handle edge cases: empty think block, no think block, think block with no content after it + +### Task 2: WebContainer Sandbox + +**New dependency:** `@webcontainer/api` + +**Architecture:** +- `lib/webcontainer-sandbox.ts` — singleton WebContainer boot, exec, teardown +- `hooks/use-webcontainer.ts` — React state management, boot-on-first-run, command history +- Replaces Docker relay for the web demo (relay code stays for self-hosted/MCP use) + +**Key design decisions:** +- WebContainer boots lazily on first "Run" click, not on page load (saves resources) +- Command history persists in hook state (not localStorage — session-scoped) +- `ExecResult` interface mirrors existing `ExecutionResult` type but without `auditId` (no server-side audit for browser sandbox) +- The `useSandbox` hook is replaced by `useWebContainer` in `shell-session.tsx` when `NEXT_PUBLIC_SANDBOX_ENABLED` is `"webcontainer"` or `true` + +**WebContainer ExecResult:** +```typescript +export interface ExecResult { + stdout: string; + stderr: string; + exitCode: number; + durationMs: number; +} +``` + +### Task 3: COOP/COEP Headers + +**Problem:** WebContainers require `Cross-Origin-Embedder-Policy` and `Cross-Origin-Opener-Policy` headers for SharedArrayBuffer. These headers can break: +- Vercel Analytics (`@vercel/analytics`) — loads external script +- Gradio client connections to `huggingface.co` +- Any third-party iframe/script + +**Strategy:** +1. First try `credentialless` instead of `require-corp` for COEP (less restrictive, Chrome 96+) +2. If WebContainers work with `credentialless`, use that globally +3. If not, add strict COOP/COEP only to a `/sandbox` route segment and keep main page without them +4. Test Vercel Analytics compatibility in each configuration + +**Changes to `next.config.ts`:** +- Add COOP/COEP headers (strategy TBD based on testing) +- Update CSP `connect-src` to allow WebContainer origins if needed + +### Task 4: Wire Up UI + +**Changes to `components/shell-session.tsx`:** +- Add mode selector (Cloud / Browser / Auto) — simple button group or dropdown +- In Browser mode: use `generate()` from `lib/browser-engine.ts` instead of `POST /api/translate` +- Auto mode: try browser first, fall back to cloud on error +- Show model download progress bar when Browser mode first loads +- Replace `useSandbox()` with `useWebContainer()` for execution + +**Changes to `components/execution-output.tsx`:** +- Make `auditId` optional (WebContainer exec has no audit trail) +- Support rendering command history (multiple exec results in sequence) + +**Changes to `hooks/use-translate.ts`:** +- Accept a `mode` parameter or create a new `useBrowserTranslate` hook +- Browser mode calls `generate()` directly (no fetch) + +### Task 5: Deployment + +- Merge to main, push, Vercel auto-deploys +- Set env vars: `HF_TOKEN`, `NEXT_PUBLIC_SANDBOX_ENABLED=true` +- Configure domain: nl2shell.com CNAME → cname.vercel-dns.com (Cloudflare, DNS-only) +- Verify SSL, headers, all modes working + +### Task 6: Cloud Mode UX Improvements (if time permits) + +- Better loading states for 503 (Space sleeping) +- Auto-retry on 503 with "Model is waking up..." message +- Already partially handled in `route.ts` error responses + +--- + +## Source Code Structure Changes + +### New files +| File | Purpose | +|------|---------| +| `lib/browser-engine.ts` | Transformers.js pipeline, model loading, text generation | +| `lib/webcontainer-sandbox.ts` | WebContainer boot, exec, teardown singleton | +| `hooks/use-webcontainer.ts` | React hook for sandbox lifecycle + history | + +### Modified files +| File | Changes | +|------|---------| +| `lib/clean-response.ts` | Add `` block stripping | +| `components/shell-session.tsx` | Mode selector, browser inference path, WebContainer sandbox | +| `components/execution-output.tsx` | Optional `auditId`, history rendering | +| `hooks/use-translate.ts` | Support browser mode (or new hook) | +| `next.config.ts` | COOP/COEP headers, CSP updates | +| `package.json` | Add `@huggingface/transformers`, `@webcontainer/api` | +| `types/sandbox.d.ts` | Add `WebContainerExecResult` or make `auditId` optional | + +### Unchanged (no modifications needed) +| File | Reason | +|------|--------| +| `app/api/translate/route.ts` | Cloud mode stays as-is | +| `app/api/execute/route.ts` | Docker relay stays for future self-hosted use | +| `relay/*` | Docker relay untouched | +| `lib/safety.ts` | Already handles both modes' output | +| `app/layout.tsx` | Vercel Analytics stays; COOP/COEP handled in next.config.ts | + +--- + +## Interface Changes + +### `ExecutionResult` type update +```typescript +// types/sandbox.d.ts +export interface ExecutionResult { + stdout: string; + stderr: string; + exitCode: number; + durationMs: number; + auditId?: string; // Optional — absent for WebContainer exec +} +``` + +### New `InferenceMode` type +```typescript +type InferenceMode = "cloud" | "browser" | "auto"; +``` + +### `useTranslate` hook extension +```typescript +// Option A: mode parameter +export function useTranslate(mode?: InferenceMode) + +// Option B: separate hook for browser (cleaner separation) +export function useBrowserTranslate() +``` + +Decision: **Option A** — single hook with mode parameter, keeps `shell-session.tsx` simpler. + +--- + +## Verification Approach + +After each implementation step: +```bash +npx tsc --noEmit # Zero type errors +npm run lint # Zero lint errors +npm run build # Clean production build +``` + +Manual testing (dev server): +1. Cloud mode: query → shell command (existing flow, regression check) +2. Browser mode: model downloads → query → shell command (no `` in output) +3. Sandbox: boot WebContainer → execute → see output → filesystem persists +4. Mode selector: all three modes switch correctly +5. Danger warning: `rm -rf /` shows red badge in all modes + +Production verification: +```bash +curl -I https://nl2shell.com # 200 OK, correct headers +``` + +--- + +## Risk Assessment + +| Risk | Impact | Mitigation | +|------|--------|------------| +| COOP/COEP breaks Vercel Analytics | Medium | Test `credentialless` first; fallback to route-specific headers | +| WebContainer boot slow (>5s) | Low | Lazy boot on first "Run", show spinner | +| Transformers.js model too large | Medium | Start with 0.5B model (~300MB ONNX); upgrade to fine-tuned later | +| CSP blocks WebContainer origins | Medium | Add required origins to `connect-src` incrementally | +| HF Space sleeping on first visit | Low | Already handled with 503 retry messages in translate API | + +--- + +## Dependencies to Install + +```bash +npm install @huggingface/transformers @webcontainer/api +``` + +- `@huggingface/transformers` — Transformers.js v3, ONNX Runtime Web, WebGPU inference +- `@webcontainer/api` — StackBlitz WebContainers for in-browser command execution From 8e76721519c17171cf13041476d91565221d57b2 Mon Sep 17 00:00:00 2001 From: Arya Teja Rudraraju Date: Sun, 5 Apr 2026 02:14:08 -0700 Subject: [PATCH 3/7] Fix cleanResponse and create browser inference engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step complete. Here's what was done: **Created `lib/browser-engine.ts`** — Transformers.js pipeline for in-browser WebGPU inference: - Singleton pipeline pattern with lazy loading (loads on first `generate()` call) - Uses `onnx-community/Qwen2.5-0.5B-Instruct` with `q4f16` quantization on WebGPU - Status event system (`onStatusChange`) for UI progress tracking (idle → downloading → loading → ready → generating) - Handles both chat-mode and plain-mode pipeline output shapes - Pipes output through `cleanResponse()` automatically **Updated `lib/clean-response.ts`** — Added `` block stripping as the first cleaning operation: - Regex `^[\s\S]*?<\/think>\s*` handles empty blocks, multiline content, and trailing whitespace - Runs before markdown fence removal so the pipeline is: strip think → strip fences → strip backticks → strip prompts → strip preambles → strip comments **Fixed pre-existing build issue** — Excluded `relay/` from `tsconfig.json` (it has unresolved `uuid`/`better-sqlite3` type deps that were breaking `next build`). All checks pass: `tsc --noEmit` (0 errors), `eslint` (0 warnings), `next build` (clean). --- .zenflow/tasks/new-task-7cad/plan.md | 12 +- lib/browser-engine.ts | 115 ++++++++++ lib/clean-response.ts | 5 + package-lock.json | 324 ++++++++++++++++++++++++++- package.json | 1 + tsconfig.json | 2 +- 6 files changed, 444 insertions(+), 15 deletions(-) create mode 100644 lib/browser-engine.ts diff --git a/.zenflow/tasks/new-task-7cad/plan.md b/.zenflow/tasks/new-task-7cad/plan.md index b1f74cd..a4cf74e 100644 --- a/.zenflow/tasks/new-task-7cad/plan.md +++ b/.zenflow/tasks/new-task-7cad/plan.md @@ -26,14 +26,16 @@ Full spec saved to `.zenflow/tasks/new-task-7cad/spec.md`. --- -### [ ] Step: Fix cleanResponse and create browser inference engine +### [x] Step: Fix cleanResponse and create browser inference engine + Create `lib/browser-engine.ts` (Transformers.js pipeline for in-browser WebGPU inference) and update `lib/clean-response.ts` to strip `` blocks from model output. -- Install `@huggingface/transformers` -- Create `lib/browser-engine.ts` with singleton pipeline, lazy loading, progress callbacks, and status events -- Update `lib/clean-response.ts` to strip `...` blocks before other cleaning -- Verify: `npx tsc --noEmit && npm run lint && npm run build` +- [x] Install `@huggingface/transformers` +- [x] Create `lib/browser-engine.ts` with singleton pipeline, lazy loading, progress callbacks, and status events +- [x] Update `lib/clean-response.ts` to strip `...` blocks before other cleaning +- [x] Exclude `relay/` from tsconfig (pre-existing build error) +- [x] Verify: `npx tsc --noEmit && npm run lint && npm run build` --- diff --git a/lib/browser-engine.ts b/lib/browser-engine.ts new file mode 100644 index 0000000..0c47150 --- /dev/null +++ b/lib/browser-engine.ts @@ -0,0 +1,115 @@ +"use client"; + +import { pipeline, TextGenerationPipeline } from "@huggingface/transformers"; +import { cleanResponse } from "@/lib/clean-response"; + +const MODEL_ID = "onnx-community/Qwen2.5-0.5B-Instruct"; + +const SYSTEM_PROMPT = `You are NL2Shell, a tool that converts natural language to shell commands. +Rules: +- Output ONLY the shell command, nothing else +- No explanations, no markdown, no code fences +- If the request is ambiguous, pick the most common interpretation +- Use standard Unix/Linux commands`; + +export interface BrowserEngineStatus { + stage: "idle" | "downloading" | "loading" | "ready" | "generating" | "error"; + progress?: number; + error?: string; +} + +type StatusCallback = (status: BrowserEngineStatus) => void; + +let pipelineInstance: TextGenerationPipeline | null = null; +let loadPromise: Promise | null = null; +let currentStatus: BrowserEngineStatus = { stage: "idle" }; +const listeners = new Set(); + +function setStatus(status: BrowserEngineStatus) { + currentStatus = status; + for (const cb of listeners) cb(status); +} + +export function getStatus(): BrowserEngineStatus { + return currentStatus; +} + +export function isReady(): boolean { + return pipelineInstance !== null; +} + +export function onStatusChange(cb: StatusCallback): () => void { + listeners.add(cb); + return () => listeners.delete(cb); +} + +async function loadPipeline(): Promise { + if (pipelineInstance) return pipelineInstance; + if (loadPromise) return loadPromise; + + loadPromise = (async () => { + setStatus({ stage: "downloading", progress: 0 }); + + const pipe = await pipeline("text-generation", MODEL_ID, { + dtype: "q4f16", + device: "webgpu", + progress_callback: (progress: { progress?: number; status?: string }) => { + if (progress.status === "progress" && typeof progress.progress === "number") { + setStatus({ stage: "downloading", progress: Math.round(progress.progress) }); + } + }, + }); + + setStatus({ stage: "loading" }); + pipelineInstance = pipe as TextGenerationPipeline; + setStatus({ stage: "ready" }); + return pipelineInstance; + })(); + + try { + return await loadPromise; + } catch (err) { + loadPromise = null; + const message = err instanceof Error ? err.message : "Failed to load model"; + setStatus({ stage: "error", error: message }); + throw err; + } +} + +export async function generate(query: string): Promise { + const pipe = await loadPipeline(); + setStatus({ stage: "generating" }); + + const messages = [ + { role: "system", content: SYSTEM_PROMPT }, + { role: "user", content: query }, + ]; + + const result = await pipe(messages, { + max_new_tokens: 128, + temperature: 0.1, + do_sample: true, + return_full_text: false, + }); + + setStatus({ stage: "ready" }); + + // Transformers.js text-generation with chat messages returns: + // [{ generated_text: [{ role, content }, ...] }] (chat mode) + // OR [{ generated_text: "string" }] (plain mode) + const output = result[0]?.generated_text; + + let raw: string; + if (Array.isArray(output)) { + // Chat mode: find the assistant's reply (last message) + const lastMsg = output.at(-1); + raw = + typeof lastMsg === "object" && lastMsg !== null && "content" in lastMsg + ? String(lastMsg.content) + : ""; + } else { + raw = typeof output === "string" ? output : ""; + } + + return cleanResponse(raw); +} diff --git a/lib/clean-response.ts b/lib/clean-response.ts index 98a671b..574f342 100644 --- a/lib/clean-response.ts +++ b/lib/clean-response.ts @@ -1,6 +1,11 @@ export function cleanResponse(text: string): string { let cleaned = text.trim(); + // Strip ... blocks (Qwen reasoning tags) + // Handles: empty blocks, blocks with content, multiline content + cleaned = cleaned.replace(/^[\s\S]*?<\/think>\s*/, ""); + cleaned = cleaned.trim(); + // Remove markdown code fences (```bash ... ```) cleaned = cleaned.replace(/^```(?:bash|sh|shell|zsh)?\n?/, ""); cleaned = cleaned.replace(/\n?```$/, ""); diff --git a/package-lock.json b/package-lock.json index 0760237..1d0d060 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@base-ui/react": "^1.2.0", "@gradio/client": "^2.1.0", + "@huggingface/transformers": "^4.0.1", "@supabase/supabase-js": "^2.99.1", "@vercel/analytics": "^2.0.0", "@vercel/speed-insights": "^2.0.0", @@ -1332,6 +1333,34 @@ "hono": "^4" } }, + "node_modules/@huggingface/jinja": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.6.tgz", + "integrity": "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/tokenizers": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz", + "integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==", + "license": "Apache-2.0" + }, + "node_modules/@huggingface/transformers": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.0.1.tgz", + "integrity": "sha512-tAQYEy+cnW0ku/NxBSjFXCymi+DZa1/JkoGf4McxjzO36CZZIL/J4TF6X7i/tzs75yTjshUDgsvSz03s2xym2A==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.6", + "@huggingface/tokenizers": "^0.1.3", + "onnxruntime-node": "1.24.3", + "onnxruntime-web": "1.25.0-dev.20260327-722743c0e2", + "sharp": "^0.34.5" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "devOptional": true, @@ -1379,7 +1408,6 @@ "node_modules/@img/colour": { "version": "1.1.0", "license": "MIT", - "optional": true, "engines": { "node": ">=18" } @@ -4576,6 +4604,70 @@ "license": "MIT", "peer": true }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.2", "license": "MIT", @@ -6599,6 +6691,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "license": "MIT", @@ -7272,6 +7373,13 @@ "license": "ISC", "peer": true }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "devOptional": true, @@ -8355,7 +8463,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -8381,7 +8488,6 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -8427,6 +8533,12 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, "node_modules/devalue": { "version": "5.6.4", "license": "MIT", @@ -8806,6 +8918,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.3", "hasInstallScript": true, @@ -8859,7 +8977,6 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -9635,6 +9752,12 @@ "node": ">=16" } }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, "node_modules/flatted": { "version": "3.4.1", "devOptional": true, @@ -9995,6 +10118,35 @@ "node": "18 || 20 || >=22" } }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/global-directory": { "version": "4.0.1", "license": "MIT", @@ -10022,7 +10174,6 @@ }, "node_modules/globalthis": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -10094,6 +10245,12 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, "node_modules/gzip-size": { "version": "7.0.0", "license": "MIT", @@ -10150,7 +10307,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -11083,6 +11239,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "license": "MIT", @@ -11622,6 +11784,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "dev": true, @@ -11708,6 +11876,18 @@ "source-map-js": "^1.2.1" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "license": "MIT", @@ -12552,7 +12732,6 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12710,6 +12889,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onnxruntime-common": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", + "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", + "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.5.16", + "global-agent": "^3.0.0", + "onnxruntime-common": "1.24.3" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.25.0-dev.20260327-722743c0e2", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.25.0-dev.20260327-722743c0e2.tgz", + "integrity": "sha512-8PXdZy4Ekhg10CLg+cFFt39b4tFDGMRJB6lGjnQL6eA+2boUQYDymZ0gtxiS+H6oIWoCjQp/ziyirvFbaFKfiw==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.24.0-dev.20251116-b39e144322", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz", + "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", + "license": "MIT" + }, "node_modules/open": { "version": "11.0.0", "license": "MIT", @@ -13079,6 +13301,12 @@ "pathe": "^2.0.3" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "dev": true, @@ -13596,6 +13824,30 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "license": "MIT", @@ -13950,6 +14202,23 @@ "node": ">=0.10.0" } }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/rollup": { "version": "4.59.0", "license": "MIT", @@ -14233,6 +14502,12 @@ "semver": "bin/semver.js" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, "node_modules/send": { "version": "1.2.1", "license": "MIT", @@ -14257,6 +14532,33 @@ "url": "https://opencollective.com/express" } }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "license": "BSD-3-Clause", @@ -14402,7 +14704,6 @@ "version": "0.34.5", "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -14444,7 +14745,6 @@ "node_modules/sharp/node_modules/semver": { "version": "7.7.4", "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -14627,6 +14927,12 @@ "source-map": "^0.6.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/srvx": { "version": "0.11.9", "license": "MIT", diff --git a/package.json b/package.json index 8993aed..d84fc23 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@base-ui/react": "^1.2.0", "@gradio/client": "^2.1.0", + "@huggingface/transformers": "^4.0.1", "@supabase/supabase-js": "^2.99.1", "@vercel/analytics": "^2.0.0", "@vercel/speed-insights": "^2.0.0", diff --git a/tsconfig.json b/tsconfig.json index 3a13f90..fdda534 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,5 @@ ".next/dev/types/**/*.ts", "**/*.mts" ], - "exclude": ["node_modules"] + "exclude": ["node_modules", "relay"] } From 393a7df9cba00638911a8e506acea15197b82aec Mon Sep 17 00:00:00 2001 From: Arya Teja Rudraraju Date: Sun, 5 Apr 2026 02:15:44 -0700 Subject: [PATCH 4/7] Build WebContainer sandbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step complete. Here's what was done: - **Installed** `@webcontainer/api` - **Created `lib/webcontainer-sandbox.ts`** — singleton WebContainer with `bootSandbox()`, `execCommand()`, `teardownSandbox()`, and `isSandboxReady()`. Mounts a `/workspace` directory and spawns `sh -c` for command execution. - **Created `hooks/use-webcontainer.ts`** — React hook with lazy-loading via dynamic import, auto-boot on first execute, execution state tracking, and command history accumulation. Returns `ExecutionResult` compatible with existing UI. - **Made `auditId` optional** in `types/sandbox.d.ts` — WebContainer exec has no server-side audit trail. - **Updated `components/execution-output.tsx`** — conditionally renders the audit ID line only when present. All checks pass: zero type errors, zero lint errors, clean production build. --- .zenflow/tasks/new-task-7cad/plan.md | 13 ++-- components/execution-output.tsx | 8 +- hooks/use-webcontainer.ts | 107 +++++++++++++++++++++++++++ lib/webcontainer-sandbox.ts | 67 +++++++++++++++++ package-lock.json | 7 ++ package.json | 1 + types/sandbox.d.ts | 2 +- 7 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 hooks/use-webcontainer.ts create mode 100644 lib/webcontainer-sandbox.ts diff --git a/.zenflow/tasks/new-task-7cad/plan.md b/.zenflow/tasks/new-task-7cad/plan.md index a4cf74e..c3335ef 100644 --- a/.zenflow/tasks/new-task-7cad/plan.md +++ b/.zenflow/tasks/new-task-7cad/plan.md @@ -39,15 +39,16 @@ Create `lib/browser-engine.ts` (Transformers.js pipeline for in-browser WebGPU i --- -### [ ] Step: Build WebContainer sandbox +### [x] Step: Build WebContainer sandbox + Create the in-browser sandbox using WebContainers to replace the Docker relay for demo use. -- Install `@webcontainer/api` -- Create `lib/webcontainer-sandbox.ts` (boot, exec, teardown singleton) -- Create `hooks/use-webcontainer.ts` (React hook: boot-on-first-run, exec, history tracking) -- Update `types/sandbox.d.ts` to make `auditId` optional -- Verify: `npx tsc --noEmit && npm run lint && npm run build` +- [x] Install `@webcontainer/api` +- [x] Create `lib/webcontainer-sandbox.ts` (boot, exec, teardown singleton) +- [x] Create `hooks/use-webcontainer.ts` (React hook: boot-on-first-run, exec, history tracking) +- [x] Update `types/sandbox.d.ts` to make `auditId` optional +- [x] Verify: `npx tsc --noEmit && npm run lint && npm run build` --- diff --git a/components/execution-output.tsx b/components/execution-output.tsx index d245776..22a74c3 100644 --- a/components/execution-output.tsx +++ b/components/execution-output.tsx @@ -69,9 +69,11 @@ export function ExecutionOutput({ result, command }: ExecutionOutputProps) { {/* Audit trail reference */} -

- audit: {result.auditId} -

+ {result.auditId && ( +

+ audit: {result.auditId} +

+ )} ); } diff --git a/hooks/use-webcontainer.ts b/hooks/use-webcontainer.ts new file mode 100644 index 0000000..3783403 --- /dev/null +++ b/hooks/use-webcontainer.ts @@ -0,0 +1,107 @@ +"use client"; + +import { useCallback, useRef, useState } from "react"; +import type { ExecutionResult } from "@/types/sandbox"; + +interface WebContainerState { + isReady: boolean; + isBooting: boolean; + isExecuting: boolean; + output: ExecutionResult | null; + error: string | null; + history: Array<{ + command: string; + stdout: string; + exitCode: number; + timestamp: number; + }>; +} + +export function useWebContainer() { + const [state, setState] = useState({ + isReady: false, + isBooting: false, + isExecuting: false, + output: null, + error: null, + history: [], + }); + + const sandboxRef = useRef( + null, + ); + + const getSandbox = useCallback(async () => { + if (!sandboxRef.current) { + sandboxRef.current = await import("@/lib/webcontainer-sandbox"); + } + return sandboxRef.current; + }, []); + + const boot = useCallback(async () => { + setState((s) => ({ ...s, isBooting: true, error: null })); + try { + const sb = await getSandbox(); + await sb.bootSandbox(); + setState((s) => ({ ...s, isReady: true, isBooting: false })); + } catch (err) { + setState((s) => ({ + ...s, + isBooting: false, + error: + err instanceof Error ? err.message : "Failed to boot sandbox", + })); + } + }, [getSandbox]); + + const execute = useCallback( + async (command: string) => { + setState((s) => ({ ...s, isExecuting: true, output: null, error: null })); + try { + const sb = await getSandbox(); + if (!sb.isSandboxReady()) { + setState((s) => ({ ...s, isBooting: true })); + await sb.bootSandbox(); + setState((s) => ({ ...s, isReady: true, isBooting: false })); + } + + const result = await sb.execCommand(command); + + const executionResult: ExecutionResult = { + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + durationMs: result.durationMs, + }; + + setState((s) => ({ + ...s, + isExecuting: false, + output: executionResult, + history: [ + ...s.history, + { + command, + stdout: result.stdout, + exitCode: result.exitCode, + timestamp: Date.now(), + }, + ], + })); + } catch (err) { + setState((s) => ({ + ...s, + isExecuting: false, + error: err instanceof Error ? err.message : "Execution failed", + })); + } + }, + [getSandbox], + ); + + const clearOutput = useCallback(() => { + setState((s) => ({ ...s, output: null, error: null })); + }, []); + + return { ...state, boot, execute, clearOutput }; +} diff --git a/lib/webcontainer-sandbox.ts b/lib/webcontainer-sandbox.ts new file mode 100644 index 0000000..adfa0ce --- /dev/null +++ b/lib/webcontainer-sandbox.ts @@ -0,0 +1,67 @@ +"use client"; + +import { WebContainer } from "@webcontainer/api"; + +let container: WebContainer | null = null; +let bootPromise: Promise | null = null; + +export async function bootSandbox(): Promise { + if (container) return; + if (bootPromise) { + await bootPromise; + return; + } + + bootPromise = WebContainer.boot(); + try { + container = await bootPromise; + await container.mount({ + workspace: { directory: {} }, + }); + } catch (err) { + bootPromise = null; + throw err; + } +} + +export function isSandboxReady(): boolean { + return container !== null; +} + +export interface ExecResult { + stdout: string; + stderr: string; + exitCode: number; + durationMs: number; +} + +export async function execCommand(command: string): Promise { + if (!container) throw new Error("Sandbox not booted"); + + const start = performance.now(); + const process = await container.spawn("sh", ["-c", command], { + cwd: "/workspace", + }); + + let stdout = ""; + process.output.pipeTo( + new WritableStream({ + write(chunk) { + stdout += chunk; + }, + }), + ); + + const exitCode = await process.exit; + const durationMs = Math.round(performance.now() - start); + + return { stdout: stdout.trim(), stderr: "", exitCode, durationMs }; +} + +export async function teardownSandbox(): Promise { + if (container) { + container.teardown(); + container = null; + bootPromise = null; + } +} diff --git a/package-lock.json b/package-lock.json index 1d0d060..f4d26b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@supabase/supabase-js": "^2.99.1", "@vercel/analytics": "^2.0.0", "@vercel/speed-insights": "^2.0.0", + "@webcontainer/api": "^1.6.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.7.0", @@ -6635,6 +6636,12 @@ "license": "MIT", "peer": true }, + "node_modules/@webcontainer/api": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@webcontainer/api/-/api-1.6.1.tgz", + "integrity": "sha512-2RS2KiIw32BY1Icf6M1DvqSmcon9XICZCDgS29QJb2NmF12ZY2V5Ia+949hMKB3Wno+P/Y8W+sPP59PZeXSELg==", + "license": "MIT" + }, "node_modules/abbrev": { "version": "3.0.1", "license": "ISC", diff --git a/package.json b/package.json index d84fc23..cc85b5c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@supabase/supabase-js": "^2.99.1", "@vercel/analytics": "^2.0.0", "@vercel/speed-insights": "^2.0.0", + "@webcontainer/api": "^1.6.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.7.0", diff --git a/types/sandbox.d.ts b/types/sandbox.d.ts index 8c4fd74..d02636a 100644 --- a/types/sandbox.d.ts +++ b/types/sandbox.d.ts @@ -10,7 +10,7 @@ export interface ExecutionResult { stderr: string; exitCode: number; durationMs: number; - auditId: string; + auditId?: string; } export interface ExecRequest { From dab069f8b69df3e8474a5af3ebbf2df742de19d0 Mon Sep 17 00:00:00 2001 From: Arya Teja Rudraraju Date: Sun, 5 Apr 2026 02:25:28 -0700 Subject: [PATCH 5/7] =?UTF-8?q?Wire=20up=20UI=20=E2=80=94=20mode=20selecto?= =?UTF-8?q?r,=20browser=20translate,=20sandbox=20execution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step complete. Here's what was implemented: **`hooks/use-translate.ts`** - Added `InferenceMode` type (`cloud` | `browser` | `auto`), `browserStatus` tracking, and mode-aware translation. Cloud mode calls `/api/translate`, Browser mode uses Transformers.js `generate()` directly, Auto mode uses browser if the model is ready, otherwise falls back to cloud. Browser engine status subscription is lazy (only when mode is non-cloud). **`components/shell-session.tsx`** - Added a segmented mode selector (Cloud/Browser/Auto) with lucide icons next to the input label. Replaced `useSandbox()` with `useWebContainer()` for in-browser command execution. Added browser engine download progress bar (shown during model download/initialization). Added sandbox boot/execute status indicator. Sandbox is enabled by default (`!== "false"` instead of `=== "true"`). WebContainer command history is passed to `ExecutionOutput`. **`components/execution-output.tsx`** - Added `history` prop that renders previous sandbox commands above the current output in a dimmed terminal style with a separator. Added scrollable container (`max-h-[400px]`) for long output. `auditId` remains optional. All checks pass: `tsc --noEmit`, `lint`, and `build`. --- .zenflow/tasks/new-task-7cad/plan.md | 13 +-- components/execution-output.tsx | 36 ++++++- components/shell-session.tsx | 145 ++++++++++++++++++++++----- hooks/use-translate.ts | 108 ++++++++++++++------ 4 files changed, 235 insertions(+), 67 deletions(-) diff --git a/.zenflow/tasks/new-task-7cad/plan.md b/.zenflow/tasks/new-task-7cad/plan.md index c3335ef..fb8327d 100644 --- a/.zenflow/tasks/new-task-7cad/plan.md +++ b/.zenflow/tasks/new-task-7cad/plan.md @@ -52,15 +52,16 @@ Create the in-browser sandbox using WebContainers to replace the Docker relay fo --- -### [ ] Step: Wire up UI — mode selector, browser translate, sandbox execution +### [x] Step: Wire up UI — mode selector, browser translate, sandbox execution + Integrate browser inference and WebContainer sandbox into the main UI. -- Add inference mode selector (Cloud / Browser / Auto) to `components/shell-session.tsx` -- Update `hooks/use-translate.ts` to accept mode parameter and call browser engine directly in browser mode -- Replace `useSandbox()` with `useWebContainer()` in shell-session for WebContainer execution -- Update `components/execution-output.tsx` for optional auditId and command history display -- Verify: `npx tsc --noEmit && npm run lint && npm run build` +- [x] Add inference mode selector (Cloud / Browser / Auto) to `components/shell-session.tsx` +- [x] Update `hooks/use-translate.ts` to accept mode parameter and call browser engine directly in browser mode +- [x] Replace `useSandbox()` with `useWebContainer()` in shell-session for WebContainer execution +- [x] Update `components/execution-output.tsx` for optional auditId and command history display +- [x] Verify: `npx tsc --noEmit && npm run lint && npm run build` --- diff --git a/components/execution-output.tsx b/components/execution-output.tsx index 22a74c3..80fcfe5 100644 --- a/components/execution-output.tsx +++ b/components/execution-output.tsx @@ -2,12 +2,24 @@ import type { ExecutionResult } from "@/types/sandbox"; +interface HistoryEntry { + command: string; + stdout: string; + exitCode: number; + timestamp: number; +} + interface ExecutionOutputProps { result: ExecutionResult; command: string; + history?: HistoryEntry[]; } -export function ExecutionOutput({ result, command }: ExecutionOutputProps) { +export function ExecutionOutput({ + result, + command, + history, +}: ExecutionOutputProps) { const hasStdout = result.stdout.trim().length > 0; const hasStderr = result.stderr.trim().length > 0; const isSuccess = result.exitCode === 0; @@ -39,8 +51,26 @@ export function ExecutionOutput({ result, command }: ExecutionOutputProps) { {/* Output */} -
- {/* Command echo */} +
+ {/* Previous commands (history) */} + {history && history.length > 0 && ( +
+ {history.map((entry) => ( +
+
+ $ {entry.command} +
+ {entry.stdout.trim() && ( +
+                      {entry.stdout}
+                    
+ )} +
+ ))} +
+ )} + + {/* Current command echo */}
$ {command}
diff --git a/components/shell-session.tsx b/components/shell-session.tsx index e79352b..7095990 100644 --- a/components/shell-session.tsx +++ b/components/shell-session.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useState } from "react"; -import { Loader2, Terminal } from "lucide-react"; +import { Cloud, Loader2, Monitor, Terminal, Zap } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; @@ -10,10 +10,16 @@ import { CommandOutput } from "@/components/command-output"; import { ExecutionOutput } from "@/components/execution-output"; import { ExamplePrompts } from "@/components/example-prompts"; import { AILoader } from "@/components/ai-loader"; -import { useTranslate } from "@/hooks/use-translate"; -import { useSandbox } from "@/hooks/use-sandbox"; +import { useTranslate, type InferenceMode } from "@/hooks/use-translate"; +import { useWebContainer } from "@/hooks/use-webcontainer"; -const SANDBOX_ENABLED = process.env.NEXT_PUBLIC_SANDBOX_ENABLED === "true"; +const SANDBOX_ENABLED = process.env.NEXT_PUBLIC_SANDBOX_ENABLED !== "false"; + +const MODES: { value: InferenceMode; label: string; icon: typeof Cloud }[] = [ + { value: "cloud", label: "Cloud", icon: Cloud }, + { value: "browser", label: "Browser", icon: Monitor }, + { value: "auto", label: "Auto", icon: Zap }, +]; interface HistoryEntry { query: string; @@ -26,8 +32,10 @@ export function ShellSession() { const [input, setInput] = useState(""); const [lastQuery, setLastQuery] = useState(""); const [history, setHistory] = useState([]); - const { result, isLoading, error, translate, reset } = useTranslate(); - const sandbox = useSandbox(); + const [mode, setMode] = useState("cloud"); + const { result, isLoading, error, browserStatus, translate, reset } = + useTranslate(mode); + const sandbox = useWebContainer(); const handleSubmit = useCallback(() => { const trimmed = input.trim(); @@ -43,7 +51,7 @@ export function ShellSession() { setLastQuery(text.trim()); translate(text); }, - [translate] + [translate], ); const handleExampleSelect = useCallback( @@ -52,15 +60,22 @@ export function ShellSession() { setLastQuery(example.trim()); translate(example); }, - [translate] + [translate], ); const handleClear = useCallback(() => { if (result && lastQuery) { - setHistory((prev) => [ - { query: lastQuery, command: result.command, meta: result.meta, timestamp: Date.now() }, - ...prev, - ].slice(0, 20)); + setHistory((prev) => + [ + { + query: lastQuery, + command: result.command, + meta: result.meta, + timestamp: Date.now(), + }, + ...prev, + ].slice(0, 20), + ); } setInput(""); setLastQuery(""); @@ -74,18 +89,46 @@ export function ShellSession() { } }; + const showBrowserProgress = + isLoading && + mode !== "cloud" && + (browserStatus.stage === "downloading" || + browserStatus.stage === "loading"); + return (
{/* Input card */}
- +
+ + + {/* Mode selector */} +
+ {MODES.map(({ value, label, icon: Icon }) => ( + + ))} +
+
+