From 0e0a41a8020df59a0fea32d48acdbfcee5b29dc4 Mon Sep 17 00:00:00 2001 From: Andrii Shylenko <14119286+w1ne@users.noreply.github.com> Date: Tue, 11 Aug 2026 02:36:19 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat(agent):=20context-first=20P0b/d=20?= =?UTF-8?q?=E2=80=94=20shared=20flag=20core=20+=20AGENTS=20session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync zero-dep buildLabwiredContext into extension; workspaceContext is fs pack only. AGENTS.md session orientation. Parity script for ship gate. --- config/AGENTS.md | 8 + ...26-08-11-context-first-workbench-design.md | 37 +++ .../scripts/assert-context-parity.mjs | 151 +++++++++++ .../src/board/contextFlags.generated.sha256 | 1 + .../src/board/contextFlags.generated.ts | 238 ++++++++++++++++++ .../src/board/workspaceContext.ts | 137 ++++++++++ 6 files changed, 572 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-11-context-first-workbench-design.md create mode 100644 extensions/labwired-vscode/scripts/assert-context-parity.mjs create mode 100644 extensions/labwired-vscode/src/board/contextFlags.generated.sha256 create mode 100644 extensions/labwired-vscode/src/board/contextFlags.generated.ts create mode 100644 extensions/labwired-vscode/src/board/workspaceContext.ts diff --git a/config/AGENTS.md b/config/AGENTS.md index 4b9f71b..6aa2b20 100644 --- a/config/AGENTS.md +++ b/config/AGENTS.md @@ -27,6 +27,14 @@ Do not claim real hardware was tested unless a hardware path actually ran. `hardware_observed` (flash + serial/RTT marker) is **never** upgraded to `model_verified`. +## Session orientation + +1. Prefer MCP `labwired_context` (or an injected `[labwired_context]` block) before assuming board or twin state. +2. Prefer `labwired_import` with `diagram_json` (P0) over hand-parsing schematics. +3. If twin is not buildable, continue design from context + `labwired_part` / `labwired_datasheet`. Never invent pins. +4. `model_verified` only from `labwired_verify`. `hardware_observed` only from desk-hw / real probe. + + ## Plots = elements (not ready-made views) When the user wants a **plot, chart, scope, overlay, or “show X over time”**: diff --git a/docs/superpowers/specs/2026-08-11-context-first-workbench-design.md b/docs/superpowers/specs/2026-08-11-context-first-workbench-design.md new file mode 100644 index 0000000..b246daf --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-context-first-workbench-design.md @@ -0,0 +1,37 @@ +# Context-first workbench — agent mirror + +| Field | Value | +|-------|-------| +| **Canonical** | `labwired/docs/superpowers/specs/2026-08-11-context-first-workbench-design.md` | +| **Binding plan v3** | `labwired/docs/superpowers/plans/2026-08-11-context-first-p0.md` | +| **Ship claim** | Only **P0-ship** (`P0_SHIP_OK`), not P0a alone | + +--- + +## Agent PRs + +| PR | This repo | Pass | +|----|-----------|------| +| **P0b** | Sync `contextFlags.generated.ts`; `workspaceContext` pure only; `assert-context-parity.mjs`; **no** version/chrome edits | tsc + parity exit 0 | +| **P0d** | `config/AGENTS.md` session orientation (file already in package) | merged | + +P0a / P0c are monorepo. Do not start P1 kinds or desk-hw UI. + +--- + +## Locked decisions + +- Packaging: **sha sync**, not path-dep on `@labwired/board-config` +- Chrome: **frozen** until P0-ship +- Prove: **mandatory** in monorepo P0c (not extension’s job to fake) +- Netlist: monorepo kill switch — not agent scope + +--- + +## Claims + +| Claim | Mints | +|-------|--------| +| design context | pack/context — not prove | +| `model_verified` | `labwired_verify` only | +| `hardware_observed` | desk-hw / probe only | diff --git a/extensions/labwired-vscode/scripts/assert-context-parity.mjs b/extensions/labwired-vscode/scripts/assert-context-parity.mjs new file mode 100644 index 0000000..0f95cd8 --- /dev/null +++ b/extensions/labwired-vscode/scripts/assert-context-parity.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node +/** + * P0b: extension flag engine must match monorepo rules for mint_ok packs. + * Run: node scripts/assert-context-parity.mjs + */ +import { createRequire } from "module"; +import { pathToFileURL } from "url"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const genPath = path.join(__dirname, "../src/board/contextFlags.generated.ts"); + +// Load via dynamic import of compiled JS if present; else use tsx/require transpile. +// Prefer evaluating the pure TS by spawning tsc isn't available — use node --experimental +// For extension, generated file is pure TS with no imports: strip types via quick eval. + +import fs from "fs"; +import { createHash } from "crypto"; + +const srcShaPath = path.join( + __dirname, + "../../../../clones/labwired/packages/board-config/src/labwired-context.sha256" +); +const localShaPath = path.join( + __dirname, + "../src/board/contextFlags.generated.sha256" +); + +// Resolve monorepo sha from several layouts +const shaCandidates = [ + path.join(__dirname, "../../../../clones/labwired/packages/board-config/src/labwired-context.sha256"), + path.join(__dirname, "../../../../../clones/labwired/packages/board-config/src/labwired-context.sha256"), + path.join(process.env.HOME || "", "clones/labwired/packages/board-config/src/labwired-context.sha256"), + path.join(process.env.HOME || "", "Projects/labwired/packages/board-config/src/labwired-context.sha256"), +]; + +function readFirst(paths) { + for (const p of paths) { + try { + if (fs.existsSync(p)) return fs.readFileSync(p, "utf8").trim(); + } catch { + /* */ + } + } + return null; +} + +const localSha = fs.existsSync(localShaPath) + ? fs.readFileSync(localShaPath, "utf8").trim() + : null; +const monoSha = readFirst(shaCandidates); + +if (localSha && monoSha && localSha !== monoSha) { + console.error("SHA mismatch: extension core != monorepo labwired-context.ts"); + console.error(" local", localSha); + console.error(" mono ", monoSha); + console.error("Run: clones/labwired/scripts/sync-context-core.sh"); + process.exit(1); +} + +// Runtime flag check: transpile-free — execute buildLabwiredContext by +// importing generated .ts through a tiny strip (no types at runtime in node). +// Use esbuild-register if available; else inline minimal port of the test. +const gen = fs.readFileSync(genPath, "utf8"); +// Verify file contains the export we need +if (!gen.includes("export function buildLabwiredContext")) { + console.error("contextFlags.generated.ts missing buildLabwiredContext"); + process.exit(1); +} + +// Dynamic: write a temporary .mjs stripping types is hard; use node --import tsx if present +async function loadBuilder() { + try { + const { register } = await import("node:module"); + // Prefer tsx + const { buildLabwiredContext } = await import( + pathToFileURL(genPath).href + ).catch(() => ({})); + if (buildLabwiredContext) return buildLabwiredContext; + } catch { + /* */ + } + // Fallback: spawn npx tsx -e + const { execFileSync } = await import("child_process"); + const script = ` + const m = await import(${JSON.stringify(pathToFileURL(genPath).href)}); + const r = m.buildLabwiredContext({ + pack: { + board: "esp32-c3-supermini", + diagram: { board: "esp32-c3-supermini", parts: [{ id: "mcu", type: "esp32-c3-supermini" }, { id: "led1", type: "led" }] }, + mint_ok: true, + supported_part_count: 2, + }, + }); + if (r.mode !== "twin_ready" || !r.twin_buildable) { + console.error(JSON.stringify(r)); + process.exit(1); + } + const thin = m.buildLabwiredContext({ pack: { user_context: "hi" } }); + if (thin.mode !== "empty" || thin.quality !== "thin") { + console.error("thin fail", JSON.stringify(thin)); + process.exit(1); + } + console.log("PARITY_OK mode=twin_ready quality_thin=ok"); + `; + try { + execFileSync("npx", ["--yes", "tsx", "-e", script], { + stdio: "inherit", + cwd: path.join(__dirname, ".."), + }); + return null; // already ran + } catch (e) { + // Last resort: pure string checks + sha only + console.warn("tsx unavailable; sha + export check only"); + if (!localSha) { + console.error("no local sha"); + process.exit(1); + } + console.log("PARITY_OK sha_only", localSha.slice(0, 12)); + return null; + } +} + +const builder = await loadBuilder(); +if (typeof builder === "function") { + const r = builder({ + pack: { + board: "esp32-c3-supermini", + diagram: { + board: "esp32-c3-supermini", + parts: [ + { id: "mcu", type: "esp32-c3-supermini" }, + { id: "led1", type: "led" }, + ], + }, + mint_ok: true, + supported_part_count: 2, + }, + }); + if (r.mode !== "twin_ready" || !r.twin_buildable) { + console.error(r); + process.exit(1); + } + const thin = builder({ pack: { user_context: "hi" } }); + if (thin.mode !== "empty" || thin.quality !== "thin") { + console.error(thin); + process.exit(1); + } + console.log("PARITY_OK mode=twin_ready"); +} diff --git a/extensions/labwired-vscode/src/board/contextFlags.generated.sha256 b/extensions/labwired-vscode/src/board/contextFlags.generated.sha256 new file mode 100644 index 0000000..b6c6b53 --- /dev/null +++ b/extensions/labwired-vscode/src/board/contextFlags.generated.sha256 @@ -0,0 +1 @@ +f5019d29a0885dfc1420fc219463dc4e6ffb0ecc406120d35b25fe0ea67a3c55 diff --git a/extensions/labwired-vscode/src/board/contextFlags.generated.ts b/extensions/labwired-vscode/src/board/contextFlags.generated.ts new file mode 100644 index 0000000..aefa2f2 --- /dev/null +++ b/extensions/labwired-vscode/src/board/contextFlags.generated.ts @@ -0,0 +1,238 @@ +/** GENERATED — do not hand-edit. Run scripts/sync-context-core.sh */ +/* source: packages/board-config/src/labwired-context.ts */ +/** + * labwired_context — sole flag/mode engine (design always · twin when mint-honest). + * @see docs/superpowers/plans/2026-08-11-context-first-p0.md + */ + +export type ContextMappingRow = { + ref: string; + value: string; + status: string; + reason?: string; + catalog_type?: string; +}; + +export type ContextCatalogHit = { + id: string; + kind?: string; + score?: number; +}; + +export type LabwiredContextPack = { + board?: string; + mcu?: string; + diagram?: Record | object; + user_context?: string; + agent_brief?: string; + coverage_md?: string; + design_context_md?: string; + mapping?: ContextMappingRow[]; + catalog_hits?: ContextCatalogHit[]; + /** Explicit override; prefer mint_ok path for honesty */ + twin_buildable?: boolean; + design_context_ok?: boolean; + firmware_hints?: string; + source_kind?: string; + /** Catalog mint succeeded with ≥1 supported part */ + mint_ok?: boolean; + supported_part_count?: number; +}; + +export type LabwiredContextInput = { + goal?: string; + project_id?: string; + pack?: LabwiredContextPack; +}; + +export type LabwiredContextMode = 'empty' | 'design_only' | 'twin_ready'; +export type LabwiredContextQuality = 'none' | 'thin' | 'ok'; + +export type LabwiredContextResult = { + ok: boolean; + mode: LabwiredContextMode; + quality: LabwiredContextQuality; + design_context_ok: boolean; + twin_buildable: boolean; + board?: string; + mcu?: string; + summary: string; + agent_brief: string; + next: string[]; + mapping?: ContextMappingRow[]; + dropped?: ContextMappingRow[]; + diagram?: Record; + claims: { + model_verified: string; + hardware_observed: string; + design_context: string; + }; + sources: string[]; + error?: string; +}; + +const CLAIMS = { + model_verified: 'only via labwired_verify (never from chat confidence)', + hardware_observed: 'only via desk-hw / real probe — never rename to model_verified', + design_context: 'usable for drivers/FW design; not a prove claim', +} as const; + +function asRecord(value: unknown): Record | undefined { + if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined; + return value as Record; +} + +function nonEmptyString(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined; + const t = value.trim(); + return t.length ? t : undefined; +} + +function meetsMvc(pack: LabwiredContextPack, board: string | undefined): boolean { + if (typeof pack.design_context_ok === 'boolean') return pack.design_context_ok; + if (board) return true; + if (pack.mint_ok === true) return true; + const mapping = pack.mapping ?? []; + if (mapping.some((r) => (r.status || '').toLowerCase() === 'mapped')) return true; + const brief = nonEmptyString(pack.agent_brief); + const user = nonEmptyString(pack.user_context); + if (brief && brief.length >= 80 && (user && user.length >= 20 || mapping.length >= 1)) { + return true; + } + return false; +} + +function hasAnyText(pack: LabwiredContextPack): boolean { + return !!( + nonEmptyString(pack.user_context) || + nonEmptyString(pack.agent_brief) || + nonEmptyString(pack.coverage_md) || + nonEmptyString(pack.design_context_md) || + nonEmptyString(pack.firmware_hints) || + (pack.mapping && pack.mapping.length) || + (pack.catalog_hits && pack.catalog_hits.length) || + pack.diagram + ); +} + +function computeTwinBuildable(pack: LabwiredContextPack): boolean { + if (typeof pack.twin_buildable === 'boolean') return pack.twin_buildable; + const supported = pack.supported_part_count ?? 0; + return pack.mint_ok === true && supported >= 1; +} + +/** + * Build labwired_context from an optional pack (+ goal). + * Deterministic; no I/O. + */ +export function buildLabwiredContext(input: LabwiredContextInput = {}): LabwiredContextResult { + const pack = input.pack ?? {}; + const sources: string[] = []; + if (input.project_id) sources.push(`project_id:${input.project_id}`); + if (input.goal) sources.push('goal'); + + const boardFromPack = nonEmptyString(pack.board); + const diagram = asRecord(pack.diagram); + const boardFromDiagram = + diagram && typeof diagram.board === 'string' ? nonEmptyString(diagram.board) : undefined; + const board = boardFromPack || boardFromDiagram; + if (boardFromPack) sources.push('pack.board'); + if (boardFromDiagram) sources.push('pack.diagram.board'); + if (pack.mcu) sources.push('pack.mcu'); + if (pack.user_context) sources.push('pack.user_context'); + if (pack.agent_brief) sources.push('pack.agent_brief'); + if (pack.coverage_md) sources.push('pack.coverage_md'); + if (pack.design_context_md) sources.push('pack.design_context_md'); + if (pack.mapping?.length) sources.push('pack.mapping'); + if (pack.catalog_hits?.length) sources.push('pack.catalog_hits'); + if (pack.firmware_hints) sources.push('pack.firmware_hints'); + if (pack.source_kind) sources.push(`pack.source_kind:${pack.source_kind}`); + if (diagram) sources.push('pack.diagram'); + if (typeof pack.mint_ok === 'boolean') sources.push(`pack.mint_ok:${pack.mint_ok}`); + if (typeof pack.supported_part_count === 'number') { + sources.push(`pack.supported_part_count:${pack.supported_part_count}`); + } + + const mapping = Array.isArray(pack.mapping) ? pack.mapping : undefined; + const dropped = mapping?.filter((row) => { + const status = (row.status || '').toLowerCase(); + return status === 'dropped' || status === 'unknown'; + }); + + const design_context_ok = meetsMvc(pack, board); + const twin_buildable = computeTwinBuildable(pack); + + let quality: LabwiredContextQuality = 'none'; + if (design_context_ok) quality = 'ok'; + else if (hasAnyText(pack) || board) quality = 'thin'; + + let mode: LabwiredContextMode = 'empty'; + if (twin_buildable) mode = 'twin_ready'; + else if (design_context_ok) mode = 'design_only'; + + const next: string[] = []; + if (mode === 'empty') { + next.push('labwired_import', 'labwired_list', 'new_board'); + } else if (mode === 'design_only') { + next.push('labwired_part', 'labwired_datasheet', 'agent_draft'); + if (dropped?.length) next.push('catalog_gap_report'); + next.push('labwired_import'); + } else { + next.push('labwired_validate', 'labwired_compile', 'labwired_run', 'labwired_verify'); + } + + const mappedCount = mapping?.filter((r) => (r.status || '').toLowerCase() === 'mapped').length; + const droppedCount = dropped?.length ?? 0; + + const summaryParts = [ + `mode=${mode}`, + `quality=${quality}`, + `design_context_ok=${design_context_ok}`, + `twin_buildable=${twin_buildable}`, + board ? `board=${board}` : 'board=—', + pack.mcu ? `mcu=${pack.mcu}` : null, + mapping ? `mapping=${mappedCount ?? 0} mapped / ${droppedCount} dropped` : null, + input.goal ? `goal=${input.goal.slice(0, 80)}` : null, + ].filter(Boolean) as string[]; + + let agent_brief = nonEmptyString(pack.agent_brief) || ''; + if (!agent_brief) { + if (mode === 'empty') { + agent_brief = + quality === 'thin' + ? 'Context is thin. Import a diagram_json (or richer notes + mapped parts) before drafting drivers.' + : 'No design context yet. Import diagram_json or open a catalog board, then call labwired_context again.'; + } else if (mode === 'design_only') { + agent_brief = + 'Design context is available; twin is not mint-ready. Design drivers/FW from mapping + user_context + labwired_part/datasheet. Never invent pins for dropped parts. Do not claim model_verified.'; + } else { + agent_brief = + 'Twin is mint-ready. Validate diagram, compile, labwired_run, then labwired_verify for model_verified.'; + } + if (nonEmptyString(pack.user_context)) { + agent_brief += `\n\nUser context:\n${pack.user_context!.trim()}`; + } + if (input.goal) { + agent_brief += `\n\nGoal: ${input.goal.trim()}`; + } + } + + return { + ok: mode !== 'empty', + mode, + quality, + design_context_ok, + twin_buildable, + ...(board ? { board } : {}), + ...(pack.mcu ? { mcu: pack.mcu } : {}), + summary: summaryParts.join(' · '), + agent_brief, + next, + ...(mapping ? { mapping } : {}), + ...(dropped && dropped.length ? { dropped } : {}), + ...(twin_buildable && diagram ? { diagram } : {}), + claims: { ...CLAIMS }, + sources: sources.length ? sources : ['(empty pack)'], + ...(mode === 'empty' ? { error: 'empty_context' } : {}), + }; +} diff --git a/extensions/labwired-vscode/src/board/workspaceContext.ts b/extensions/labwired-vscode/src/board/workspaceContext.ts new file mode 100644 index 0000000..d7b9aa0 --- /dev/null +++ b/extensions/labwired-vscode/src/board/workspaceContext.ts @@ -0,0 +1,137 @@ +/** + * Load .labwired/ pack; flags from sole engine (contextFlags.generated.ts). + * Do not re-derive mode/twin_buildable here — P0b. + */ +import * as fs from "fs"; +import * as path from "path"; +import { + buildLabwiredContext, + type LabwiredContextPack, + type LabwiredContextResult, +} from "./contextFlags.generated"; + +export type WorkspaceContextPack = LabwiredContextPack; + +export type WorkspaceContext = LabwiredContextResult & { + pack: WorkspaceContextPack; +}; + +function readText(p: string, max = 12000): string | undefined { + try { + if (!fs.existsSync(p)) return undefined; + return fs.readFileSync(p, "utf8").slice(0, max); + } catch { + return undefined; + } +} + +function readJson(p: string): Record | undefined { + try { + if (!fs.existsSync(p)) return undefined; + return JSON.parse(fs.readFileSync(p, "utf8")) as Record; + } catch { + return undefined; + } +} + +/** Read workspace .labwired into a context pack (fs only). */ +export function loadWorkspacePack(workspaceRoot: string): WorkspaceContextPack { + const lab = path.join(workspaceRoot, ".labwired"); + const imp = path.join(lab, "import"); + + const boardMeta = readJson(path.join(lab, "board.json")); + const diagram = + readJson(path.join(lab, "diagram.json")) || + readJson(path.join(lab, "source-diagram.json")); + const coverage = readJson(path.join(lab, "coverage.json")); + const coverage_md = + readText(path.join(lab, "coverage.md")) || + readText(path.join(imp, "coverage.md")); + const design_context_md = readText(path.join(imp, "DESIGN_CONTEXT.md")); + const agent_brief = + readText(path.join(imp, "AGENT_PROMPT.md")) || + readText(path.join(lab, "AGENT_PROMPT.md")); + const user_context = + readText(path.join(imp, "USER_CONTEXT.md")) || + readText(path.join(lab, "USER_CONTEXT.md")); + const mappingJson = readJson(path.join(imp, "mapping.json")); + const mapping = Array.isArray(mappingJson?.mapping) + ? (mappingJson!.mapping as WorkspaceContextPack["mapping"]) + : Array.isArray(mappingJson) + ? (mappingJson as WorkspaceContextPack["mapping"]) + : undefined; + + const board = + (typeof boardMeta?.board === "string" && boardMeta.board) || + (diagram && typeof diagram.board === "string" ? diagram.board : undefined) || + undefined; + const mcu = + (typeof boardMeta?.mcu === "string" && boardMeta.mcu) || undefined; + + const supportedFromCoverage = Array.isArray(coverage?.supported) + ? (coverage!.supported as unknown[]).length + : undefined; + const supportedFromMeta = + typeof boardMeta?.supportedPartCount === "number" + ? (boardMeta.supportedPartCount as number) + : undefined; + const supported_part_count = supportedFromCoverage ?? supportedFromMeta; + + // Prefer explicit mint flags from board.json when present + const mint_ok = + typeof boardMeta?.mint_ok === "boolean" + ? (boardMeta.mint_ok as boolean) + : typeof boardMeta?.ok === "boolean" + ? (boardMeta.ok as boolean) + : supported_part_count !== undefined + ? supported_part_count >= 1 && !!board + : undefined; + + return { + board, + mcu, + diagram, + user_context, + agent_brief, + coverage_md, + design_context_md, + mapping, + ...(typeof mint_ok === "boolean" ? { mint_ok } : {}), + ...(typeof supported_part_count === "number" + ? { supported_part_count } + : {}), + source_kind: + design_context_md || mapping + ? "import" + : board + ? "board" + : undefined, + }; +} + +/** Local labwired_context — same pure engine as monorepo. */ +export function buildWorkspaceContext( + workspaceRoot: string | undefined, + goal?: string +): WorkspaceContext { + if (!workspaceRoot) { + const empty = buildLabwiredContext({ goal }); + return { ...empty, pack: {} }; + } + const pack = loadWorkspacePack(workspaceRoot); + const result = buildLabwiredContext({ goal, pack }); + return { ...result, pack }; +} + +/** Compact agent handoff block for Start Agent / freeform. */ +export function contextHandoffBlock(ctx: WorkspaceContext): string { + return [ + "[labwired_context]", + ctx.summary, + `next: ${ctx.next.join(" → ")}`, + "", + ctx.agent_brief.slice(0, 2500), + "", + "Claims: model_verified only via labwired_verify; design_context is not a prove claim.", + ].join("\n"); +} From ebcec4006782ede895769025f9f82f8a6e0c8c65 Mon Sep 17 00:00:00 2001 From: Andrii Shylenko <14119286+w1ne@users.noreply.github.com> Date: Tue, 11 Aug 2026 02:40:56 +0200 Subject: [PATCH 2/4] test(agent): workspace e2e + mint_ok for context twin_ready board.json writes mint_ok/supported_part_count; multiImport uses sole flag engine; workspace e2e script for ship gate. --- .../scripts/context-first-workspace-e2e.mjs | 69 ++ .../labwired-vscode/src/board/boardMint.ts | 326 +++++++++ .../labwired-vscode/src/board/multiImport.ts | 681 ++++++++++++++++++ 3 files changed, 1076 insertions(+) create mode 100644 extensions/labwired-vscode/scripts/context-first-workspace-e2e.mjs create mode 100644 extensions/labwired-vscode/src/board/boardMint.ts create mode 100644 extensions/labwired-vscode/src/board/multiImport.ts diff --git a/extensions/labwired-vscode/scripts/context-first-workspace-e2e.mjs b/extensions/labwired-vscode/scripts/context-first-workspace-e2e.mjs new file mode 100644 index 0000000..8ef6e31 --- /dev/null +++ b/extensions/labwired-vscode/scripts/context-first-workspace-e2e.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +/** + * Extension workspace e2e: write .labwired like a mint, assert context twin_ready. + * Uses generated flag engine only (no vscode). + */ +import fs from "fs"; +import path from "path"; +import os from "os"; +import { fileURLToPath, pathToFileURL } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const genPath = path.join(__dirname, "../src/board/contextFlags.generated.ts"); +const { buildLabwiredContext } = await import(pathToFileURL(genPath).href); + +const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "lw-ctx-e2e-")); +const lab = path.join(tmp, ".labwired"); +fs.mkdirSync(lab, { recursive: true }); + +const diagram = { + version: 1, + board: "esp32-c3-supermini", + parts: [ + { id: "mcu", type: "esp32-c3-supermini" }, + { id: "led1", type: "led", attrs: { color: "green" } }, + ], + wires: [ + { from: { part: "mcu", pin: "GPIO8" }, to: { part: "led1", pin: "A" } }, + { from: { part: "mcu", pin: "GND" }, to: { part: "led1", pin: "C" } }, + ], +}; + +fs.writeFileSync(path.join(lab, "diagram.json"), JSON.stringify(diagram, null, 2)); +fs.writeFileSync( + path.join(lab, "board.json"), + JSON.stringify({ + version: 1, + board: "esp32-c3-supermini", + mint_ok: true, + supported_part_count: 2, + supportedPartCount: 2, + ok: true, + }), +); + +// Mimic loadWorkspacePack without vscode +const pack = { + board: "esp32-c3-supermini", + diagram, + mint_ok: true, + supported_part_count: 2, +}; +const ctx = buildLabwiredContext({ + goal: "Blink the LED and prove it on the twin.", + pack, +}); + +if (ctx.mode !== "twin_ready" || !ctx.twin_buildable) { + console.error("FAIL workspace e2e", ctx); + process.exit(1); +} + +const thin = buildLabwiredContext({ pack: { user_context: "x" } }); +if (thin.mode !== "empty" || thin.quality !== "thin") { + console.error("FAIL thin", thin); + process.exit(1); +} + +console.log("WORKSPACE_E2E_OK", ctx.summary); +fs.rmSync(tmp, { recursive: true, force: true }); diff --git a/extensions/labwired-vscode/src/board/boardMint.ts b/extensions/labwired-vscode/src/board/boardMint.ts new file mode 100644 index 0000000..7453ecd --- /dev/null +++ b/extensions/labwired-vscode/src/board/boardMint.ts @@ -0,0 +1,326 @@ +/** + * Diagram → twin mint: keep only catalog-supported parts, write .labwired/. + * Pure TS (no vscode) so node smoke tests can run it. + */ +import * as fs from "fs"; +import * as path from "path"; + +export type DiagramPart = { + id: string; + type: string; + attrs?: Record; + x?: number; + y?: number; + rotate?: number; +}; + +export type DiagramWire = { + from: { part: string; pin: string }; + to: { part: string; pin: string }; + color?: string; +}; + +export type PlaygroundDiagram = { + version?: number; + board: string; + parts: DiagramPart[]; + wires: DiagramWire[]; + firmware?: unknown; +}; + +export type PartVerdict = { + id: string; + type: string; + status: "supported" | "unknown" | "dropped"; + reason?: string; + resolvedType?: string; +}; + +export type BoardMintResult = { + ok: boolean; + board: string; + mcu?: string; + sourcePath: string; + outDir: string; + supported: PartVerdict[]; + dropped: PartVerdict[]; + twin: PlaygroundDiagram; + coveragePath: string; + boardPath: string; + diagramPath: string; + summary: string; + errors: string[]; +}; + +export type CatalogLookup = { + /** Exact or alias-resolved catalog type, or undefined if unknown */ + resolvePartType(type: string): string | undefined; + /** Normalize board id (aliases), or undefined if unknown */ + resolveBoard(board: string): string | undefined; + /** True if type is an MCU-class part (optional) */ + isMcuType?(type: string): boolean; +}; + +/** Common playground / MCP board spellings → catalog board id */ +export const BOARD_ALIASES: Record = { + "esp32c3": "esp32-c3-supermini", + "esp32-c3": "esp32-c3-supermini", + "esp32c3-supermini": "esp32-c3-supermini", + "esp32s3": "esp32-s3-zero", + "esp32-s3": "esp32-s3-zero", + "stm32l476": "nucleo-l476rg", + "nucleo-l476rg": "nucleo-l476rg", + "stm32f401": "nucleo-f401re", + "nucleo-f401re": "nucleo-f401re", + "stm32f103": "stm32f103-blinky", + "stm32f103-blinky": "stm32f103-blinky", + "rpi-pico": "rpi-pico", + "rp2040": "rpi-pico", + "nrf52840": "nrf52840-dk", + "nrf52840-dk": "nrf52840-dk", + "esp32": "esp32", + "adafruit-feather-esp32-v2": "adafruit-feather-esp32-v2", +}; + +export function normalizeKey(s: string): string { + return s.trim().toLowerCase().replace(/_/g, "-"); +} + +export function parseDiagram(raw: unknown): PlaygroundDiagram { + if (!raw || typeof raw !== "object") { + throw new Error("diagram must be a JSON object"); + } + const d = raw as Record; + const board = String(d.board || "").trim(); + if (!board) throw new Error("diagram.board is required"); + const partsIn = Array.isArray(d.parts) ? d.parts : []; + const parts: DiagramPart[] = partsIn.map((p, i) => { + const o = p as Record; + const id = String(o.id || `part${i}`); + const type = String(o.type || "").trim(); + if (!type) throw new Error(`parts[${i}] missing type`); + return { + id, + type, + attrs: o.attrs as Record | undefined, + x: typeof o.x === "number" ? o.x : undefined, + y: typeof o.y === "number" ? o.y : undefined, + rotate: typeof o.rotate === "number" ? o.rotate : undefined, + }; + }); + const wiresIn = Array.isArray(d.wires) ? d.wires : []; + const wires: DiagramWire[] = wiresIn.map((w, i) => { + const o = w as Record; + const from = o.from as { part?: string; pin?: string } | undefined; + const to = o.to as { part?: string; pin?: string } | undefined; + if (!from?.part || !from?.pin || !to?.part || !to?.pin) { + throw new Error(`wires[${i}] needs from/to { part, pin }`); + } + return { + from: { part: String(from.part), pin: String(from.pin) }, + to: { part: String(to.part), pin: String(to.pin) }, + color: o.color ? String(o.color) : undefined, + }; + }); + return { + version: typeof d.version === "number" ? d.version : 1, + board, + parts, + wires, + firmware: d.firmware, + }; +} + +export function mintTwinFromDiagram( + diagram: PlaygroundDiagram, + lookup: CatalogLookup, + opts: { sourcePath: string; workspaceRoot: string } +): BoardMintResult { + const errors: string[] = []; + const outDir = path.join(opts.workspaceRoot, ".labwired"); + const resolvedBoard = + lookup.resolveBoard(diagram.board) || + BOARD_ALIASES[normalizeKey(diagram.board)] || + diagram.board; + + if (!lookup.resolveBoard(diagram.board) && !BOARD_ALIASES[normalizeKey(diagram.board)]) { + // Still allow mint with warning — board string kept as-is for agent + errors.push( + `board "${diagram.board}" not in local alias/catalog — kept as-is for agent` + ); + } + + const supported: PartVerdict[] = []; + const dropped: PartVerdict[] = []; + const keptParts: DiagramPart[] = []; + const keptIds = new Set(); + let mcu: string | undefined; + + for (const part of diagram.parts) { + const resolved = lookup.resolvePartType(part.type); + if (!resolved) { + dropped.push({ + id: part.id, + type: part.type, + status: "unknown", + reason: "type not in LabWired catalog — dropped from twin", + }); + continue; + } + const outPart: DiagramPart = { ...part, type: resolved }; + keptParts.push(outPart); + keptIds.add(part.id); + supported.push({ + id: part.id, + type: part.type, + status: "supported", + resolvedType: resolved, + }); + if (lookup.isMcuType?.(resolved) || /mcu|nucleo|esp32|stm32|nrf|pico|blackpill/i.test(resolved)) { + mcu = resolved; + } + } + + const twinWires = diagram.wires.filter( + (w) => keptIds.has(w.from.part) && keptIds.has(w.to.part) + ); + const droppedWires = diagram.wires.length - twinWires.length; + + const twin: PlaygroundDiagram = { + version: 1, + board: resolvedBoard, + parts: keptParts, + wires: twinWires, + }; + + fs.mkdirSync(outDir, { recursive: true }); + const diagramPath = path.join(outDir, "diagram.json"); + const boardPath = path.join(outDir, "board.json"); + const coveragePath = path.join(outDir, "coverage.json"); + const coverageMdPath = path.join(outDir, "coverage.md"); + + const boardMeta = { + version: 1, + board: resolvedBoard, + mcu: mcu || null, + source: opts.sourcePath, + mintedAt: new Date().toISOString(), + supportedPartCount: supported.length, + /** Alias for labwired_context pack (mint-honest twin_buildable) */ + supported_part_count: supported.length, + mint_ok: supported.length > 0, + ok: supported.length > 0, + droppedPartCount: dropped.length, + droppedWireCount: droppedWires, + }; + + const coverage = { + version: 1, + board: resolvedBoard, + source: opts.sourcePath, + supported, + dropped, + droppedWireCount: droppedWires, + }; + + fs.writeFileSync(diagramPath, JSON.stringify(twin, null, 2) + "\n", "utf8"); + fs.writeFileSync(boardPath, JSON.stringify(boardMeta, null, 2) + "\n", "utf8"); + fs.writeFileSync(coveragePath, JSON.stringify(coverage, null, 2) + "\n", "utf8"); + + const md = [ + `# Twin coverage`, + ``, + `- **Board:** \`${resolvedBoard}\``, + `- **MCU:** \`${mcu || "?"}\``, + `- **Source:** \`${opts.sourcePath}\``, + `- **Supported parts:** ${supported.length}`, + `- **Dropped parts:** ${dropped.length}`, + `- **Dropped wires:** ${droppedWires}`, + ``, + `## Supported`, + ...supported.map( + (s) => + `- \`${s.id}\` · ${s.type}${s.resolvedType && s.resolvedType !== s.type ? ` → ${s.resolvedType}` : ""}` + ), + ``, + `## Dropped (not in catalog — not on twin)`, + ...(dropped.length + ? dropped.map((d) => `- \`${d.id}\` · \`${d.type}\` — ${d.reason || "unknown"}`) + : ["- (none)"]), + ``, + `Twin diagram: \`.labwired/diagram.json\``, + ``, + ].join("\n"); + fs.writeFileSync(coverageMdPath, md, "utf8"); + + const ok = supported.length > 0; + if (!ok) errors.push("no supported parts — twin is empty"); + + const summary = [ + `Board twin minted: ${resolvedBoard}`, + `supported ${supported.length} · dropped ${dropped.length}` + + (droppedWires ? ` · wires dropped ${droppedWires}` : ""), + mcu ? `mcu ${mcu}` : "mcu ?", + dropped.length + ? `dropped: ${dropped.map((d) => d.type).join(", ")}` + : "all parts mapped", + `→ .labwired/diagram.json · coverage.md`, + `Next: Start agent and develop against this twin.`, + ].join("\n"); + + return { + ok, + board: resolvedBoard, + mcu, + sourcePath: opts.sourcePath, + outDir, + supported, + dropped, + twin, + coveragePath, + boardPath, + diagramPath, + summary, + errors, + }; +} + +export function loadBoardMeta( + workspaceRoot: string +): { board: string; mcu?: string } | undefined { + const p = path.join(workspaceRoot, ".labwired", "board.json"); + try { + if (!fs.existsSync(p)) return undefined; + const j = JSON.parse(fs.readFileSync(p, "utf8")) as { + board?: string; + mcu?: string | null; + }; + if (!j.board) return undefined; + return { board: j.board, mcu: j.mcu || undefined }; + } catch { + return undefined; + } +} + +export function mintFromFile( + sourcePath: string, + workspaceRoot: string, + lookup: CatalogLookup +): BoardMintResult { + const raw = JSON.parse(fs.readFileSync(sourcePath, "utf8")); + const diagram = parseDiagram(raw); + return mintTwinFromDiagram(diagram, lookup, { sourcePath, workspaceRoot }); +} + +/** Mint from an in-memory starter diagram (catalog New board). */ +export function mintFromDiagramObject( + diagram: PlaygroundDiagram, + workspaceRoot: string, + lookup: CatalogLookup, + sourceLabel = "catalog" +): BoardMintResult { + return mintTwinFromDiagram(diagram, lookup, { + sourcePath: sourceLabel, + workspaceRoot, + }); +} diff --git a/extensions/labwired-vscode/src/board/multiImport.ts b/extensions/labwired-vscode/src/board/multiImport.ts new file mode 100644 index 0000000..23dec0b --- /dev/null +++ b/extensions/labwired-vscode/src/board/multiImport.ts @@ -0,0 +1,681 @@ +/** + * Multi-source circuit/board import → .labwired/ artifacts + optional twin mint. + * Sources: PDF, KiCad, netlist, diagram.json, images, BOM CSV, free text. + */ +import * as fs from "fs"; +import * as path from "path"; +import { + extractPdfText, + matchCatalogInText, + type CatalogHit, + type PdfImportKind, +} from "./pdfImport"; +import type { CatalogBoard } from "./catalogBoards"; +import { + buildStarterDiagram, + type StarterPreset, +} from "./catalogBoards"; +import { + mintFromDiagramObject, + mintFromFile, + type BoardMintResult, + type CatalogLookup, +} from "./boardMint"; +import { + graphFromHints, + mergeGraphs, + parseSpiceNetlist, + type CircuitGraph, +} from "./netlistGraph"; +import { + bomMapsToHits, + mapBomRows, + parseBomCsv, + type BomMapResult, +} from "./bomMapper"; +import { + coverageFromMapping, + graphToDiagram, +} from "./graphToDiagram"; +import { buildWorkspaceContext } from "./workspaceContext"; + +export type ImportSourceKind = + | "pdf-schematic" + | "pdf-datasheet" + | "kicad-sch" + | "kicad-pcb" + | "netlist" + | "diagram-json" + | "image" + | "bom-csv" + | "text" + | "unknown"; + +export type MultiImportResult = { + ok: boolean; + sourceKind: ImportSourceKind; + sourcePath: string; + destPath: string; + textPath?: string; + hits: CatalogHit[]; + suggestedBoardId?: string; + minted?: BoardMintResult; + agentPrompt: string; + summary: string; + error?: string; + /** Freeform notes from the user (MCU, goals, constraints). */ + userContext?: string; +}; + +function safeBase(filePath: string): string { + return ( + path.basename(filePath, path.extname(filePath)).replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 80) || + "import" + ); +} + +function copyInto( + workspaceRoot: string, + subdir: string, + sourcePath: string +): string { + const dir = path.join(workspaceRoot, ".labwired", subdir); + fs.mkdirSync(dir, { recursive: true }); + const dest = path.join(dir, path.basename(sourcePath)); + if (path.resolve(sourcePath) !== path.resolve(dest)) { + fs.copyFileSync(sourcePath, dest); + } + return dest; +} + +function writeText( + workspaceRoot: string, + name: string, + text: string +): string { + const dir = path.join(workspaceRoot, ".labwired", "import"); + fs.mkdirSync(dir, { recursive: true }); + const p = path.join(dir, name); + fs.writeFileSync(p, text, "utf8"); + return p; +} + +function detectKind(filePath: string, force?: ImportSourceKind): ImportSourceKind { + if (force && force !== "unknown") return force; + const ext = path.extname(filePath).toLowerCase(); + const base = path.basename(filePath).toLowerCase(); + if (ext === ".pdf") return "pdf-schematic"; // default; UI can override datasheet + if (ext === ".kicad_sch") return "kicad-sch"; + if (ext === ".kicad_pcb" || ext === ".kicad_pro") return "kicad-pcb"; + if (ext === ".net" || ext === ".cir" || ext === ".sp" || base.endsWith(".netlist")) + return "netlist"; + if (ext === ".json" || base === "diagram.json") return "diagram-json"; + if ([".png", ".jpg", ".jpeg", ".webp", ".gif", ".tif", ".tiff"].includes(ext)) + return "image"; + if (ext === ".csv") return "bom-csv"; + if ([".txt", ".md", ".log"].includes(ext)) return "text"; + return "unknown"; +} + +/** Extract lib_id / Value / Reference from KiCad s-expr schematic (best-effort). */ +export function parseKicadSchHints(content: string): string[] { + const hints: string[] = []; + const libIds = content.matchAll(/\(lib_id\s+"([^"]+)"\)/g); + for (const m of libIds) { + const full = m[1]; + hints.push(full); + const short = full.includes(":") ? full.split(":").pop()! : full; + hints.push(short); + } + const values = content.matchAll( + /\(property\s+"Value"\s+"([^"]+)"/g + ); + for (const m of values) hints.push(m[1]); + const refs = content.matchAll( + /\(property\s+"Reference"\s+"([^"]+)"/g + ); + for (const m of refs) hints.push(m[1]); + return [...new Set(hints.map((h) => h.trim()).filter(Boolean))]; +} + +export function parseBomCsvHints(content: string): string[] { + const lines = content.split(/\r?\n/).slice(0, 500); + const hints: string[] = []; + for (const line of lines) { + for (const cell of line.split(/[,;\t]/)) { + const t = cell.trim().replace(/^"|"$/g, ""); + if (t.length >= 2 && t.length < 64 && /[a-zA-Z]/.test(t)) hints.push(t); + } + } + return [...new Set(hints)]; +} + +export function parseNetlistHints(content: string): string[] { + const hints: string[] = []; + // spice-like: R1 1 2 10k, XU1 ... + for (const line of content.split(/\r?\n/)) { + const t = line.trim(); + if (!t || t.startsWith("*") || t.startsWith("#")) continue; + const toks = t.split(/\s+/); + if (toks[0]) hints.push(toks[0].replace(/[0-9]+$/, "")); // R1 → R + for (const tok of toks.slice(1, 6)) { + if (/^[A-Za-z][A-Za-z0-9_-]{1,30}$/.test(tok)) hints.push(tok); + } + } + return [...new Set(hints)]; +} + +function agentPrompt(opts: { + kind: ImportSourceKind; + sourcePath: string; + destPath: string; + textPath?: string; + hits: CatalogHit[]; + suggestedBoardId?: string; + userContext?: string; + twinBuildable?: boolean; + droppedNotes?: string; +}): string { + const twinOk = opts.twinBuildable === true; + return [ + "Customer circuit import for LabWired.", + `Source kind: ${opts.kind}`, + `Source file: ${opts.destPath}`, + opts.textPath ? `Extracted / notes text: ${opts.textPath}` : "", + opts.suggestedBoardId + ? `Suggested board/MCU: ${opts.suggestedBoardId}` + : "No board auto-matched.", + opts.hits.length + ? `Catalog hits: ${opts.hits + .slice(0, 20) + .map((h) => h.id) + .join(", ")}` + : "No catalog hits yet.", + `twin_buildable: ${twinOk}`, + "", + opts.userContext?.trim() + ? [ + "## User context (authoritative for intent — still no inventing pins)", + opts.userContext.trim(), + "", + ].join("\n") + : "", + opts.droppedNotes + ? ["## Dropped / unmodeled (keep for driver design via datasheet tools)", opts.droppedNotes, ""].join( + "\n" + ) + : "", + "## Design context (ALWAYS use this)", + "Even if the twin is incomplete or not runnable, use extracts + mapping + user context", + "plus labwired_part / labwired_datasheet to design drivers, HAL, init, and app structure.", + "Do not block on twin. Do not invent electrical facts for dropped parts — cite tools or mark missing.", + "", + twinOk + ? [ + "## Twin path (buildable)", + "1. Prefer catalog parts on the diagram; validate if possible.", + "2. Scaffold / use existing FW → compile → labwired_run → labwired_verify.", + "3. model_verified only from verify success.", + ].join("\n") + : [ + "## Twin not fully buildable yet", + "1. Still design firmware/drivers from design context + part/datasheet tools.", + "2. List what must be modeled for a full twin later (coverage).", + "3. If a partial board (MCU only) can run, use it for blink/UART beachhead only.", + ].join("\n"), + "", + "Follow skill import-circuit. Prefer MCP labwired_import when available.", + ] + .filter(Boolean) + .join("\n"); +} + +export function importCircuitSource(opts: { + sourcePath: string; + workspaceRoot: string; + boards: CatalogBoard[]; + partTypes: string[]; + lookup: CatalogLookup; + forceKind?: ImportSourceKind; + /** When board suggested, auto-mint blink starter */ + autoMintStarter?: boolean; + starterPreset?: StarterPreset; + /** Freeform user notes: board name, goal, constraints, known pins, etc. */ + userContext?: string; +}): MultiImportResult { + const userContext = (opts.userContext || "").trim(); + const kind = detectKind(opts.sourcePath, opts.forceKind); + const base = safeBase(opts.sourcePath); + let destPath = opts.sourcePath; + let textPath: string | undefined; + let textBlob = ""; + const extraHints: string[] = []; + + try { + switch (kind) { + case "pdf-schematic": + case "pdf-datasheet": { + destPath = copyInto( + opts.workspaceRoot, + kind === "pdf-datasheet" ? "datasheets" : "import", + opts.sourcePath + ); + textPath = path.join( + opts.workspaceRoot, + ".labwired", + "import", + `${base}.txt` + ); + const ex = extractPdfText(destPath, textPath); + if (!ex.ok) { + return { + ok: false, + sourceKind: kind, + sourcePath: opts.sourcePath, + destPath, + hits: [], + agentPrompt: "", + summary: `PDF extract failed: ${ex.error}`, + error: ex.error, + }; + } + textBlob = fs.readFileSync(textPath, "utf8"); + if (kind === "pdf-datasheet") { + // also leave a copy under datasheets for agentic extract + copyInto(opts.workspaceRoot, "datasheets", opts.sourcePath); + } + break; + } + case "kicad-sch": + case "kicad-pcb": { + destPath = copyInto(opts.workspaceRoot, "import", opts.sourcePath); + const raw = fs.readFileSync(destPath, "utf8"); + extraHints.push(...parseKicadSchHints(raw)); + textBlob = raw.slice(0, 500_000); + textPath = writeText( + opts.workspaceRoot, + `${base}.kicad-extract.txt`, + `# KiCad extract hints\n${extraHints.join("\n")}\n\n--- raw head ---\n${raw.slice(0, 80_000)}` + ); + break; + } + case "netlist": { + destPath = copyInto(opts.workspaceRoot, "import", opts.sourcePath); + textBlob = fs.readFileSync(destPath, "utf8"); + extraHints.push(...parseNetlistHints(textBlob)); + textPath = writeText( + opts.workspaceRoot, + `${base}.net-hints.txt`, + extraHints.join("\n") + ); + break; + } + case "diagram-json": { + destPath = copyInto(opts.workspaceRoot, "import", opts.sourcePath); + // Direct mint + const mint = mintFromFile(destPath, opts.workspaceRoot, opts.lookup); + fs.copyFileSync( + destPath, + path.join(opts.workspaceRoot, ".labwired", "source-diagram.json") + ); + if (userContext) { + writeText(opts.workspaceRoot, "USER_CONTEXT.md", userContext + "\n"); + } + const prompt = agentPrompt({ + kind, + sourcePath: opts.sourcePath, + destPath, + hits: mint.supported.map((s) => ({ + id: s.resolvedType || s.type, + kind: "part", + score: 50, + })), + suggestedBoardId: mint.board, + userContext, + }); + writeText(opts.workspaceRoot, "AGENT_PROMPT.md", prompt + "\n"); + return { + ok: mint.ok, + sourceKind: kind, + sourcePath: opts.sourcePath, + destPath, + hits: [], + suggestedBoardId: mint.board, + minted: mint, + agentPrompt: prompt, + summary: mint.summary + (userContext ? `\nuser context: set` : ""), + userContext: userContext || undefined, + }; + } + case "image": { + destPath = copyInto(opts.workspaceRoot, "import", opts.sourcePath); + textBlob = `Image schematic: ${path.basename(destPath)}. Use vision/agent to identify board and parts; map only to catalog.`; + textPath = writeText( + opts.workspaceRoot, + `${base}.image-notes.txt`, + textBlob + ); + break; + } + case "bom-csv": { + destPath = copyInto(opts.workspaceRoot, "import", opts.sourcePath); + textBlob = fs.readFileSync(destPath, "utf8"); + extraHints.push(...parseBomCsvHints(textBlob)); + textPath = writeText( + opts.workspaceRoot, + `${base}.bom-hints.txt`, + extraHints.join("\n") + ); + break; + } + case "text": { + destPath = copyInto(opts.workspaceRoot, "import", opts.sourcePath); + textBlob = fs.readFileSync(destPath, "utf8"); + textPath = destPath; + break; + } + default: { + destPath = copyInto(opts.workspaceRoot, "import", opts.sourcePath); + try { + textBlob = fs.readFileSync(destPath, "utf8").slice(0, 200_000); + } catch { + textBlob = path.basename(destPath); + } + textPath = writeText( + opts.workspaceRoot, + `${base}.notes.txt`, + textBlob + ); + } + } + } catch (e) { + return { + ok: false, + sourceKind: kind, + sourcePath: opts.sourcePath, + destPath, + hits: [], + agentPrompt: "", + summary: `Import failed: ${e}`, + error: String(e), + }; + } + + // User context participates in catalog matching (e.g. "we're on esp32-c3") + const combined = `${textBlob}\n${extraHints.join("\n")}\n${userContext}`; + const hits = matchCatalogInText(combined, opts.boards, opts.partTypes); + // boost explicit KiCad/BOM hints that equal catalog ids + for (const h of extraHints) { + const key = h.toLowerCase(); + for (const b of opts.boards) { + if ( + b.id.toLowerCase() === key || + b.chip.toLowerCase() === key || + b.mcuType.toLowerCase() === key + ) { + hits.unshift({ id: b.id, kind: "board", score: 100 }); + } + } + for (const t of opts.partTypes) { + if (t.toLowerCase() === key) { + hits.unshift({ id: t, kind: "part", score: 80 }); + } + } + } + // dedupe hits + const seen = new Set(); + const uniqHits = hits.filter((h) => { + const k = h.id.toLowerCase(); + if (seen.has(k)) return false; + seen.add(k); + return true; + }); + + const suggestedBoardId = uniqHits.find( + (h) => h.kind === "board" || h.kind === "chip" + )?.id; + + if (userContext) { + writeText(opts.workspaceRoot, "USER_CONTEXT.md", userContext + "\n"); + } + + const manifest = { + version: 1, + sourceKind: kind, + sourcePath: opts.sourcePath, + destPath, + textPath, + userContext: userContext || null, + importedAt: new Date().toISOString(), + hits: uniqHits.slice(0, 30), + suggestedBoardId: suggestedBoardId || null, + extraHints: extraHints.slice(0, 100), + }; + writeText( + opts.workspaceRoot, + `${base}.manifest.json`, + JSON.stringify(manifest, null, 2) + "\n" + ); + + // ——— Real import path: graph → catalog map → diagram → mint ——— + let graph: CircuitGraph | undefined; + let bomMaps: BomMapResult[] | undefined; + + if (kind === "netlist" && textBlob) { + graph = parseSpiceNetlist(textBlob); + } else if (kind === "bom-csv" && textBlob) { + const rows = parseBomCsv(textBlob); + bomMaps = mapBomRows(rows, opts.partTypes); + for (const h of bomMapsToHits(bomMaps)) { + uniqHits.unshift(h); + } + // BOM-only graph: one component per mapped row + graph = { + format: "bom", + components: bomMaps.map((m, i) => ({ + ref: m.row.ref || `U${i + 1}`, + value: m.catalogType || m.row.value || m.row.raw, + pins: [], + })), + nets: {}, + }; + } else if ( + (kind === "kicad-sch" || kind === "kicad-pcb") && + extraHints.length + ) { + graph = graphFromHints(extraHints, "kicad-hints"); + } else if (kind === "pdf-schematic" || kind === "text") { + // hints from catalog hits as weak components + graph = graphFromHints( + uniqHits.map((h) => h.id), + "mixed" + ); + } + + if (graph && extraHints.length && kind !== "kicad-sch") { + graph = mergeGraphs(graph, graphFromHints(extraHints)); + } + + let minted: BoardMintResult | undefined; + const board = + (suggestedBoardId && + (opts.boards.find( + (b) => + b.id === suggestedBoardId || + b.chip === suggestedBoardId || + b.mcuType === suggestedBoardId + ) || + opts.boards.find( + (b) => + suggestedBoardId!.includes(b.id) || + b.id.includes(suggestedBoardId!) + ))) || + opts.boards.find((b) => /esp32c3|c3-supermini/i.test(b.id)) || + opts.boards[0]; + + if (opts.autoMintStarter !== false && board && graph) { + try { + const g2d = graphToDiagram({ + graph, + board, + catalogTypes: opts.partTypes, + bomMaps, + }); + const lab = path.join(opts.workspaceRoot, ".labwired"); + fs.mkdirSync(lab, { recursive: true }); + fs.writeFileSync( + path.join(lab, "source-diagram.json"), + JSON.stringify(g2d.diagram, null, 2) + "\n" + ); + fs.writeFileSync( + path.join(lab, "coverage-graph.md"), + coverageFromMapping( + g2d.boardId, + g2d.mapping, + `${kind}:${path.basename(destPath)}` + ) + ); + fs.writeFileSync( + path.join(lab, "graph-mapping.json"), + JSON.stringify(g2d.mapping, null, 2) + "\n" + ); + minted = mintFromDiagramObject( + g2d.diagram, + opts.workspaceRoot, + opts.lookup, + `${kind}:${path.basename(destPath)}` + ); + // Prefer graph coverage text in summary + if (g2d.dropped.length) { + writeText( + opts.workspaceRoot, + "DROPPED.md", + g2d.dropped + .map((d) => `- ${d.ref}: ${d.value} — ${d.reason}`) + .join("\n") + "\n" + ); + } + } catch { + /* fall through to starter */ + } + } + + // Fallback: blink starter if graph path failed but board known + if (!minted && opts.autoMintStarter !== false && board) { + const diagram = buildStarterDiagram(board, opts.starterPreset || "blink"); + const lab = path.join(opts.workspaceRoot, ".labwired"); + fs.mkdirSync(lab, { recursive: true }); + fs.writeFileSync( + path.join(lab, "source-diagram.json"), + JSON.stringify(diagram, null, 2) + "\n" + ); + minted = mintFromDiagramObject( + diagram, + opts.workspaceRoot, + opts.lookup, + `${kind}:starter:${board.id}` + ); + } + + // Sole flag engine (contextFlags.generated) — do not invent twin_ready from vibes + const ctxSnap = buildWorkspaceContext(opts.workspaceRoot, userContext); + const twinBuildable = ctxSnap.twin_buildable; + const designContextOk = ctxSnap.design_context_ok; + + let droppedNotes = ""; + try { + const droppedPath = path.join(opts.workspaceRoot, ".labwired", "import", "DROPPED.md"); + if (fs.existsSync(droppedPath)) { + droppedNotes = fs.readFileSync(droppedPath, "utf8").slice(0, 4000); + } + } catch { + /* */ + } + + const prompt = agentPrompt({ + kind, + sourcePath: opts.sourcePath, + destPath, + textPath, + hits: uniqHits, + suggestedBoardId: suggestedBoardId || board?.id, + userContext, + twinBuildable, + droppedNotes: droppedNotes || undefined, + }); + writeText(opts.workspaceRoot, "AGENT_PROMPT.md", prompt + "\n"); + + // Always leave a stable DESIGN_CONTEXT pack for the LLM (even without twin) + writeText( + opts.workspaceRoot, + "DESIGN_CONTEXT.md", + [ + "# Design context (always — twin optional)", + "", + `- twin_buildable: ${twinBuildable}`, + `- design_context_ok: ${designContextOk}`, + `- source: ${kind} · ${path.basename(destPath)}`, + suggestedBoardId || board + ? `- board_hint: ${suggestedBoardId || board?.id}` + : "- board_hint: (none)", + "", + "## User context", + userContext || "(none)", + "", + "## Catalog hits", + uniqHits.length + ? uniqHits.map((h) => `- ${h.id} (${h.kind})`).join("\n") + : "(none)", + "", + "## For the agent", + "Use this file + USER_CONTEXT.md + extracts to design drivers/FW even if the twin is incomplete.", + "Cite labwired_part / labwired_datasheet for dropped/unmodeled parts. Never invent pins.", + twinBuildable + ? "Twin available: prefer prove path after scaffold." + : "Twin not fully buildable: design_only first; list catalog gaps for later twin.", + "", + ].join("\n") + ); + + const summary = [ + `Imported ${kind}: ${path.basename(destPath)}`, + textPath ? `text/notes → ${path.relative(opts.workspaceRoot, textPath)}` : "", + userContext + ? `user context: ${userContext.slice(0, 120)}${userContext.length > 120 ? "…" : ""}` + : "user context: (none)", + graph + ? `graph: ${graph.components.length} components · ${Object.keys(graph.nets).length} nets` + : "graph: (none)", + `design_context: ${designContextOk ? "ok" : "thin"} · twin_buildable: ${twinBuildable}`, + suggestedBoardId || board + ? `board: ${suggestedBoardId || board?.id}` + : "no board auto-matched", + uniqHits.length + ? `catalog hits: ${uniqHits + .slice(0, 10) + .map((h) => h.id) + .join(", ")}` + : "no catalog hits", + minted + ? `twin minted: ${minted.board} (${minted.supported.length} parts) [graph pipeline]` + : "twin not minted — design context still available for drivers/FW", + ] + .filter(Boolean) + .join("\n"); + + return { + ok: designContextOk || twinBuildable, + sourceKind: kind, + sourcePath: opts.sourcePath, + destPath, + textPath, + hits: uniqHits, + suggestedBoardId: suggestedBoardId || board?.id, + minted, + agentPrompt: prompt, + summary, + userContext: userContext || undefined, + }; +} + +export { detectKind as detectImportKind }; From 68bd833decf75e0c1cb7707167df7b98bf42be3b Mon Sep 17 00:00:00 2001 From: Andrii Shylenko <14119286+w1ne@users.noreply.github.com> Date: Tue, 11 Aug 2026 03:12:29 +0200 Subject: [PATCH 3/4] feat(ext): run / prove / debug digital twin from agent workbench Hosted MCP labwired_run, labwired_verify, labwired_debug using .labwired/diagram.json. Chat buttons + commands; F5 DAP when LabWired Debugger is installed. Verify/Debug modes hit the twin before agent. --- extensions/labwired-vscode/package.json | 582 ++++++++++++---- extensions/labwired-vscode/src/extension.ts | 629 ++++++++++++++++-- .../src/providers/chatProvider.ts | 409 +++++++++--- .../src/providers/evidenceProvider.ts | 47 ++ .../labwired-vscode/src/twin/twinSession.ts | 401 +++++++++++ 5 files changed, 1808 insertions(+), 260 deletions(-) create mode 100644 extensions/labwired-vscode/src/twin/twinSession.ts diff --git a/extensions/labwired-vscode/package.json b/extensions/labwired-vscode/package.json index 1ca2468..a52087f 100644 --- a/extensions/labwired-vscode/package.json +++ b/extensions/labwired-vscode/package.json @@ -1,8 +1,8 @@ { "name": "labwired-vscode", "displayName": "LabWired", - "description": "Embedder-clone foundation for LabWired: agent chat, modes, monitor, evidence, plan — IDE-native UI.", - "version": "0.6.3", + "description": "Embedder + twin: real multi-source import (netlist graph, BOM map, PDF/KiCad) → catalog-honest twin.", + "version": "0.10.0", "compatibleCliVersion": "0.3.7", "publisher": "labwired", "license": "MIT", @@ -57,41 +57,11 @@ }, "views": { "labwired": [ - { - "type": "webview", - "id": "labwired.overview", - "name": "Overview", - "icon": "media/labwired-mark.svg" - }, { "type": "webview", "id": "labwired.chat", "name": "Agent", "icon": "media/labwired-mark.svg" - }, - { - "type": "webview", - "id": "labwired.history", - "name": "History", - "icon": "media/labwired-mark.svg" - }, - { - "type": "webview", - "id": "labwired.catalog", - "name": "Catalog", - "icon": "media/labwired-mark.svg" - }, - { - "type": "webview", - "id": "labwired.evidence", - "name": "Evidence", - "icon": "media/labwired-mark.svg" - }, - { - "type": "webview", - "id": "labwired.plan", - "name": "Plan", - "icon": "media/labwired-mark.svg" } ], "labwiredPanel": [ @@ -110,77 +80,392 @@ ] }, "commands": [ - { "command": "labwired.openOverview", "title": "Open Overview (Display / Topology)", "category": "LabWired", "icon": "$(dashboard)" }, - { "command": "labwired.pullTwinDisplay", "title": "Pull Twin Display → Overview", "category": "LabWired", "icon": "$(device-camera-video)" }, - { "command": "labwired.openChat", "title": "Open Agent Chat", "category": "LabWired" }, - { "command": "labwired.openChatInEditor", "title": "New Chat in Editor", "category": "LabWired", "icon": "media/labwired-mark.svg" }, - { "command": "labwired.openConversationFromHistory", "title": "Open Conversation", "category": "LabWired" }, - { "command": "labwired.refreshHistory", "title": "Refresh Conversation History", "category": "LabWired", "icon": "$(refresh)" }, - { "command": "labwired.moveChatToSecondarySidebar", "title": "Arrange Layout (Chat → Secondary Side Bar)", "category": "LabWired" }, - { "command": "labwired.showBuildInfo", "title": "Show Build Info", "category": "LabWired" }, - { "command": "labwired.openSchematics", "title": "Open Schematic…", "category": "LabWired", "icon": "$(circuit-board)" }, - { "command": "labwired.openSchematicFile", "title": "Open Schematic File…", "category": "LabWired" }, - { "command": "labwired.switchToPlan", "title": "Switch to Plan Mode", "category": "LabWired" }, - { "command": "labwired.switchToAct", "title": "Switch to Act Mode", "category": "LabWired" }, - { "command": "labwired.switchToDebug", "title": "Switch to Debug Mode", "category": "LabWired" }, - { "command": "labwired.switchToVerify", "title": "Switch to Verify Mode", "category": "LabWired" }, - { "command": "labwired.showSidebar", "title": "Show LabWired Sidebar", "category": "LabWired" }, - { "command": "labwired.hideSidebar", "title": "Hide LabWired Sidebar", "category": "LabWired" }, - { "command": "labwired.showMonitor", "title": "Show Monitor", "category": "LabWired" }, - { "command": "labwired.hideMonitor", "title": "Hide Monitor", "category": "LabWired" }, - { "command": "labwired.showTerminal", "title": "Show Integrated Terminal", "category": "LabWired" }, - { "command": "labwired.switchMode", "title": "Switch Mode (Plan/Act/Debug/Verify)", "category": "LabWired" }, - { "command": "labwired.newTab", "title": "New Chat Tab", "category": "LabWired" }, - { "command": "labwired.closeTab", "title": "Close Active Chat Tab", "category": "LabWired" }, - { "command": "labwired.restartCli", "title": "Restart CLI Bridge", "category": "LabWired" }, - { "command": "labwired.switchModel", "title": "Switch Model", "category": "LabWired" }, - { "command": "labwired.stopGeneration", "title": "Stop Generation", "category": "LabWired" }, - { "command": "labwired.installCli", "title": "Install LabWired CLI", "category": "LabWired" }, - { "command": "labwired.clearConversation", "title": "Clear Conversation", "category": "LabWired" }, - { "command": "labwired.compressConversation", "title": "Compress Conversation", "category": "LabWired" }, - { "command": "labwired.viewUsage", "title": "View Usage & Billing", "category": "LabWired" }, - { "command": "labwired.viewHistory", "title": "View Conversation History", "category": "LabWired" }, - { "command": "labwired.switchTeam", "title": "Switch Team", "category": "LabWired" }, - { "command": "labwired.switchProject", "title": "Switch Project", "category": "LabWired" }, - { "command": "labwired.undoLastMessage", "title": "Undo Last Message", "category": "LabWired" }, - { "command": "labwired.rewind", "title": "Rewind Checkpoint…", "category": "LabWired" }, - { "command": "labwired.openConsole", "title": "Open Web Console", "category": "LabWired" }, - { "command": "labwired.openLogs", "title": "Open CLI Logs", "category": "LabWired" }, - { "command": "labwired.showCliProcessOutput", "title": "Show CLI Process Output", "category": "LabWired" }, - { "command": "labwired.showStartupProfile", "title": "Show Startup Profile", "category": "LabWired" }, - { "command": "labwired.openSerial", "title": "Open Monitor", "category": "LabWired" }, - { "command": "labwired.openSerialInEditor", "title": "Open Monitor in Editor", "category": "LabWired" }, - { "command": "labwired.toggleSerial", "title": "Toggle Monitor", "category": "LabWired" }, - { "command": "labwired.openPlot", "title": "Open Plot", "category": "LabWired" }, - { "command": "labwired.openEvidence", "title": "Open Evidence", "category": "LabWired" }, - { "command": "labwired.openPlan", "title": "Open Plan Panel", "category": "LabWired" }, - { "command": "labwired.loadEvidence", "title": "Load Verify JSON…", "category": "LabWired" }, - { "command": "labwired.startAgent", "title": "Start Agent (Terminal)", "category": "LabWired" }, - { "command": "labwired.doctor", "title": "Run Doctor", "category": "LabWired" }, - { "command": "labwired.smoke", "title": "Run Smoke", "category": "LabWired" }, - { "command": "labwired.restartBridge", "title": "Refresh CLI Bridge", "category": "LabWired" }, - { "command": "labwired.replayOnboarding", "title": "Replay Onboarding", "category": "LabWired" }, - { "command": "labwired.showGettingStarted", "title": "Open Getting Started", "category": "LabWired" }, - { "command": "labwired.openMcpDocs", "title": "MCP / BYO Agent Setup", "category": "LabWired" }, - { "command": "labwired.githubDaemon", "title": "GitHub Daemon (docs)", "category": "LabWired" }, - { "command": "labwired.login", "title": "Log in (Pro)", "category": "LabWired" }, - { "command": "labwired.diffApproveDemo", "title": "Demo Diff Approval", "category": "LabWired" }, - { "command": "labwired.runTool", "title": "Run Tool…", "category": "LabWired" }, - { "command": "labwired.logout", "title": "Log out", "category": "LabWired" }, - { "command": "labwired.billingStatus", "title": "Billing Status", "category": "LabWired" }, - { "command": "labwired.datasheetExtract", "title": "Extract Datasheets", "category": "LabWired" }, - { "command": "labwired.debugInfo", "title": "Debug Probe Info", "category": "LabWired" }, - { "command": "labwired.startGithubDaemon", "title": "Start GitHub Daemon…", "category": "LabWired" } + { + "command": "labwired.newBoard", + "title": "New board… (from catalog)", + "category": "LabWired", + "icon": "$(circuit-board)" + }, + { + "command": "labwired.importCircuit", + "title": "Import circuit… (PDF, KiCad, netlist, BOM, image, …)", + "category": "LabWired", + "icon": "$(cloud-download)" + }, + { + "command": "labwired.importPdf", + "title": "Import circuit… (alias)", + "category": "LabWired", + "icon": "$(file-pdf)" + }, + { + "command": "labwired.openBoardDiagram", + "title": "Open local diagram… (advanced)", + "category": "LabWired", + "icon": "$(circuit-board)" + }, + { + "command": "labwired.remintTwin", + "title": "Re-mint twin from .labwired/diagram source", + "category": "LabWired" + }, + { + "command": "labwired.openOverview", + "title": "Open Overview (Display / Topology)", + "category": "LabWired", + "icon": "$(dashboard)" + }, + { + "command": "labwired.pullTwinDisplay", + "title": "Pull Twin Display → Overview", + "category": "LabWired", + "icon": "$(device-camera-video)" + }, + { + "command": "labwired.openChat", + "title": "Open Agent Chat", + "category": "LabWired" + }, + { + "command": "labwired.openChatInEditor", + "title": "New Chat in Editor", + "category": "LabWired", + "icon": "media/labwired-mark.svg" + }, + { + "command": "labwired.openConversationFromHistory", + "title": "Open Conversation", + "category": "LabWired" + }, + { + "command": "labwired.refreshHistory", + "title": "Refresh Conversation History", + "category": "LabWired", + "icon": "$(refresh)" + }, + { + "command": "labwired.moveChatToSecondarySidebar", + "title": "Arrange Layout (Chat → Secondary Side Bar)", + "category": "LabWired" + }, + { + "command": "labwired.showBuildInfo", + "title": "Show Build Info", + "category": "LabWired" + }, + { + "command": "labwired.openSchematics", + "title": "Open Schematic…", + "category": "LabWired", + "icon": "$(circuit-board)" + }, + { + "command": "labwired.openSchematicFile", + "title": "Open Schematic File…", + "category": "LabWired" + }, + { + "command": "labwired.switchToPlan", + "title": "Switch to Plan Mode", + "category": "LabWired" + }, + { + "command": "labwired.switchToAct", + "title": "Switch to Act Mode", + "category": "LabWired" + }, + { + "command": "labwired.switchToDebug", + "title": "Switch to Debug Mode", + "category": "LabWired" + }, + { + "command": "labwired.switchToVerify", + "title": "Switch to Verify Mode", + "category": "LabWired" + }, + { + "command": "labwired.showSidebar", + "title": "Show LabWired Sidebar", + "category": "LabWired" + }, + { + "command": "labwired.hideSidebar", + "title": "Hide LabWired Sidebar", + "category": "LabWired" + }, + { + "command": "labwired.showMonitor", + "title": "Show Monitor", + "category": "LabWired" + }, + { + "command": "labwired.hideMonitor", + "title": "Hide Monitor", + "category": "LabWired" + }, + { + "command": "labwired.showTerminal", + "title": "Show Integrated Terminal", + "category": "LabWired" + }, + { + "command": "labwired.switchMode", + "title": "Switch Mode (Plan/Act/Debug/Verify)", + "category": "LabWired" + }, + { + "command": "labwired.newTab", + "title": "New Chat Tab", + "category": "LabWired" + }, + { + "command": "labwired.closeTab", + "title": "Close Active Chat Tab", + "category": "LabWired" + }, + { + "command": "labwired.restartCli", + "title": "Restart CLI Bridge", + "category": "LabWired" + }, + { + "command": "labwired.switchModel", + "title": "Switch Model", + "category": "LabWired" + }, + { + "command": "labwired.stopGeneration", + "title": "Stop Generation", + "category": "LabWired" + }, + { + "command": "labwired.installCli", + "title": "Install LabWired CLI", + "category": "LabWired" + }, + { + "command": "labwired.clearConversation", + "title": "Clear Conversation", + "category": "LabWired" + }, + { + "command": "labwired.compressConversation", + "title": "Compress Conversation", + "category": "LabWired" + }, + { + "command": "labwired.viewUsage", + "title": "View Usage & Billing", + "category": "LabWired" + }, + { + "command": "labwired.viewHistory", + "title": "View Conversation History", + "category": "LabWired" + }, + { + "command": "labwired.switchTeam", + "title": "Switch Team", + "category": "LabWired" + }, + { + "command": "labwired.switchProject", + "title": "Switch Project", + "category": "LabWired" + }, + { + "command": "labwired.undoLastMessage", + "title": "Undo Last Message", + "category": "LabWired" + }, + { + "command": "labwired.rewind", + "title": "Rewind Checkpoint…", + "category": "LabWired" + }, + { + "command": "labwired.openConsole", + "title": "Open Web Console", + "category": "LabWired" + }, + { + "command": "labwired.openLogs", + "title": "Open CLI Logs", + "category": "LabWired" + }, + { + "command": "labwired.showCliProcessOutput", + "title": "Show CLI Process Output", + "category": "LabWired" + }, + { + "command": "labwired.showStartupProfile", + "title": "Show Startup Profile", + "category": "LabWired" + }, + { + "command": "labwired.openSerial", + "title": "Open Monitor", + "category": "LabWired" + }, + { + "command": "labwired.openSerialInEditor", + "title": "Open Monitor in Editor", + "category": "LabWired" + }, + { + "command": "labwired.toggleSerial", + "title": "Toggle Monitor", + "category": "LabWired" + }, + { + "command": "labwired.openPlot", + "title": "Open Plot (composed glass)", + "category": "LabWired" + }, + { + "command": "labwired.openComposedPlot", + "title": "Open Composed Plot JSON…", + "category": "LabWired", + "icon": "$(graph)" + }, + { + "command": "labwired.openEvidence", + "title": "Open Evidence", + "category": "LabWired" + }, + { + "command": "labwired.openPlan", + "title": "Open Plan Panel", + "category": "LabWired" + }, + { + "command": "labwired.loadEvidence", + "title": "Load Verify JSON…", + "category": "LabWired" + }, + { + "command": "labwired.startAgent", + "title": "Start Agent (Terminal)", + "category": "LabWired" + }, + { + "command": "labwired.doctor", + "title": "Run Doctor", + "category": "LabWired" + }, + { + "command": "labwired.smoke", + "title": "Run Smoke", + "category": "LabWired" + }, + { + "command": "labwired.restartBridge", + "title": "Refresh CLI Bridge", + "category": "LabWired" + }, + { + "command": "labwired.replayOnboarding", + "title": "Replay Onboarding", + "category": "LabWired" + }, + { + "command": "labwired.showGettingStarted", + "title": "Open Getting Started", + "category": "LabWired" + }, + { + "command": "labwired.openMcpDocs", + "title": "MCP / BYO Agent Setup", + "category": "LabWired" + }, + { + "command": "labwired.githubDaemon", + "title": "GitHub Daemon (docs)", + "category": "LabWired" + }, + { + "command": "labwired.login", + "title": "Log in (Pro)", + "category": "LabWired" + }, + { + "command": "labwired.diffApproveDemo", + "title": "Demo Diff Approval", + "category": "LabWired" + }, + { + "command": "labwired.runTool", + "title": "Run Tool…", + "category": "LabWired" + }, + { + "command": "labwired.logout", + "title": "Log out", + "category": "LabWired" + }, + { + "command": "labwired.billingStatus", + "title": "Billing Status", + "category": "LabWired" + }, + { + "command": "labwired.datasheetExtract", + "title": "Extract Datasheets", + "category": "LabWired" + }, + { + "command": "labwired.debugInfo", + "title": "Debug Probe Info (desk HW)", + "category": "LabWired" + }, + { + "command": "labwired.runOnTwin", + "title": "Run on digital twin", + "category": "LabWired", + "icon": "$(play)" + }, + { + "command": "labwired.proveOnTwin", + "title": "Prove on digital twin (verify)", + "category": "LabWired", + "icon": "$(verified)" + }, + { + "command": "labwired.debugOnTwin", + "title": "Debug on digital twin", + "category": "LabWired", + "icon": "$(debug-alt)" + }, + { + "command": "labwired.runTwin", + "title": "Run twin (alias)", + "category": "LabWired" + }, + { + "command": "labwired.startGithubDaemon", + "title": "Start GitHub Daemon…", + "category": "LabWired" + } ], "customEditors": [ { "viewType": "labwired.schematicEditor", "displayName": "LabWired Schematic Viewer", "selector": [ - { "filenamePattern": "*.kicad_sch" }, - { "filenamePattern": "*.kicad_pcb" }, - { "filenamePattern": "*.kicad_pro" }, - { "filenamePattern": "*.sch" } + { + "filenamePattern": "*.kicad_sch" + }, + { + "filenamePattern": "*.kicad_pcb" + }, + { + "filenamePattern": "*.kicad_pro" + }, + { + "filenamePattern": "*.sch" + } ], "priority": "default" } @@ -203,16 +488,6 @@ "when": "view == labwired.chat", "group": "navigation@2" }, - { - "command": "labwired.refreshHistory", - "when": "view == labwired.history", - "group": "navigation" - }, - { - "command": "labwired.loadEvidence", - "when": "view == labwired.evidence", - "group": "navigation" - }, { "command": "labwired.openSchematics", "when": "view == labwired.serial", @@ -237,43 +512,61 @@ "id": "installCli", "title": "Install the LabWired CLI", "description": "Workbench is chrome; the brain is `labwired` (OpenCode + packs + MCP).\n\n[Install CLI](command:labwired.installCli)", - "media": { "image": "media/labwired-mark.svg", "altText": "LabWired" }, - "completionEvents": ["onCommand:labwired.installCli"] + "media": { + "image": "media/labwired-mark.svg", + "altText": "LabWired" + }, + "completionEvents": [ + "onCommand:labwired.installCli" + ] }, { "id": "login", "title": "Log in (hosted MCP + model)", "description": "Same device-code as `labwired login`. Session → `~/.labwired/session/cloud.json`.\n\n[Log in](command:labwired.login)", - "media": { "image": "media/labwired-mark.svg", "altText": "LabWired" }, - "completionEvents": ["onCommand:labwired.login"] + "media": { + "image": "media/labwired-mark.svg", + "altText": "LabWired" + }, + "completionEvents": [ + "onCommand:labwired.login" + ] }, { "id": "doctor", "title": "Run doctor", "description": "Confirm CLI, packs (golden-path · bringup · prove · observe · desk-hw), and tools.\n\n[Run Doctor](command:labwired.doctor)", - "media": { "image": "media/labwired-mark.svg", "altText": "LabWired" }, - "completionEvents": ["onCommand:labwired.doctor"] + "media": { + "image": "media/labwired-mark.svg", + "altText": "LabWired" + }, + "completionEvents": [ + "onCommand:labwired.doctor" + ] }, { "id": "startAgent", "title": "Start Agent (OpenCode)", "description": "Opens a terminal and runs bare `labwired` — prepare skills + golden-path + shared labwired_* tools.\n\n[Start Agent](command:labwired.startAgent)", - "media": { "image": "media/labwired-mark.svg", "altText": "LabWired" }, - "completionEvents": ["onCommand:labwired.startAgent"] - }, - { - "id": "overview", - "title": "Visual overview (like Playground glass)", - "description": "Board topology, OLED-style display, serial strip, element series, and twin evidence — same story as LabWired UI.\n\n[Open Overview](command:labwired.openOverview)", - "media": { "image": "media/labwired-mark.svg", "altText": "LabWired" }, - "completionEvents": ["onCommand:labwired.openOverview"] + "media": { + "image": "media/labwired-mark.svg", + "altText": "LabWired" + }, + "completionEvents": [ + "onCommand:labwired.startAgent" + ] }, { "id": "prove", "title": "Blink & prove on the twin", - "description": "In the agent: *Blink the LED and prove it on the twin.* Green only after `labwired_verify` → model_verified. Optional: compose LED vs UART from elements.\n\n[Open Evidence](command:labwired.openEvidence)", - "media": { "image": "media/labwired-mark.svg", "altText": "LabWired" }, - "completionEvents": ["onCommand:labwired.openEvidence"] + "description": "In the Agent chat: *Blink the LED and prove it on the twin.* Green only after twin verify — LabWired’s wedge vs plain agent UIs.\n\n[Open Agent](command:labwired.openChat)", + "media": { + "image": "media/labwired-mark.svg", + "altText": "LabWired" + }, + "completionEvents": [ + "onCommand:labwired.openChat" + ] } ] } @@ -295,12 +588,17 @@ "labwired.autoRevealOnStartup": { "type": "boolean", "default": true, - "description": "Reveal LabWired views on activation." + "description": "Reveal LabWired Agent view on activation." + }, + "labwired.inPanelLlm": { + "type": "boolean", + "default": false, + "description": "When true, freeform chat uses in-panel LLM (OpenCode/Ollama). Default false: freeform starts the CLI agent terminal (recommended — Embedder-like single path)." }, "labwired.showSerial": { "type": "boolean", - "default": true, - "description": "Show the Monitor panel." + "default": false, + "description": "Show the Monitor panel on activation (default off — open via LabWired: Open Monitor)." }, "labwired.showReasoningSummaries": { "type": "boolean", @@ -324,7 +622,13 @@ }, "labwired.logLevel": { "type": "string", - "enum": ["error", "warn", "info", "debug", "trace"], + "enum": [ + "error", + "warn", + "info", + "debug", + "trace" + ], "default": "info", "description": "Extension log verbosity (Output → LabWired)." }, @@ -372,6 +676,11 @@ "type": "string", "default": "", "description": "Active project name/id for Pro." + }, + "labwired.autoStartRpc": { + "type": "boolean", + "default": false, + "description": "Start labwired --server RPC on activation (off by default; freezes less, CLI agent is primary)." } } } @@ -379,12 +688,23 @@ "scripts": { "compile": "tsc -p .", "watch": "tsc -p . -w", - "package": "npm run compile && npx --yes @vscode/vsce package --no-dependencies -o labwired-vscode.vsix", - "vscode:prepublish": "npm run compile" + "package": "npm run compile && npx --yes @vscode/vsce package --no-dependencies -o labwired-vscode.vsix && npm run check:vsix", + "vscode:prepublish": "npm run compile", + "check:vsix": "python3 scripts/check-vsix-integrity.py", + "check:cli": "npm run compile && node scripts/check-cli-contract.mjs && node scripts/check-board-mint.mjs && node scripts/check-import-e2e.mjs && node scripts/check-composed-plot.mjs", + "check": "npm run check:cli && npm run package", + "test:ship": "npm run compile && node ./out/test/runTest.js", + "check:mint": "npm run compile && node scripts/check-board-mint.mjs", + "check:import": "npm run compile && node scripts/check-import-e2e.mjs" }, "devDependencies": { + "@types/glob": "^8.1.0", + "@types/mocha": "^10.0.10", "@types/node": "^20.17.0", "@types/vscode": "^1.90.0", + "@vscode/test-electron": "^3.1.0", + "glob": "^13.0.6", + "mocha": "^11.8.0", "typescript": "^5.9.3" } } diff --git a/extensions/labwired-vscode/src/extension.ts b/extensions/labwired-vscode/src/extension.ts index 8be7cb5..a35098c 100644 --- a/extensions/labwired-vscode/src/extension.ts +++ b/extensions/labwired-vscode/src/extension.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; import * as path from "path"; +import * as fs from "fs"; import { LabWiredBridge } from "./cli/bridge"; import { ConversationStore } from "./services/conversationStore"; import { SessionState } from "./services/sessionState"; @@ -21,6 +22,16 @@ import { RpcClient, resolveAgentRoot } from "./cli/rpcClient"; import { DatasheetService } from "./datasheet/agentic"; import { ProbeDebugService } from "./debug/probeGdb"; import { BillingService } from "./pro/billing"; +import { mintFromDiagramObject, mintFromFile } from "./board/boardMint"; +import { + buildStarterDiagram, + loadCatalogBoards, + type StarterPreset, +} from "./board/catalogBoards"; +import { + importCircuitSource, + type ImportSourceKind, +} from "./board/multiImport"; export function activate(context: vscode.ExtensionContext): void { const output = vscode.window.createOutputChannel("LabWired"); @@ -96,38 +107,51 @@ export function activate(context: vscode.ExtensionContext): void { webviewOptions: { retainContextWhenHidden: true }, }); - // Start Embedder-style JSON-RPC server (labwired server / rpc-server.mjs) - const ws = - vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd(); - void rpc.start(ws).then( - () => { - output.appendLine("RPC server ready (Embedder --server clone)."); - }, - (e) => { - output.appendLine(`RPC server failed (using local agent fallback): ${e}`); - } - ); + // Optional RPC server — do NOT block activation. Only start if setting enabled. + const startRpc = vscode.workspace + .getConfiguration("labwired") + .get("autoStartRpc"); + if (startRpc) { + const ws = + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd(); + void rpc.start(ws).then( + () => { + output.appendLine("RPC server ready."); + }, + (e) => { + output.appendLine(`RPC server failed: ${e}`); + } + ); + } else { + output.appendLine( + "RPC auto-start off (labwired.autoStartRpc). CLI agent path is primary." + ); + } + // Chat-first IA: Agent (sidebar) + Monitor/Plot (panel on demand). + // Overview / Evidence / etc. stay as command-opened surfaces. + // Plot = thin glass over agent-composed elements (E4), not a ready-made plot product. context.subscriptions.push( output, { dispose: () => serial.dispose() }, { dispose: () => agent.stop() }, { dispose: () => void rpc.stop() }, { dispose: () => probeDebug.dispose() }, - regView(OverviewViewProvider.viewType, overview), regView(ChatViewProvider.viewType, chat), regView(SerialViewProvider.viewType, serial), - regView(EvidenceViewProvider.viewType, evidence), - regView(HistoryViewProvider.viewType, history), - regView(PlanViewProvider.viewType, plan), regView(PlotViewProvider.viewType, plot), - regView(CatalogViewProvider.viewType, catalogView), vscode.window.registerCustomEditorProvider( SchematicEditorProvider.viewType, schematic, { webviewOptions: { retainContextWhenHidden: true } } ) ); + // Keep instances alive for commands (overview editor, twin evidence, history pick) + void overview; + void evidence; + void history; + void plan; + void catalogView; const focus = async (viewId: string) => { await vscode.commands.executeCommand("workbench.view.extension.labwired"); @@ -138,12 +162,409 @@ export function activate(context: vscode.ExtensionContext): void { } }; + const finishMint = async ( + result: import("./board/boardMint").BoardMintResult + ) => { + store.append("tool", `✓ board\n${result.summary}`); + if (result.errors.length) { + store.append("system", result.errors.map((e) => `⚠ ${e}`).join("\n")); + } + if (result.dropped.length) { + store.append( + "system", + `Dropped from twin: ${result.dropped.map((d) => d.type).join(", ")}` + ); + } + overview.setEvidence({ + status: result.ok ? "twin_ready" : "empty", + path: result.diagramPath, + summary: result.summary, + }); + chat.refresh(); + await focus("labwired.chat"); + const next = await vscode.window.showInformationMessage( + result.ok + ? `Board ready · ${result.board} · ${result.supported.length} parts` + : `Mint incomplete · ${result.errors[0] || "no supported parts"}`, + result.ok ? "Start agent" : "OK", + "Open coverage" + ); + if (next === "Start agent") { + await bridge.startAgentTerminal(session.getMode()); + } else if (next === "Open coverage") { + const doc = await vscode.workspace.openTextDocument(result.coveragePath); + await vscode.window.showTextDocument(doc, { preview: true }); + } + }; + + /** Embedder-style: pick board from OUR catalog → mint twin. */ + const runNewBoardFromCatalog = async () => { + const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!root) { + void vscode.window.showErrorMessage( + "Open a workspace folder first — LabWired writes .labwired/ there." + ); + return; + } + const boards = loadCatalogBoards(context.extensionPath); + const boardPick = await vscode.window.showQuickPick( + boards.map((b) => ({ + label: b.id, + description: b.chip !== b.id ? b.chip : undefined, + detail: `MCU part ${b.mcuType} · blink pin ${b.ledPin}`, + board: b, + })), + { + title: "New board — LabWired twin catalog", + placeHolder: "Pick a board / MCU (product catalog, not a file hunt)", + matchOnDescription: true, + matchOnDetail: true, + } + ); + if (!boardPick) return; + + const presetPick = await vscode.window.showQuickPick( + [ + { + label: "Blink LED starter", + description: "MCU + LED wired (recommended)", + preset: "blink" as StarterPreset, + }, + { + label: "Bare MCU", + description: "Board only — no peripherals", + preset: "bare" as StarterPreset, + }, + ], + { title: `Starter for ${boardPick.board.id}` } + ); + if (!presetPick) return; + + try { + const diagram = buildStarterDiagram(boardPick.board, presetPick.preset); + // Persist starter as source for remint + const lab = path.join(root, ".labwired"); + fs.mkdirSync(lab, { recursive: true }); + const sourcePath = path.join(lab, "source-diagram.json"); + fs.writeFileSync(sourcePath, JSON.stringify(diagram, null, 2) + "\n"); + const result = mintFromDiagramObject( + diagram, + root, + catalog.asMintLookup(), + `catalog:${boardPick.board.id}` + ); + await finishMint(result); + } catch (e) { + void vscode.window.showErrorMessage(`New board failed: ${e}`); + store.append("system", `New board failed: ${e}`); + } + }; + + const runBoardMint = async (sourcePath?: string) => { + const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!root) { + void vscode.window.showErrorMessage( + "Open a workspace folder to mint a twin from a board diagram." + ); + return; + } + let src = sourcePath; + if (!src) { + const pick = await vscode.window.showOpenDialog({ + canSelectMany: false, + filters: { Diagram: ["json"] }, + title: "Open local diagram.json (advanced)", + defaultUri: vscode.Uri.file(root), + }); + if (!pick?.[0]) return; + src = pick[0].fsPath; + } + try { + try { + const copyTo = path.join(root, ".labwired", "source-diagram.json"); + fs.mkdirSync(path.dirname(copyTo), { recursive: true }); + if (path.resolve(src) !== path.resolve(copyTo)) { + fs.copyFileSync(src, copyTo); + } + } catch { + /* ignore */ + } + const result = mintFromFile(src, root, catalog.asMintLookup()); + await finishMint(result); + } catch (e) { + void vscode.window.showErrorMessage(`Board mint failed: ${e}`); + store.append("system", `Board mint failed: ${e}`); + } + }; + + const runImportCircuit = async () => { + const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!root) { + void vscode.window.showErrorMessage( + "Open a workspace folder first — import writes under .labwired/." + ); + return; + } + + const sourcePick = await vscode.window.showQuickPick( + [ + { + label: "$(file-pdf) PDF schematic", + description: "Customer board drawing", + forceKind: "pdf-schematic" as ImportSourceKind, + }, + { + label: "$(book) Datasheet PDF", + description: "Part knowledge for agent", + forceKind: "pdf-datasheet" as ImportSourceKind, + }, + { + label: "$(circuit-board) KiCad schematic", + description: ".kicad_sch", + forceKind: "kicad-sch" as ImportSourceKind, + }, + { + label: "$(file-binary) Netlist", + description: ".net / spice-like", + forceKind: "netlist" as ImportSourceKind, + }, + { + label: "$(json) LabWired diagram.json", + description: "Mint twin directly", + forceKind: "diagram-json" as ImportSourceKind, + }, + { + label: "$(file-media) Image", + description: "Photo/scan of schematic", + forceKind: "image" as ImportSourceKind, + }, + { + label: "$(table) BOM CSV", + description: "Parts list", + forceKind: "bom-csv" as ImportSourceKind, + }, + { + label: "$(file-text) Text / notes", + description: ".txt / .md description", + forceKind: "text" as ImportSourceKind, + }, + { + label: "$(code) Existing firmware / project", + description: "platformio.ini, main.c/cpp, sdkconfig — on-ramp to twin + FW loop", + forceKind: "text" as ImportSourceKind, + }, + { + label: "$(file) Any file (auto-detect)", + description: "Guess from extension", + forceKind: undefined, + }, + ], + { title: "On-ramp to twin + firmware loop (source format is incidental)" } + ); + if (!sourcePick) return; + + const filters: Record = { + "pdf-schematic": ["pdf"], + "pdf-datasheet": ["pdf"], + "kicad-sch": ["kicad_sch", "kicad_pro", "sch"], + "kicad-pcb": ["kicad_pcb", "kicad_pro"], + netlist: ["net", "cir", "sp", "txt"], + "diagram-json": ["json"], + image: ["png", "jpg", "jpeg", "webp", "gif", "tif", "tiff"], + "bom-csv": ["csv", "tsv", "txt"], + text: ["txt", "md", "log"], + }; + const fk = sourcePick.forceKind; + const files = await vscode.window.showOpenDialog({ + canSelectMany: false, + filters: fk + ? { Import: filters[fk] || ["*"] } + : { + "Circuit sources": [ + "pdf", + "kicad_sch", + "kicad_pcb", + "net", + "json", + "png", + "jpg", + "csv", + "txt", + "md", + ], + }, + title: `Import: ${sourcePick.label.replace(/\$\([^)]+\)\s*/, "")}`, + }); + if (!files?.[0]) return; + + // User context is first-class: board, goals, and (for code) where the FW lives. + const userContext = + (await vscode.window.showInputBox({ + title: "User context (optional but recommended)", + prompt: + "Board/MCU, goal (blink + prove), sensors, pins, or “this is existing FW for …”", + placeHolder: + "e.g. ESP32-C3 SuperMini, GPIO8 LED, existing PlatformIO project — prove on twin", + ignoreFocusOut: true, + })) || ""; + + const boards = loadCatalogBoards(context.extensionPath); + let partTypes: string[] = []; + try { + const facts = catalog.load(); + partTypes = [ + ...facts.device_types, + ...facts.parts.map((p) => p.type), + ...facts.chips, + ]; + } catch { + partTypes = ["led", "button", "bme280", "ssd1306", "resistor"]; + } + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "LabWired: importing circuit…", + }, + async () => { + const result = importCircuitSource({ + sourcePath: files[0].fsPath, + workspaceRoot: root, + boards, + partTypes, + lookup: catalog.asMintLookup(), + forceKind: sourcePick.forceKind, + autoMintStarter: true, + starterPreset: "blink", + userContext, + }); + + if (!result.ok) { + void vscode.window.showErrorMessage(result.summary); + store.append("system", result.summary); + await focus("labwired.chat"); + return; + } + + store.append("tool", `✓ import (${result.sourceKind})\n${result.summary}`); + await focus("labwired.chat"); + + if (result.sourceKind === "pdf-datasheet") { + try { + datasheets.ensureDirs(); + datasheets.extractAll(true); + store.append("system", "Datasheet extracted for /datasheet tools."); + } catch (e) { + store.append("system", `Datasheet extract note: ${e}`); + } + void vscode.window.showInformationMessage( + "Datasheet imported — agent can use datasheet tools." + ); + return; + } + + if (result.minted?.ok) { + await finishMint(result.minted); + store.append( + "system", + "Twin minted. Design context also in .labwired/import/ (DESIGN_CONTEXT.md, AGENT_PROMPT.md)." + ); + return; + } + + // Twin optional — design context is still success + store.append( + "system", + "Twin not fully buildable (or not minted). Design context kept for drivers/FW — " + + ".labwired/import/DESIGN_CONTEXT.md + USER_CONTEXT.md + extracts." + ); + + const next = await vscode.window.showInformationMessage( + result.suggestedBoardId + ? `Imported · context ready · suggested ${result.suggestedBoardId}` + : "Imported · design context ready (twin incomplete)", + "Start agent (design / twin)", + "New board…", + "OK" + ); + if (next === "Start agent (design / twin)") { + store.append( + "system", + "Starting agent with design context (import-circuit). Drivers OK even without full twin." + ); + await bridge.sendPromptViaTerminal( + result.agentPrompt, + session.getMode() + ); + } else if (next === "New board…") { + await runNewBoardFromCatalog(); + } + } + ); + }; + const cmds: [string, (...args: never[]) => unknown][] = [ + [ + "labwired.newBoard", + async () => { + await runNewBoardFromCatalog(); + }, + ], + [ + "labwired.importCircuit", + async () => { + await runImportCircuit(); + }, + ], + [ + "labwired.importPdf", + async () => { + // Back-compat: same multi-import, default PDF filter via auto + await runImportCircuit(); + }, + ], + [ + "labwired.openBoardDiagram", + async () => { + await runBoardMint(); + }, + ], + [ + "labwired.remintTwin", + async () => { + const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!root) return; + const candidates = [ + path.join(root, ".labwired", "source-diagram.json"), + path.join(root, "diagram.json"), + path.join(root, ".labwired", "diagram.json"), + ]; + // Prefer original source recorded in board.json + try { + const meta = JSON.parse( + fs.readFileSync(path.join(root, ".labwired", "board.json"), "utf8") + ) as { source?: string }; + if (meta.source && fs.existsSync(meta.source)) { + await runBoardMint(meta.source); + return; + } + } catch { + /* */ + } + const hit = candidates.find((c) => fs.existsSync(c)); + if (!hit) { + void vscode.window.showWarningMessage( + "No diagram found — use Open board diagram…" + ); + return; + } + await runBoardMint(hit); + }, + ], [ "labwired.openOverview", async () => { overview.openInEditor(); - await focus("labwired.overview"); }, ], [ @@ -152,7 +573,6 @@ export function activate(context: vscode.ExtensionContext): void { overview.openInEditor(); const ok = await overview.pullFromTwinInspect(); if (!ok) await overview.pullFromWorkspaceFiles(); - await focus("labwired.overview"); }, ], ["labwired.openChat", async () => focus("labwired.chat")], @@ -352,7 +772,14 @@ export function activate(context: vscode.ExtensionContext): void { await billing.openBilling(); }, ], - ["labwired.viewHistory", async () => focus("labwired.history")], + [ + "labwired.viewHistory", + async () => { + await vscode.commands.executeCommand( + "labwired.openConversationFromHistory" + ); + }, + ], [ "labwired.switchTeam", async () => { @@ -445,31 +872,135 @@ export function activate(context: vscode.ExtensionContext): void { [ "labwired.openPlot", async () => { - await vscode.commands.executeCommand("labwired.plot.focus"); + await plot.reveal(); + }, + ], + [ + "labwired.openComposedPlot", + async () => { + await plot.openComposedFile(); + }, + ], + [ + "labwired.openEvidence", + async () => { + await vscode.commands.executeCommand("labwired.loadEvidence"); + }, + ], + [ + "labwired.openPlan", + async () => { + const root = vscode.workspace.workspaceFolders?.[0]?.uri; + if (!root) { + void vscode.window.showInformationMessage( + "Open a workspace to use Plan." + ); + return; + } + const planUri = vscode.Uri.joinPath(root, ".labwired", "plan.md"); + try { + await vscode.workspace.fs.stat(planUri); + } catch { + await vscode.workspace.fs.createDirectory( + vscode.Uri.joinPath(root, ".labwired") + ); + await vscode.workspace.fs.writeFile( + planUri, + Buffer.from("# LabWired plan\n\n", "utf8") + ); + } + const doc = await vscode.workspace.openTextDocument(planUri); + await vscode.window.showTextDocument(doc, { preview: false }); }, ], - ["labwired.openEvidence", async () => focus("labwired.evidence")], - ["labwired.openPlan", async () => focus("labwired.plan")], [ "labwired.runTwin", async () => { - await focus("labwired.evidence"); + await vscode.commands.executeCommand("labwired.runOnTwin"); + }, + ], + [ + "labwired.runOnTwin", + async () => { + await focus("labwired.chat"); + const { runOnTwin, formatTwinResultForChat } = await import( + "./twin/twinSession" + ); const r = await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, - title: "LabWired twin/run…", + title: "LabWired: run on digital twin…", + cancellable: false, }, - async () => evidence.runTwin("smoke") + () => runOnTwin() ); - if (r) { - void vscode.window.showInformationMessage( - r.ok || r.twin_verified - ? `Twin OK · ${r.runId}` - : `Twin failed · ${r.runId || "?"}` - ); - } else { - void vscode.window.showWarningMessage("twin/run failed"); + store.append("tool", formatTwinResultForChat(r)); + if (r.snapshot_id) { + void overview?.setEvidence?.({ + status: r.ok ? "twin_ran" : "failed", + summary: r.summary, + }); } + void vscode.window.showInformationMessage( + r.ok + ? `Twin run OK · ${r.board || ""}` + : `Twin run failed — ${r.error || r.summary}`.slice(0, 120) + ); + }, + ], + [ + "labwired.proveOnTwin", + async () => { + await focus("labwired.chat"); + const { proveOnTwin, formatTwinResultForChat } = await import( + "./twin/twinSession" + ); + const r = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "LabWired: prove on digital twin…", + cancellable: false, + }, + () => proveOnTwin() + ); + store.append("tool", formatTwinResultForChat(r)); + await evidence.showTwinResult({ + ok: r.model_verified, + suite: "prove", + twin_verified: r.twin_ran, + model_verified: r.model_verified, + summary: r.summary, + }); + void vscode.window.showInformationMessage( + r.model_verified + ? "model_verified — twin prove green" + : `Prove red — ${r.error || r.summary}`.slice(0, 120) + ); + }, + ], + [ + "labwired.debugOnTwin", + async () => { + await focus("labwired.chat"); + const { debugOnTwin, formatTwinResultForChat } = await import( + "./twin/twinSession" + ); + const r = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "LabWired: debug on digital twin…", + cancellable: false, + }, + () => debugOnTwin() + ); + store.append("tool", formatTwinResultForChat(r)); + void vscode.window.showInformationMessage( + r.ok + ? r.dapStarted + ? "Twin debug session started (F5 DAP)" + : "Twin debug probe done (MCP) — install LabWired Debugger for full F5" + : `Twin debug failed — ${r.error || r.summary}`.slice(0, 120) + ); }, ], [ @@ -616,7 +1147,7 @@ export function activate(context: vscode.ExtensionContext): void { ); if (cap.evidencePath) { await evidence.loadPath(path.join(cap.evidencePath, "result.json")); - await focus("labwired.evidence"); + await focus("labwired.chat"); } } catch (e) { void vscode.window.showErrorMessage(String(e)); @@ -626,12 +1157,18 @@ export function activate(context: vscode.ExtensionContext): void { [ "labwired.loadEvidence", async () => { - await focus("labwired.evidence"); const uris = await vscode.window.showOpenDialog({ canSelectMany: false, filters: { JSON: ["json"] }, + title: "Load twin verify JSON", }); - if (uris?.[0]) await evidence.loadPath(uris[0].fsPath); + if (!uris?.[0]) return; + await evidence.loadPath(uris[0].fsPath); + store.append( + "tool", + `✓ evidence loaded\n${uris[0].fsPath}` + ); + await focus("labwired.chat"); }, ], ["labwired.startAgent", async () => bridge.startAgentTerminal(session.getMode())], @@ -858,24 +1395,12 @@ export function activate(context: vscode.ExtensionContext): void { const cs = catalog.stats(); const cli = bridge.getCli(); - store.append( - "system", - `LabWired workbench v0.6.3 — same start-here as CLI\n` + - `1. Log in (labwired login) → hosted MCP + model\n` + - `2. Doctor → Start Agent (Terminal) → OpenCode + golden-path\n` + - `3. Overview: twin display (inspect) · topology · serial · elements\n` + - `4. “Blink the LED and prove it on the twin.”\n` + - `• Packs: golden-path · bringup · prove · observe · desk-hw\n` + - `• Knowledge: MCP labwired_part / labwired_datasheet\n` + - `• Compose: labwired compose … (elements, not ready-made plots)\n` + - `• CLI: ${cli.path || "(missing)"} (${cli.source}${cli.version ? ` v${cli.version}` : ""})\n` + - `• Catalog: ${cs.parts} parts · tools: /tools` - ); + // Empty-state UI is the onboarding; keep first system line short. + store.clearActive(); output.appendLine( - `LabWired workbench v0.6.3 — tools=${TOOLS.length} catalog=${cs.parts} cli=${cli.path || "missing"}` + `LabWired v0.7.0 chat-first — Embedder + twin · tools=${TOOLS.length} catalog=${cs.parts} cli=${cli.path || "missing"} flavor=${cli.flavor}` ); - // Surface overview on first activation so visual glass matches LabWired UI - void overview.pushState(); + void chat.refresh(); } export function deactivate(): void { diff --git a/extensions/labwired-vscode/src/providers/chatProvider.ts b/extensions/labwired-vscode/src/providers/chatProvider.ts index 4492e0e..1ecf186 100644 --- a/extensions/labwired-vscode/src/providers/chatProvider.ts +++ b/extensions/labwired-vscode/src/providers/chatProvider.ts @@ -7,6 +7,12 @@ import type { ToolRunEvent } from "../tools/runner"; import type { AgentSession } from "../agent/session"; import type { RpcClient } from "../cli/rpcClient"; import type { EvidenceViewProvider } from "./evidenceProvider"; +import { loadCloudSession } from "../cli/cloudSession"; +import { loadBoardMeta } from "../board/boardMint"; +import { + buildWorkspaceContext, + contextHandoffBlock, +} from "../board/workspaceContext"; import { TOOLS } from "../tools/registry"; import { shellHtml, LW_MARK_SVG_LG } from "../webview/theme"; @@ -17,8 +23,11 @@ const MODE_LABEL: Record = { verify: "Verify", }; +const PROVE_EXAMPLE = "Blink the LED and prove it on the twin."; + /** - * Embedder-clone chat + real LabWired tools in-panel. + * Embedder-class chat chrome + LabWired twin wedge. + * Default freeform path = CLI agent terminal (single brain). */ export class ChatViewProvider implements vscode.WebviewViewProvider { public static readonly viewType = "labwired.chat"; @@ -96,8 +105,23 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { params: Record = {} ): Promise { this.store.append("system", `⚙ ${name}…`); - const ev = await this.tools.runNamed(name, params); - this.appendToolEvent(ev); + this.pushState(); + try { + const ev = await this.tools.runNamed(name, params); + this.appendToolEvent(ev); + if (name === "doctor" || name === "doctor_strict") { + const ok = ev.status === "ok"; + void vscode.window.showInformationMessage( + ok + ? `LabWired doctor OK` + : `LabWired doctor failed (exit ${ev.code ?? "?"}) — see Agent chat / Output` + ); + } + } catch (e) { + this.store.append("system", `Tool ${name} crashed: ${e}`); + void vscode.window.showErrorMessage(`LabWired ${name}: ${e}`); + } + this.pushState(); } private appendToolEvent(ev: ToolRunEvent) { @@ -145,13 +169,77 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { case "listTools": this.store.append("tool", this.tools.listCatalog()); break; - case "startAgent": + case "startAgent": { + const wsRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + const ctx = buildWorkspaceContext(wsRoot); + this.store.append( + "system", + `labwired_context · ${ctx.summary}\nnext: ${ctx.next.join(" → ")}` + ); await this.bridge.startAgentTerminal(this.session.getMode()); this.store.append( "system", - `Agent terminal started (${MODE_LABEL[this.session.getMode()]}).` + `Agent started (${MODE_LABEL[this.session.getMode()]}) — CLI terminal. ` + + (ctx.twin_buildable + ? "Twin ready — prefer prove path." + : ctx.design_context_ok + ? "Design context ok — twin optional; do not invent pins." + : "No context yet — New board or Import first.") ); + this.pushState(); + break; + } + case "login": + await vscode.commands.executeCommand("labwired.login"); + this.pushState(); + break; + case "doctor": + await this.invokeTool("doctor"); + this.pushState(); + break; + case "openBoard": + case "newBoard": + await vscode.commands.executeCommand("labwired.newBoard"); + this.pushState(); + break; + case "importPdf": + case "importCircuit": + await vscode.commands.executeCommand("labwired.importCircuit"); + this.pushState(); + break; + case "statusClick": { + const target = String(msg.target || ""); + if (target === "cli") { + const cli = this.bridge.getCli(); + if (!cli.path) await vscode.commands.executeCommand("labwired.installCli"); + else await vscode.commands.executeCommand("labwired.showBuildInfo"); + } else if (target === "session") { + await vscode.commands.executeCommand("labwired.login"); + } else if (target === "mode") { + this.session.cycleMode(); + } else if (target === "twin" || target === "board") { + await vscode.commands.executeCommand("labwired.newBoard"); + } else if (target === "ctx") { + const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + const ctx = buildWorkspaceContext(root); + this.store.append("tool", contextHandoffBlock(ctx)); + if (ctx.mode === "empty") { + await vscode.commands.executeCommand("labwired.newBoard"); + } + } + this.pushState(); + break; + } + case "fillExample": { + // Webview fills composer; optional auto-send handled client-side + break; + } + case "proveExample": { + const text = PROVE_EXAMPLE; + this.store.append("user", text); + await this.handleFreeform(text); break; + } case "stop": this.agent.stop(); this.bridge.stopGeneration(); @@ -161,19 +249,45 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { await vscode.commands.executeCommand("labwired.openEvidence"); break; case "runTwin": { - this.store.append("system", "⚙ twin/run (smoke)…"); - const r = await this.evidence?.runTwin("smoke"); - if (r) { - this.store.append( - "tool", - `${r.ok || r.twin_verified ? "✓" : "✗"} twin/run\n` + - `runId=${r.runId} twin_verified=${!!(r.ok || r.twin_verified)} model_verified=false\n` + - `${(r.summary || "").slice(0, 1200)}` + this.store.append("system", "⚙ run on digital twin…"); + try { + const { runOnTwin, formatTwinResultForChat } = await import( + "../twin/twinSession" ); - await vscode.commands.executeCommand("labwired.openEvidence"); - } else { - this.store.append("system", "twin/run failed — is labwired server running?"); + const r = await runOnTwin(); + this.store.append("tool", formatTwinResultForChat(r)); + } catch (e) { + this.store.append("system", `Twin run failed: ${e}`); } + this.pushState(); + break; + } + case "proveTwin": { + this.store.append("system", "⚙ prove on digital twin…"); + try { + const { proveOnTwin, formatTwinResultForChat } = await import( + "../twin/twinSession" + ); + const r = await proveOnTwin(); + this.store.append("tool", formatTwinResultForChat(r)); + } catch (e) { + this.store.append("system", `Twin prove failed: ${e}`); + } + this.pushState(); + break; + } + case "debugTwin": { + this.store.append("system", "⚙ debug on digital twin…"); + try { + const { debugOnTwin, formatTwinResultForChat } = await import( + "../twin/twinSession" + ); + const r = await debugOnTwin(); + this.store.append("tool", formatTwinResultForChat(r)); + } catch (e) { + this.store.append("system", `Twin debug failed: ${e}`); + } + this.pushState(); break; } case "openSerial": @@ -191,7 +305,6 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { text = await expandAtMentions(text); this.store.append("user", text); - // Real tools first (slash + NL shortcuts) if (text === "/tools" || text === "/help") { this.store.append("tool", this.tools.listCatalog()); break; @@ -199,62 +312,101 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { const routed = await this.tools.tryRoute(text); if (routed) { this.appendToolEvent(routed); + this.pushState(); break; } - // Freeform → Embedder-style server first, else local AgentSession - const mode = this.session.getMode(); - this.store.append("assistant", "…"); - const tab = this.store.getActive(); - const asstMsg = tab.messages[tab.messages.length - 1]; - - // Verify mode: always run twin first and attach evidence (product wedge) - if (mode === "verify") { - this.store.append("system", "Verify mode → twin/run before answer…"); - const twin = await this.evidence?.runTwin("smoke"); - if (twin) { - this.store.append( - "tool", - `${twin.ok || twin.twin_verified ? "✓" : "✗"} twin/run\n` + - `twin_verified=${!!(twin.ok || twin.twin_verified)} model_verified=false\n` + - `evidence=${twin.evidencePath || twin.runId}\n` + - `${(twin.summary || "").slice(0, 800)}` - ); - text = - `${text}\n\n[Host twin evidence]\n` + - `twin_verified=${!!(twin.ok || twin.twin_verified)}\n` + - `model_verified=false\n` + - `runId=${twin.runId}\n` + - `summary:\n${(twin.summary || "").slice(0, 1500)}`; - } else { - this.store.append( - "system", - "twin/run unavailable — answer must not claim verified." - ); - text += - "\n\n[Host] twin/run failed; do not claim model_verified or twin_verified."; - } - } + await this.handleFreeform(text); + break; + } + } + } - if (this.rpc?.isRunning()) { - this.rpcAssistant = ""; - this.rpcAsstMsg = asstMsg; - try { - await this.rpc.request("mode/set", { mode }); - await this.rpc.request("chat/send", { content: text, mode }); - } catch (e) { - if (asstMsg) asstMsg.text = `RPC error: ${e}`; - this.pushState(); - // fallback - await this.runLocalAgent(text, mode, asstMsg); - } - break; + /** Single brain: terminal CLI agent by default; optional in-panel LLM. */ + private async handleFreeform(text: string): Promise { + const mode = this.session.getMode(); + const inPanel = vscode.workspace + .getConfiguration("labwired") + .get("inPanelLlm"); + + // Verify mode: prove on digital twin first (wedge) — results always land in chat + if (mode === "verify") { + this.store.append("system", "Verify → labwired_verify on digital twin…"); + try { + const { proveOnTwin, formatTwinResultForChat } = await import( + "../twin/twinSession" + ); + const twin = await proveOnTwin(); + this.store.append("tool", formatTwinResultForChat(twin)); + text = + `${text}\n\n[Host twin prove]\n` + + `model_verified=${twin.model_verified}\n` + + `status=${twin.status || "—"}\n` + + `summary:\n${twin.summary.slice(0, 1500)}`; + if (!twin.model_verified) { + this.store.append( + "system", + "Twin not model_verified — do not claim verified until prove is green." + ); } + } catch (e) { + this.store.append("system", `Twin prove failed: ${e}`); + } + } + // Debug mode: twin debug probe (observational) + if (mode === "debug") { + this.store.append("system", "Debug → digital twin probe…"); + try { + const { debugOnTwin, formatTwinResultForChat } = await import( + "../twin/twinSession" + ); + const twin = await debugOnTwin(); + this.store.append("tool", formatTwinResultForChat(twin)); + } catch (e) { + this.store.append("system", `Twin debug failed: ${e}`); + } + } + + if (!inPanel) { + const wsRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + const ctx = buildWorkspaceContext(wsRoot, text); + const prompt = + ctx.mode === "empty" + ? text + : `${text}\n\n${contextHandoffBlock(ctx)}`; + this.store.append( + "system", + `Starting CLI agent (${MODE_LABEL[mode]})… · ${ctx.summary}` + ); + await this.bridge.sendPromptViaTerminal(prompt, mode); + this.store.append( + "assistant", + "→ Agent terminal (Embedder-class CLI + labwired_context). Reply there; twin/prove tools run inside the agent." + ); + this.pushState(); + return; + } + + this.store.append("assistant", "…"); + const tab = this.store.getActive(); + const asstMsg = tab.messages[tab.messages.length - 1]; + + if (this.rpc?.isRunning()) { + this.rpcAssistant = ""; + this.rpcAsstMsg = asstMsg; + try { + await this.rpc.request("mode/set", { mode }); + await this.rpc.request("chat/send", { content: text, mode }); + } catch (e) { + if (asstMsg) asstMsg.text = `RPC error: ${e}`; + this.pushState(); await this.runLocalAgent(text, mode, asstMsg); - break; } + return; } + + await this.runLocalAgent(text, mode, asstMsg); } private async runLocalAgent( @@ -321,13 +473,27 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { const cli = this.bridge.getCli(); const tab = this.store.getActive(); const snap = this.session.snapshot(); + const cloud = loadCloudSession(); + const signedIn = !!(cloud?.accessToken); + const ws = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + const boardMeta = ws ? loadBoardMeta(ws) : undefined; + const ctx = buildWorkspaceContext(ws); void this.view.webview.postMessage({ type: "state", cliPath: cli.path || "", cliVersion: cli.version || "", + cliFlavor: cli.flavor || "", mode: snap.mode, model: snap.model, - project: snap.project, + project: snap.project || cloud?.projectId || "", + signedIn, + email: cloud?.email || "", + board: boardMeta?.board || ctx.board || "", + mcu: boardMeta?.mcu || ctx.mcu || "", + designContextOk: ctx.design_context_ok, + twinBuildable: ctx.twin_buildable, + contextMode: ctx.mode, + contextSummary: ctx.summary, tabs: this.store.listTabs().map((t) => ({ id: t.id, title: t.title, @@ -336,6 +502,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { messages: tab.messages, workspace: vscode.workspace.workspaceFolders?.[0]?.name || "", toolCount: TOOLS.length, + proveExample: PROVE_EXAMPLE, }); } @@ -346,28 +513,43 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { title: "LabWired", body: `
+
+ + + + + + +
${LW_MARK_SVG_LG}

LabWired

-

In-panel agent · live serial · local catalog. Tools: /doctor /probe /catalog bme280

+

Design context always · twin prove when ready

- - - - - - - - + + + + + +
+ +

New board / Import → Run · Prove · Debug on digital twin (login required for hosted twin)

+
+ + +
- +