@lasso-ai/cli 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,8 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Changed
11
+
12
+ - **One-shot mode as sidebar panel**: Changed one-shot mode from a centered modal to a right-side sidebar panel that only appears during one-shot mode. When users exit one-shot mode, they return to the normal edit mode with just the toolbar. This provides a cleaner, more integrated experience similar to modern AI editors.
13
+ - **Mini toolbar positioning**: Adjusted the mini toolbar position to `left: calc(50% + 140px)` (reduced from 280px) to reduce overlap with the main toolbar. Added smooth transition and centered positioning when the main toolbar is dismissed. Mini toolbar is now hidden when one-shot sidebar is open to avoid UI clutter.
14
+
10
15
  ### Added
11
16
 
17
+ - **Multiple access points for one-shot mode**: Added a Command button to the toolbar (⌘ icon) for opening the one-shot sidebar, in addition to the existing ⌘K keyboard shortcut. The sidebar includes a model selector dropdown allowing users to choose their AI model before submitting a one-shot request.
18
+ - **Model selector in command sidebar**: The one-shot sidebar now displays the currently selected model with a dropdown to switch between available models. The selector is organized by provider (Anthropic, OpenAI, Google, Local Agents) for easy navigation.
19
+ - **Cursor CLI support**: Added Cursor as a local agent option alongside Claude Code, Codex, and OpenCode. Users can now select Cursor models in the model dropdown if Cursor is installed on their system. Cursor uses similar output parsing to Claude Code with progress tracking and error handling.
20
+ - **Model locking based on API key configuration**: Models are now locked with a lock icon in the dropdown when the user hasn't configured the provider's API key in their dashboard. Free plan users can only use models from providers with configured keys. Paid plan users have all models unlocked. CLI agents (Claude Code, Codex, OpenCode, Cursor) are always unlocked since users manage their own credentials.
12
21
  - **One-shot mode with global command bar**: Press ⌘K to open a command interface
13
22
  anywhere in your app. Describe what you want to build (e.g., "Build a complete billing
14
23
  system with invoices and payment tracking") and Lasso will plan, execute, and
package/README.md CHANGED
@@ -200,7 +200,7 @@ Use:
200
200
 
201
201
  Your selection and project context stay inside the Lasso pipeline while the actual coding agent can be swapped independently.
202
202
 
203
- When installed, Lasso automatically detects the `claude`, `codex`, and
203
+ When installed, Lasso automatically detects the `claude`, `codex`, `cursor`, and
204
204
  `opencode` commands on your PATH and adds the available local agents to the
205
205
  prompt model menu. Local agents run in read-only/planning mode and return a
206
206
  reviewable proposal; Lasso remains the only process that writes accepted
@@ -444,9 +444,10 @@ sees**, so install it before starting the dev server (or restart afterwards):
444
444
  | ------------------------------------------------------------ | ---------- | ----------------------------------------------------------------------------- |
445
445
  | [Claude Code](https://lasso.byorello.space/docs/claude-code) | `claude` | `claude -p … --permission-mode plan --output-format stream-json --max-turns 3` |
446
446
  | [Codex](https://lasso.byorello.space/docs/codex) | `codex` | `codex exec --json --sandbox read-only --skip-git-repo-check` |
447
+ | [Cursor](https://lasso.byorello.space/docs/cursor) | `cursor` | `cursor agent run --non-interactive --model "<model>"` |
447
448
  | [OpenCode](https://lasso.byorello.space/docs/opencode) | `opencode` | `opencode run --format json --print-logs --agent plan` |
448
449
 
449
- All three run read-only, so the agent proposes a patch and never writes to your project.
450
+ All four run read-only, so the agent proposes a patch and never writes to your project.
450
451
  Sign each CLI in once in a real terminal; Lasso inherits the credentials it stores for
451
452
  your user.
452
453
 
@@ -39,12 +39,12 @@ type AgentInput = {
39
39
  };
40
40
  };
41
41
  export type AgentConfig = {
42
- provider: "anthropic" | "openai" | "google" | "ollama" | "claude-code" | "codex" | "opencode";
42
+ provider: "anthropic" | "openai" | "google" | "ollama" | "claude-code" | "codex" | "opencode" | "cursor";
43
43
  apiKey?: string;
44
44
  model?: string;
45
45
  baseUrl?: string;
46
46
  };
47
- export type LocalAgent = "claude-code" | "codex" | "opencode";
47
+ export type LocalAgent = "claude-code" | "codex" | "opencode" | "cursor";
48
48
  export type AgentProgressLevel = "working" | "error";
49
49
  export type AgentProgress = (message: string, detail?: string, level?: AgentProgressLevel) => void;
50
50
  export type AgentPrompt = {
package/dist/cli/agent.js CHANGED
@@ -23,7 +23,7 @@ function respondToAgentPrompt(taskId, value) {
23
23
  }
24
24
  async function detectLocalAgents() {
25
25
  const found = new Set();
26
- for (const [name, command] of [["claude-code", "claude"], ["codex", "codex"], ["opencode", "opencode"]]) {
26
+ for (const [name, command] of [["claude-code", "claude"], ["codex", "codex"], ["opencode", "opencode"], ["cursor", "cursor"]]) {
27
27
  try {
28
28
  await execFileAsync(process.platform === "win32" ? "where.exe" : "which", [command]);
29
29
  found.add(name);
@@ -229,6 +229,33 @@ function extractLocalAgentText(raw, provider) {
229
229
  }
230
230
  return results.at(-1) || raw;
231
231
  }
232
+ if (provider === "cursor") {
233
+ // Cursor uses similar output format to claude-code
234
+ try {
235
+ const payload = JSON.parse(raw);
236
+ if (payload.result)
237
+ return payload.result;
238
+ }
239
+ catch {
240
+ // Stream-json output is handled below.
241
+ }
242
+ const results = [];
243
+ for (const line of raw.split(/\r?\n/)) {
244
+ try {
245
+ const event = JSON.parse(line);
246
+ if (typeof event.result === "string")
247
+ results.push(event.result);
248
+ const message = event.message;
249
+ for (const part of message?.content || [])
250
+ if (part.type === "text" && part.text)
251
+ results.push(part.text);
252
+ }
253
+ catch {
254
+ // Ignore progress lines that are not JSON.
255
+ }
256
+ }
257
+ return results.at(-1) || raw;
258
+ }
232
259
  const texts = [];
233
260
  for (const line of raw.split(/\r?\n/)) {
234
261
  try {
@@ -294,7 +321,7 @@ function extractLocalAgentProposal(raw, provider) {
294
321
  const event = JSON.parse(line);
295
322
  const item = event.item;
296
323
  const part = event.part;
297
- const values = provider === "claude-code"
324
+ const values = provider === "claude-code" || provider === "cursor"
298
325
  ? [event.result, ...(event.message?.content || []).map((entry) => entry.text)]
299
326
  : provider === "opencode"
300
327
  ? [...collectTextFields(event), part?.type === "text" ? part.text : undefined]
@@ -335,7 +362,7 @@ function extractLocalAgentProposal(raw, provider) {
335
362
  // Keep looking; this output may only be a progress or tool event.
336
363
  }
337
364
  }
338
- throw new Error(`${provider === "opencode" ? "OpenCode" : "The agent"} did not return a reviewable proposal. It produced progress/tool events but no final JSON containing file changes.`);
365
+ throw new Error(`${provider === "opencode" ? "OpenCode" : provider === "cursor" ? "Cursor" : "The agent"} did not return a reviewable proposal. It produced progress/tool events but no final JSON containing file changes.`);
339
366
  }
340
367
  function snippet(value, max = 80) {
341
368
  const s = String(value ?? "").trim().replace(/\s+/g, " ");
@@ -413,9 +440,10 @@ function progressFromLine(raw, provider) {
413
440
  try {
414
441
  const event = JSON.parse(raw);
415
442
  const item = event.item;
416
- if (provider === "claude-code") {
443
+ if (provider === "claude-code" || provider === "cursor") {
444
+ const agentName = provider === "cursor" ? "Cursor" : "Claude Code";
417
445
  if (event.type === "system")
418
- return progressEvent("Claude Code connected");
446
+ return progressEvent(`${agentName} connected`);
419
447
  // tool_use blocks inside assistant messages
420
448
  const toolBlock = event.message?.content?.find?.((p) => p.type === "tool_use");
421
449
  if (toolBlock) {
@@ -423,13 +451,13 @@ function progressFromLine(raw, provider) {
423
451
  const inp = toolBlock.input;
424
452
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
425
453
  return progressEvent(detail
426
- ? `Claude Code · ${toolName} ${snippet(detail)}`
427
- : `Claude Code · ${toolName}`, detail);
454
+ ? `${agentName} · ${toolName} ${snippet(detail)}`
455
+ : `${agentName} · ${toolName}`, detail);
428
456
  }
429
457
  // thinking blocks inside assistant messages
430
458
  const thinkBlock = event.message?.content?.find?.((p) => p.type === "thinking");
431
459
  if (thinkBlock?.thinking) {
432
- return progressEvent(`Claude Code · ${snippet(thinkBlock.thinking)}`, thinkBlock.thinking);
460
+ return progressEvent(`${agentName} · ${snippet(thinkBlock.thinking)}`, thinkBlock.thinking);
433
461
  }
434
462
  // top-level tool event fields (stream-json verbose format)
435
463
  const tool = event.tool_name || event.name || event.tool?.name;
@@ -437,14 +465,14 @@ function progressFromLine(raw, provider) {
437
465
  const inp = event.tool_input;
438
466
  const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
439
467
  return progressEvent(detail
440
- ? `Claude Code · ${tool} ${snippet(detail)}`
441
- : `Claude Code · ${tool}`, detail);
468
+ ? `${agentName} · ${tool} ${snippet(detail)}`
469
+ : `${agentName} · ${tool}`, detail);
442
470
  }
443
471
  if (event.type === "result" || event.result) {
444
- return progressEvent("Claude Code · preparing the proposal");
472
+ return progressEvent(`${agentName} · preparing the proposal`);
445
473
  }
446
474
  if (event.type === "assistant")
447
- return progressEvent("Claude Code · reasoning about the change");
475
+ return progressEvent(`${agentName} · reasoning about the change`);
448
476
  }
449
477
  else if (provider === "codex") {
450
478
  const type = item?.type || event.type || "";
@@ -514,10 +542,13 @@ function localAgentError(command, stderr, exitCode) {
514
542
  const output = stderr.trim();
515
543
  const hint = agentFailureHint(output);
516
544
  if (hint)
517
- return `${command === "opencode" ? "OpenCode" : command} could not finish: ${hint}`;
545
+ return `${command === "opencode" ? "OpenCode" : command === "cursor" ? "Cursor" : command} could not finish: ${hint}`;
518
546
  if (command === "opencode" && /waiting for permission or authentication/i.test(output)) {
519
547
  return "OpenCode needs permission or authentication. Run OpenCode once in a terminal, approve the requested access or sign in, then retry in Lasso.";
520
548
  }
549
+ if (command === "cursor" && /waiting for permission or authentication/i.test(output)) {
550
+ return "Cursor needs permission or authentication. Run Cursor once in a terminal, approve the requested access or sign in, then retry in Lasso.";
551
+ }
521
552
  if (command === "codex" && output.includes("missing field `base_instructions`")) {
522
553
  return "Codex could not read its models cache because it uses an older cache format. Update Codex, then retry. Lasso already bypasses the repository trust check.";
523
554
  }
@@ -537,6 +568,9 @@ function localCommand(provider, model, prompt) {
537
568
  if (provider === "claude-code") {
538
569
  return { command: "claude", args: ["-p", prompt || "", "--output-format", "stream-json", "--verbose", "--permission-mode", "plan", "--max-turns", "3", ...(selectedModel ? ["--model", selectedModel] : [])] };
539
570
  }
571
+ if (provider === "cursor") {
572
+ return { command: "cursor", args: ["agent", "run", "--non-interactive", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
573
+ }
540
574
  if (provider === "opencode") {
541
575
  return { command: "opencode", args: ["run", "--format", "json", "--print-logs", "--log-level", "INFO", "--agent", "plan", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
542
576
  }
@@ -545,7 +579,7 @@ function localCommand(provider, model, prompt) {
545
579
  function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskId, onPrompt) {
546
580
  return new Promise((resolve, reject) => {
547
581
  const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
548
- const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : "Claude Code";
582
+ const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : provider === "cursor" ? "Cursor" : "Claude Code";
549
583
  let stdout = "";
550
584
  let stderr = "";
551
585
  let pendingStdout = "";
@@ -653,6 +687,9 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
653
687
  if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
654
688
  onProgress?.("OpenCode · provider rejected the request; stopping this task…", undefined, "error");
655
689
  }
690
+ if (provider === "cursor" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
691
+ onProgress?.("Cursor · provider rejected the request; stopping this task…", undefined, "error");
692
+ }
656
693
  }
657
694
  resolve({ stdout, stderr, exitCode: code ?? 1 });
658
695
  });
@@ -678,7 +715,7 @@ async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt)
678
715
  ? `\n\nDRAG REPOSITIONING TASK:\nThe user dragged this element by dx: ${input.context.drag.delta?.dx ?? 0}px, dy: ${input.context.drag.delta?.dy ?? 0}px to target coordinates (left: ${input.context.drag.targetRect?.left ?? 0}px, top: ${input.context.drag.targetRect?.top ?? 0}px). Modify the source code (CSS classes, Tailwind classes, flex/grid alignment, margin offsets, or positioning properties) so the element is accurately rendered at this target position.`
679
716
  : "";
680
717
  const instruction = `Selection context:\n${JSON.stringify(input.element, null, 2)}\n${JSON.stringify(visualContext, null, 2)}\n\nConversation history:\n${history}\n\nPrevious change history for this selection:\n${priorChanges}\n\nRelevant source context:\n${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}\n\nCRITICAL: You MUST return ONLY JSON with actual file changes. Do NOT explain what you see or describe the code. Always propose concrete edits when given an instruction. Return JSON format: {"summary":"brief action taken","changes":[{"filePath":"relative/path","oldString":"exact existing text","newString":"replacement text"}]}`;
681
- if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
718
+ if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
682
719
  return proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
683
720
  }
684
721
  const system = "You are Lasso, a source-code editing agent. Your ONLY job is to propose concrete file changes. NEVER explain, describe, or analyze code without proposing edits. ALWAYS return valid JSON with at least one change when the user requests a modification. Each oldString must occur exactly once in its file. Never rewrite whole files. Keep changes focused and minimal. If you cannot find the exact text to change, look harder at the provided context - do not give up and explain instead.";
@@ -734,7 +771,7 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
734
771
  const context = await contextFor(cwd, input.element);
735
772
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
736
773
  const prompt = `Answer the user's question conversationally and directly. Do not propose file changes and do not return JSON. If the question is about the selected UI, use the selection and source context below.\n\nUser question:\n${input.question}\n\nSelected element:\n${JSON.stringify(input.element, null, 2)}\n\nVisual context:\n${JSON.stringify({ ...input.context, screenshots: undefined }, null, 2)}\n\nConversation:\n${history}\n\nRelevant source context:\n${context || "No matching source context was found."}`;
737
- if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
774
+ if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
738
775
  const local = localCommand(config.provider, config.model, prompt);
739
776
  const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress, input.taskId, onPrompt);
740
777
  if (result.exitCode !== 0)
@@ -263,15 +263,39 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
263
263
  return [];
264
264
  }
265
265
  }
266
- const availableModels = async () => {
266
+ async function cursorModels() {
267
+ if (!localAgents.has("cursor"))
268
+ return [];
269
+ try {
270
+ const result = await execFileAsync("cursor", ["model", "list"], { maxBuffer: 1024 * 1024 });
271
+ return result.stdout
272
+ .split(/\r?\n/)
273
+ .map((line) => line.trim())
274
+ .filter((line) => line && !line.startsWith("Available models"))
275
+ .map((line) => {
276
+ const parts = line.split(/\s+/);
277
+ const id = parts[0]?.trim();
278
+ const name = parts.slice(1).join(" ").trim();
279
+ return id ? { id: `cursor:${id}`, label: `Cursor · ${name}`, provider: "cli" } : null;
280
+ })
281
+ .filter((model) => model !== null);
282
+ }
283
+ catch {
284
+ return [];
285
+ }
286
+ }
287
+ const availableModels = async (collabConfig = null) => {
267
288
  localAgents = await (0, agent_1.detectLocalAgents)();
268
289
  const locals = await localModels();
269
290
  const discoveredOpenCodeModels = await openCodeModels();
291
+ const discoveredCursorModels = await cursorModels();
270
292
  if (locals.length && !agentConfig) {
271
293
  const base = process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api";
272
294
  agentConfig = { provider: "ollama", apiKey: "ollama", model: process.env.OLLAMA_MODEL || fileEnv.OLLAMA_MODEL, baseUrl: `${base.replace(/\/api\/?$/, "")}/v1` };
273
295
  }
274
- return [
296
+ const isPaidPlan = collabConfig?.plan && collabConfig.plan !== "free";
297
+ const configuredProviders = collabConfig?.configuredProviders || [];
298
+ const allModels = [
275
299
  { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" },
276
300
  { id: "claude-opus-4-1-20250805", label: "Claude Opus 4.1", provider: "anthropic" },
277
301
  { id: "gpt-4.1", label: "GPT-4.1", provider: "openai" },
@@ -298,17 +322,57 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
298
322
  { id: "opencode:anthropic/claude-sonnet-4-5", label: "OpenCode · Claude Sonnet", provider: "cli" },
299
323
  { id: "opencode:openai/gpt-5", label: "OpenCode · GPT-5", provider: "cli" },
300
324
  ] : []),
325
+ ...(localAgents.has("cursor") ? [
326
+ { id: "cursor:claude-sonnet-4.5", label: "Cursor · Claude Sonnet 4.5", provider: "cli" },
327
+ { id: "cursor:claude-opus-4", label: "Cursor · Claude Opus 4", provider: "cli" },
328
+ { id: "cursor:claude-3.5-sonnet", label: "Cursor · Claude 3.5 Sonnet", provider: "cli" },
329
+ { id: "cursor:gpt-4o", label: "Cursor · GPT-4o", provider: "cli" },
330
+ { id: "cursor:gpt-4o-mini", label: "Cursor · GPT-4o mini", provider: "cli" },
331
+ ] : []),
301
332
  ...discoveredOpenCodeModels,
333
+ ...discoveredCursorModels,
302
334
  ...locals,
303
335
  ];
336
+ // Lock models based on provider API key configuration
337
+ // Paid plans have all models unlocked
338
+ // Free plans only have models unlocked for providers with configured API keys
339
+ const providerKeyMapping = {
340
+ anthropic: "ANTHROPIC_API_KEY",
341
+ openai: "OPENAI_API_KEY",
342
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
343
+ ollama: "OLLAMA_BASE_URL",
344
+ };
345
+ return allModels.map((model) => {
346
+ if (model.provider === "cli") {
347
+ // CLI agents are always unlocked (user manages their own credentials)
348
+ return model;
349
+ }
350
+ if (isPaidPlan) {
351
+ // Paid plans have all hosted models unlocked
352
+ return model;
353
+ }
354
+ // Free plans: check if provider is configured
355
+ const envKey = providerKeyMapping[model.provider];
356
+ const isConfigured = configuredProviders.includes(model.provider) ||
357
+ Boolean(process.env[envKey]) ||
358
+ Boolean(fileEnv[envKey]);
359
+ if (!isConfigured) {
360
+ return {
361
+ ...model,
362
+ locked: true,
363
+ lockedReason: "Configure your API key in the dashboard to use this model",
364
+ };
365
+ }
366
+ return model;
367
+ });
304
368
  };
305
369
  wss.on("connection", (socket) => {
306
370
  overlaySocket = socket;
307
371
  console.log(chalk_1.default.green("✓") + " Overlay connected");
308
- socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models: [], collab: collabConfig?.registered ? collabConfig : null }));
309
- void availableModels().then((models) => {
372
+ socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models: [], collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null }));
373
+ void availableModels(collabConfig).then((models) => {
310
374
  if (socket.readyState === socket.OPEN)
311
- socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models, collab: collabConfig?.registered ? collabConfig : null }));
375
+ socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: Boolean(agentConfig) || localAgents.size > 0, models, collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null }));
312
376
  });
313
377
  void getGitState(cwd).then((git) => {
314
378
  if (socket.readyState === socket.OPEN)
@@ -438,7 +502,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
438
502
  }
439
503
  if (msg.type === "ask") {
440
504
  const localProvider = msg.provider === "cli";
441
- const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
505
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
442
506
  if (!lassoKeyConfigured && !localProvider) {
443
507
  socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Set your Lasso API key before asking the hosted agent a question." }));
444
508
  return;
@@ -473,7 +537,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
473
537
  }
474
538
  else if (msg.type === "edit") {
475
539
  const localProvider = msg.provider === "cli";
476
- const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
540
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
477
541
  if (!lassoKeyConfigured && !localProvider) {
478
542
  socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Set VITE_LASSO_API_KEY or NEXT_LASSO_API_KEY in your app environment before sending an edit." }));
479
543
  return;
@@ -509,7 +573,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
509
573
  }
510
574
  else if (msg.type === "git_generate_message") {
511
575
  const localProvider = msg.provider === "cli";
512
- const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
576
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
513
577
  if (!agentConfig && !localProvider) {
514
578
  socket.send(JSON.stringify({ type: "git_commit_message", error: "No AI provider is configured." }));
515
579
  return;
@@ -605,7 +669,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
605
669
  return;
606
670
  }
607
671
  const localProvider = msg.provider === "cli";
608
- const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : "codex";
672
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
609
673
  const selectedConfig = localProvider
610
674
  ? { provider: cliProvider, model: msg.model || "claude-code:sonnet" }
611
675
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model || agentConfig.model };
@@ -63,8 +63,9 @@ async function runCreateCommand(projectName, framework, typescript, packageManag
63
63
  let args;
64
64
  if (framework === "next") {
65
65
  const tsFlag = typescript ? "--ts" : "--js";
66
+ const pmFlag = packageManager === "pnpm" ? "--use-pnpm" : packageManager === "yarn" ? "--use-yarn" : "--use-npm";
66
67
  command = "npx";
67
- args = ["create-next-app@latest", projectName, tsFlag, "--tailwind", "--eslint", "--app", "--src-dir", "--import-alias", "@/*", "--no-git"];
68
+ args = ["create-next-app@latest", projectName, tsFlag, "--tailwind", "--eslint", "--app", "--src-dir", "--import-alias", "@/*", "--no-git", pmFlag];
68
69
  }
69
70
  else if (framework === "react") {
70
71
  command = "npx";
@@ -172,16 +173,17 @@ async function createProject(options) {
172
173
  }
173
174
  if (!packageManager) {
174
175
  const detected = await detectPackageManager();
176
+ const choices = [
177
+ { title: "pnpm", value: "pnpm" },
178
+ { title: "npm", value: "npm" },
179
+ { title: "yarn", value: "yarn" },
180
+ ];
175
181
  const response = await (0, prompts_1.default)({
176
182
  type: "select",
177
183
  name: "packageManager",
178
184
  message: "Package manager:",
179
- choices: [
180
- { title: "pnpm", value: "pnpm" },
181
- { title: "npm", value: "npm" },
182
- { title: "yarn", value: "yarn" },
183
- ],
184
- initial: detected,
185
+ choices,
186
+ initial: Math.max(0, choices.findIndex((c) => c.value === detected)),
185
187
  });
186
188
  packageManager = response.packageManager;
187
189
  }
@@ -11,6 +11,8 @@ export interface CollabConfig {
11
11
  workspaceId?: string;
12
12
  token?: string;
13
13
  apiKey?: string;
14
+ plan?: string;
15
+ configuredProviders?: string[];
14
16
  }
15
17
  export interface InitResult {
16
18
  ok: boolean;
@@ -292,6 +292,8 @@ async function resolveCollabSession(cwd, fileEnv, projectId) {
292
292
  config.registered = true;
293
293
  config.claimed = Boolean(body.session?.id);
294
294
  config.registeredAt = new Date().toISOString();
295
+ config.plan = body.project?.plan || "free";
296
+ config.configuredProviders = body.project?.configuredProviders || [];
295
297
  console.log(chalk_1.default.green("✓") +
296
298
  " Project session opened " +
297
299
  chalk_1.default.cyan(config.name || projectId) +
package/dist/overlay.js CHANGED
@@ -8094,7 +8094,7 @@
8094
8094
 
8095
8095
  .lasso-toolbar-mini {
8096
8096
  position: fixed;
8097
- left: calc(50% + 280px);
8097
+ left: calc(50% + 140px);
8098
8098
  bottom: 24px;
8099
8099
  display: inline-flex;
8100
8100
  align-items: center;
@@ -8108,8 +8108,21 @@
8108
8108
  -webkit-backdrop-filter: blur(20px);
8109
8109
  pointer-events: auto;
8110
8110
  z-index: 10;
8111
+ transition: left 200ms ease, opacity 200ms ease;
8111
8112
  }
8113
+
8114
+ .lasso-toolbar.dismissed + .lasso-toolbar-mini {
8115
+ left: 50%;
8116
+ transform: translateX(-50%);
8117
+ }
8118
+
8112
8119
  .lasso-toolbar-mini.dismissed { display: none; }
8120
+
8121
+ /* Hide mini toolbar when one-shot sidebar is open */
8122
+ .lasso-command-sidebar:not([hidden]) ~ .lasso-toolbar-mini {
8123
+ opacity: 0;
8124
+ pointer-events: none;
8125
+ }
8113
8126
  .lasso-new-page-btn {
8114
8127
  display: inline-flex;
8115
8128
  align-items: center;
@@ -8343,6 +8356,16 @@
8343
8356
  color: var(--lo-error);
8344
8357
  }
8345
8358
 
8359
+ /* Command button */
8360
+ .lasso-tool-btn.command-tool {
8361
+ color: var(--lo-text-2);
8362
+ }
8363
+
8364
+ .lasso-tool-btn.command-tool:hover {
8365
+ color: var(--lo-primary);
8366
+ background: rgba(129, 201, 149, 0.16);
8367
+ }
8368
+
8346
8369
  /* Badge on comment button */
8347
8370
  .lasso-comments-badge {
8348
8371
  display: none;
@@ -9841,6 +9864,15 @@
9841
9864
  background: var(--lo-surface-hover);
9842
9865
  }
9843
9866
 
9867
+ .lasso-prompt-model-item.locked {
9868
+ cursor: not-allowed;
9869
+ opacity: 0.6;
9870
+ }
9871
+
9872
+ .lasso-prompt-model-item.locked:hover {
9873
+ background: transparent;
9874
+ }
9875
+
9844
9876
  .lasso-prompt-model-item .lasso-prompt-model-check {
9845
9877
  flex-shrink: 0;
9846
9878
  color: var(--lo-primary);
@@ -9851,6 +9883,11 @@
9851
9883
  opacity: 1;
9852
9884
  }
9853
9885
 
9886
+ .lasso-prompt-model-item .lasso-prompt-model-lock {
9887
+ flex-shrink: 0;
9888
+ color: var(--lo-text-3);
9889
+ }
9890
+
9854
9891
  .lasso-prompt-element {
9855
9892
  min-width: 0;
9856
9893
  margin-left: auto;
@@ -11080,47 +11117,19 @@
11080
11117
  .lasso-agent-prompt-actions button:active { transform: scale(0.97); }
11081
11118
 
11082
11119
  /* ==========================================================
11083
- COMMAND BAR (One-Shot Build Mode)
11120
+ COMMAND SIDEBAR (One-Shot Build Mode)
11084
11121
  ========================================================== */
11085
11122
 
11086
- .lasso-command-backdrop {
11123
+ .lasso-command-sidebar {
11087
11124
  position: fixed;
11088
- inset: 0;
11089
- z-index: 100;
11090
- background: rgba(8, 9, 11, 0.6);
11091
- backdrop-filter: blur(4px);
11092
- -webkit-backdrop-filter: blur(4px);
11093
- pointer-events: auto;
11094
- }
11095
-
11096
- .lasso-command-backdrop[hidden] {
11097
- display: none;
11098
- }
11099
-
11100
- .lasso-command-bar {
11101
- position: fixed;
11102
- top: 0;
11103
- left: 0;
11104
- right: 0;
11105
- bottom: 0;
11106
- z-index: 101;
11107
- display: flex;
11108
- align-items: center;
11109
- justify-content: center;
11110
- padding: 24px;
11111
- pointer-events: none;
11112
- }
11113
-
11114
- .lasso-command-bar[hidden] {
11115
- display: none;
11116
- }
11117
-
11118
- .lasso-command-card {
11119
- width: min(600px, calc(100vw - 48px));
11120
- max-height: calc(100vh - 48px);
11125
+ top: 16px;
11126
+ right: 16px;
11127
+ bottom: 16px;
11128
+ width: 380px;
11129
+ max-width: calc(100vw - 32px);
11121
11130
  display: flex;
11122
11131
  flex-direction: column;
11123
- padding: 20px;
11132
+ padding: 0;
11124
11133
  border: 1px solid var(--lo-border);
11125
11134
  border-radius: var(--lo-radius-xl);
11126
11135
  background: rgba(24, 25, 29, 0.98);
@@ -11130,12 +11139,17 @@
11130
11139
  box-shadow: 0 24px 56px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.08);
11131
11140
  pointer-events: auto;
11132
11141
  animation: lasso-command-in 200ms cubic-bezier(0.16, 1, 0.3, 1);
11142
+ z-index: 100;
11143
+ }
11144
+
11145
+ .lasso-command-sidebar[hidden] {
11146
+ display: none;
11133
11147
  }
11134
11148
 
11135
11149
  @keyframes lasso-command-in {
11136
11150
  from {
11137
11151
  opacity: 0;
11138
- transform: translateY(12px) scale(0.98);
11152
+ transform: translateX(12px);
11139
11153
  }
11140
11154
  to {
11141
11155
  opacity: 1;
@@ -11143,11 +11157,12 @@
11143
11157
  }
11144
11158
  }
11145
11159
 
11146
- .lasso-command-header {
11160
+ .lasso-command-sidebar-header {
11147
11161
  display: flex;
11148
11162
  align-items: center;
11149
11163
  justify-content: space-between;
11150
- margin-bottom: 16px;
11164
+ padding: 16px 20px;
11165
+ border-bottom: 1px solid var(--lo-border);
11151
11166
  }
11152
11167
 
11153
11168
  .lasso-command-brand {
@@ -11157,12 +11172,12 @@
11157
11172
  }
11158
11173
 
11159
11174
  .lasso-command-brand-logo {
11160
- width: 28px;
11161
- height: 28px;
11175
+ width: 24px;
11176
+ height: 24px;
11162
11177
  }
11163
11178
 
11164
11179
  .lasso-command-brand-title {
11165
- font-size: 15px;
11180
+ font-size: 14px;
11166
11181
  font-weight: 600;
11167
11182
  letter-spacing: -0.01em;
11168
11183
  }
@@ -11186,10 +11201,124 @@
11186
11201
  color: var(--lo-text);
11187
11202
  }
11188
11203
 
11189
- .lasso-command-body {
11204
+ .lasso-command-sidebar-body {
11205
+ display: flex;
11206
+ flex-direction: column;
11207
+ gap: 16px;
11208
+ padding: 20px;
11209
+ flex: 1;
11210
+ overflow-y: auto;
11211
+ }
11212
+
11213
+ .lasso-command-model-select {
11214
+ position: relative;
11215
+ }
11216
+
11217
+ .lasso-command-model-btn {
11218
+ width: 100%;
11219
+ display: flex;
11220
+ align-items: center;
11221
+ gap: 8px;
11222
+ padding: 10px 12px;
11223
+ border: 1px solid var(--lo-border);
11224
+ border-radius: var(--lo-radius-md);
11225
+ background: var(--lo-surface-2);
11226
+ color: var(--lo-text);
11227
+ font-family: inherit;
11228
+ font-size: 12px;
11229
+ font-weight: 500;
11230
+ cursor: pointer;
11231
+ transition: border-color 140ms ease, background 140ms ease;
11232
+ }
11233
+
11234
+ .lasso-command-model-btn:hover {
11235
+ border-color: var(--lo-primary);
11236
+ background: var(--lo-surface-hover);
11237
+ }
11238
+
11239
+ .lasso-command-model-icon {
11240
+ flex: 0 0 auto;
11241
+ }
11242
+
11243
+ .lasso-command-model-label {
11244
+ flex: 1;
11245
+ min-width: 0;
11246
+ overflow: hidden;
11247
+ text-overflow: ellipsis;
11248
+ white-space: nowrap;
11249
+ }
11250
+
11251
+ .lasso-command-model-chevron {
11252
+ flex: 0 0 auto;
11253
+ transition: transform 200ms ease;
11254
+ }
11255
+
11256
+ .lasso-command-model-menu {
11257
+ position: absolute;
11258
+ top: calc(100% + 4px);
11259
+ left: 0;
11260
+ right: 0;
11261
+ max-height: 320px;
11262
+ overflow-y: auto;
11263
+ padding: 8px;
11264
+ border: 1px solid var(--lo-border);
11265
+ border-radius: var(--lo-radius-lg);
11266
+ background: rgba(24, 25, 29, 0.98);
11267
+ backdrop-filter: blur(24px);
11268
+ -webkit-backdrop-filter: blur(24px);
11269
+ color: var(--lo-text);
11270
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
11271
+ z-index: 10;
11272
+ }
11273
+
11274
+ .lasso-command-model-section {
11275
+ margin-bottom: 8px;
11276
+ }
11277
+
11278
+ .lasso-command-model-section:last-child {
11279
+ margin-bottom: 0;
11280
+ }
11281
+
11282
+ .lasso-command-model-section-header {
11283
+ padding: 4px 8px;
11284
+ font-size: 10px;
11285
+ font-weight: 600;
11286
+ text-transform: uppercase;
11287
+ letter-spacing: 0.05em;
11288
+ color: var(--lo-text-3);
11289
+ }
11290
+
11291
+ .lasso-command-model-item {
11292
+ width: 100%;
11293
+ display: flex;
11294
+ align-items: center;
11295
+ gap: 8px;
11296
+ padding: 6px 8px;
11297
+ border: 0;
11298
+ border-radius: var(--lo-radius-md);
11299
+ background: transparent;
11300
+ color: var(--lo-text);
11301
+ font-family: inherit;
11302
+ font-size: 12px;
11303
+ font-weight: 450;
11304
+ text-align: left;
11305
+ cursor: pointer;
11306
+ transition: background 120ms ease;
11307
+ }
11308
+
11309
+ .lasso-command-model-item:hover {
11310
+ background: var(--lo-surface-hover);
11311
+ }
11312
+
11313
+ .lasso-command-model-item.selected {
11314
+ background: rgba(129, 201, 149, 0.16);
11315
+ color: var(--lo-primary);
11316
+ }
11317
+
11318
+ .lasso-command-input-wrapper {
11319
+ flex: 1;
11190
11320
  display: flex;
11191
11321
  flex-direction: column;
11192
- gap: 12px;
11193
11322
  }
11194
11323
 
11195
11324
  .lasso-command-input {
@@ -17378,8 +17507,8 @@
17378
17507
  const pin = document.createElement("button");
17379
17508
  pin.type = "button";
17380
17509
  const isResolved = thread.root.status === "RESOLVED";
17381
- const isOpen2 = state.openThreadUid === thread.root.uid;
17382
- pin.className = `lasso-comment-pin${isResolved ? " resolved" : ""}${isOpen2 ? " active" : ""}`;
17510
+ const isOpen = state.openThreadUid === thread.root.uid;
17511
+ pin.className = `lasso-comment-pin${isResolved ? " resolved" : ""}${isOpen ? " active" : ""}`;
17383
17512
  pin.dataset.commentUid = thread.root.uid;
17384
17513
  const authorColor = userNameColor(thread.root.author?.id || "");
17385
17514
  const authorInitials = initials(thread.root.author?.name || "");
@@ -19019,8 +19148,17 @@
19019
19148
  }
19020
19149
  if (message.type === "config" && message.models?.length) {
19021
19150
  state.MODELS = message.models;
19022
- state.selectedModel = state.MODELS.find((m) => m.id === storedModelId()) || state.MODELS[0];
19023
- rememberModel(state.selectedModel);
19151
+ const currentLocked = state.selectedModel?.locked;
19152
+ if (currentLocked) {
19153
+ const firstUnlocked = state.MODELS.find((m) => !m.locked);
19154
+ if (firstUnlocked) {
19155
+ state.selectedModel = firstUnlocked;
19156
+ rememberModel(state.selectedModel);
19157
+ }
19158
+ } else {
19159
+ state.selectedModel = state.MODELS.find((m) => m.id === storedModelId()) || state.MODELS.find((m) => !m.locked) || state.MODELS[0];
19160
+ rememberModel(state.selectedModel);
19161
+ }
19024
19162
  refreshModelMenu();
19025
19163
  }
19026
19164
  if (message.type === "git_state" && message.git) {
@@ -19649,7 +19787,7 @@
19649
19787
  return;
19650
19788
  }
19651
19789
  const item = event.target.closest(".lasso-prompt-model-item");
19652
- if (!item) return;
19790
+ if (!item || item.classList.contains("locked")) return;
19653
19791
  const found = state.MODELS.find((m) => m.id === item.dataset.model);
19654
19792
  if (!found) return;
19655
19793
  state.selectedModel = found;
@@ -19889,6 +20027,7 @@
19889
20027
  if (key === "claude-code") return "Claude Code";
19890
20028
  if (key === "codex") return "Codex";
19891
20029
  if (key === "opencode") return "OpenCode";
20030
+ if (key === "cursor") return "Cursor";
19892
20031
  return key;
19893
20032
  }
19894
20033
  function modelItem(model) {
@@ -19896,7 +20035,15 @@
19896
20035
  item.type = "button";
19897
20036
  item.className = "lasso-prompt-model-item";
19898
20037
  item.dataset.model = model.id;
19899
- item.innerHTML = `${providerIcon(model.provider)}<span>${model.label}</span><svg class="lasso-prompt-model-check" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>`;
20038
+ if (model.locked) {
20039
+ item.classList.add("locked");
20040
+ item.disabled = true;
20041
+ item.setAttribute("aria-disabled", "true");
20042
+ item.setAttribute("title", model.lockedReason || "This model is locked");
20043
+ item.innerHTML = `${providerIcon(model.provider)}<span>${model.label}</span><svg class="lasso-prompt-model-lock" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
20044
+ } else {
20045
+ item.innerHTML = `${providerIcon(model.provider)}<span>${model.label}</span><svg class="lasso-prompt-model-check" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>`;
20046
+ }
19900
20047
  return item;
19901
20048
  }
19902
20049
  function syncModelMenu2() {
@@ -21640,6 +21787,254 @@
21640
21787
  return panelEl ? !panelEl.hidden : false;
21641
21788
  }
21642
21789
 
21790
+ // src/overlay/commandbar/commandbar.ts
21791
+ var commandBarEl = null;
21792
+ var commandInput = null;
21793
+ var commandSubmit = null;
21794
+ var commandVoice = null;
21795
+ var commandClose = null;
21796
+ var commandModelBtn = null;
21797
+ var commandModelMenu = null;
21798
+ var commandModelIcon = null;
21799
+ var commandModelLabel = null;
21800
+ var isOneShotMode = false;
21801
+ function buildCommandBar() {
21802
+ const dom = getDOM();
21803
+ const el = document.createElement("div");
21804
+ el.className = "lasso-command-sidebar";
21805
+ el.hidden = true;
21806
+ el.innerHTML = `
21807
+ <div class="lasso-command-sidebar-header">
21808
+ <div class="lasso-command-brand">
21809
+ <img class="lasso-command-brand-logo" src="${LASSO_ICON_DATA_URL}" alt="Lasso" />
21810
+ <span class="lasso-command-brand-title">Build with AI</span>
21811
+ </div>
21812
+ <button class="lasso-command-close" type="button" aria-label="Close">
21813
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
21814
+ <path d="M18 6L6 18M6 6l12 12"/>
21815
+ </svg>
21816
+ </button>
21817
+ </div>
21818
+
21819
+ <div class="lasso-command-sidebar-body">
21820
+ <div class="lasso-command-model-select">
21821
+ <button class="lasso-command-model-btn" type="button" aria-label="Select model">
21822
+ <span class="lasso-command-model-icon"></span>
21823
+ <span class="lasso-command-model-label"></span>
21824
+ <svg class="lasso-command-model-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
21825
+ <path d="M6 9l6 6 6-6"/>
21826
+ </svg>
21827
+ </button>
21828
+ <div class="lasso-command-model-menu" hidden></div>
21829
+ </div>
21830
+
21831
+ <div class="lasso-command-input-wrapper">
21832
+ <textarea
21833
+ class="lasso-command-input"
21834
+ placeholder="Describe what you want to build...&#10;&#10;Examples:&#10;\u2022 Build a settings page with profile, notifications and security sections&#10;\u2022 Add authentication with login and signup&#10;\u2022 Create a dashboard with customer table and analytics"
21835
+ rows="6"
21836
+ ></textarea>
21837
+ </div>
21838
+
21839
+ <div class="lasso-command-actions">
21840
+ <button class="lasso-command-voice" type="button" aria-label="Voice input" title="Voice input (speech-to-text)">
21841
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
21842
+ <rect x="9" y="3" width="6" height="11" rx="3"/>
21843
+ <path d="M5 11a7 7 0 0 0 14 0M12 18v3M8 21h8"/>
21844
+ </svg>
21845
+ </button>
21846
+
21847
+ <button class="lasso-command-submit primary" type="button" aria-label="Build">
21848
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
21849
+ <path d="M5 12h14M12 5l7 7-7 7"/>
21850
+ </svg>
21851
+ <span>Build</span>
21852
+ </button>
21853
+ </div>
21854
+
21855
+ <div class="lasso-command-shortcuts">
21856
+ <span class="lasso-command-shortcut">
21857
+ <kbd>\u2318</kbd><kbd>K</kbd>
21858
+ <span>Toggle one-shot mode</span>
21859
+ </span>
21860
+ <span class="lasso-command-shortcut">
21861
+ <kbd>\u2318</kbd><kbd>\u21B5</kbd>
21862
+ <span>Submit</span>
21863
+ </span>
21864
+ </div>
21865
+ </div>
21866
+ `;
21867
+ dom.shadow.appendChild(el);
21868
+ commandBarEl = el;
21869
+ commandInput = el.querySelector(".lasso-command-input");
21870
+ commandSubmit = el.querySelector(".lasso-command-submit");
21871
+ commandVoice = el.querySelector(".lasso-command-voice");
21872
+ commandClose = el.querySelector(".lasso-command-close");
21873
+ commandModelBtn = el.querySelector(".lasso-command-model-btn");
21874
+ commandModelMenu = el.querySelector(".lasso-command-model-menu");
21875
+ commandModelIcon = el.querySelector(".lasso-command-model-icon");
21876
+ commandModelLabel = el.querySelector(".lasso-command-model-label");
21877
+ commandModelBtn.addEventListener("click", (event) => {
21878
+ event.preventDefault();
21879
+ event.stopPropagation();
21880
+ if (commandModelMenu) commandModelMenu.hidden = !commandModelMenu.hidden;
21881
+ });
21882
+ commandModelMenu.addEventListener("click", (event) => {
21883
+ event.stopPropagation();
21884
+ const item = event.target.closest(".lasso-command-model-item");
21885
+ if (!item) return;
21886
+ const found = state.MODELS.find((m) => m.id === item.dataset.model);
21887
+ if (!found) return;
21888
+ state.selectedModel = found;
21889
+ if (commandModelIcon && commandModelLabel) {
21890
+ commandModelIcon.innerHTML = providerIcon(found.provider, true);
21891
+ commandModelLabel.textContent = found.label;
21892
+ }
21893
+ if (commandModelMenu) commandModelMenu.hidden = true;
21894
+ });
21895
+ document.addEventListener("click", (event) => {
21896
+ if (!commandModelMenu || commandModelMenu.hidden) return;
21897
+ const path = event.composedPath ? event.composedPath() : [];
21898
+ if (path.includes(commandModelBtn) || path.includes(commandModelMenu)) return;
21899
+ const target = event.target;
21900
+ if (commandModelBtn?.contains(target) || commandModelMenu?.contains(target)) return;
21901
+ commandModelMenu.hidden = true;
21902
+ });
21903
+ commandClose.addEventListener("click", (event) => {
21904
+ event.preventDefault();
21905
+ event.stopPropagation();
21906
+ closeCommandBar();
21907
+ });
21908
+ commandVoice.addEventListener("click", async (event) => {
21909
+ event.preventDefault();
21910
+ event.stopPropagation();
21911
+ if (isRecordingVoice()) {
21912
+ commandVoice.classList.remove("recording");
21913
+ commandVoice.classList.add("transcribing");
21914
+ commandVoice.title = "Transcribing\u2026";
21915
+ try {
21916
+ const result = await stopVoiceRecording();
21917
+ if (result.text && commandInput) {
21918
+ const prev = commandInput.value.trim();
21919
+ commandInput.value = prev ? `${prev} ${result.text}` : result.text;
21920
+ commandInput.focus();
21921
+ autoResizeTextarea(commandInput);
21922
+ showActivity("Transcribed", "#81c995");
21923
+ } else {
21924
+ showActivity("No speech detected.", "#fdd663");
21925
+ }
21926
+ } catch (err) {
21927
+ showActivity(err?.message || "Transcription failed", "#f28b82");
21928
+ } finally {
21929
+ commandVoice.classList.remove("transcribing");
21930
+ commandVoice.title = "Voice input";
21931
+ }
21932
+ } else {
21933
+ try {
21934
+ await startVoiceRecording();
21935
+ commandVoice.classList.add("recording");
21936
+ commandVoice.title = "Recording\u2026 Click again to stop";
21937
+ showActivity("Listening\u2026 speak now", "#ea4335");
21938
+ } catch (err) {
21939
+ showActivity(err?.message || "Microphone access denied", "#f28b82");
21940
+ }
21941
+ }
21942
+ });
21943
+ commandInput.addEventListener("keydown", (event) => {
21944
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
21945
+ event.preventDefault();
21946
+ handleSubmit();
21947
+ }
21948
+ });
21949
+ commandSubmit.addEventListener("click", (event) => {
21950
+ event.preventDefault();
21951
+ event.stopPropagation();
21952
+ handleSubmit();
21953
+ });
21954
+ commandInput.addEventListener("input", () => {
21955
+ if (commandInput) autoResizeTextarea(commandInput);
21956
+ });
21957
+ commandInput.addEventListener("keydown", (event) => {
21958
+ if (event.key === "Escape") {
21959
+ event.preventDefault();
21960
+ closeCommandBar();
21961
+ }
21962
+ });
21963
+ }
21964
+ function autoResizeTextarea(textarea) {
21965
+ textarea.style.height = "auto";
21966
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
21967
+ }
21968
+ function openCommandBar() {
21969
+ if (!commandBarEl) return;
21970
+ commandBarEl.hidden = false;
21971
+ isOneShotMode = true;
21972
+ if (commandInput) {
21973
+ commandInput.value = "";
21974
+ commandInput.focus();
21975
+ autoResizeTextarea(commandInput);
21976
+ }
21977
+ if (commandModelIcon && commandModelLabel && state.selectedModel) {
21978
+ commandModelIcon.innerHTML = providerIcon(state.selectedModel.provider, true);
21979
+ commandModelLabel.textContent = state.selectedModel.label;
21980
+ }
21981
+ populateModelMenu();
21982
+ }
21983
+ function closeCommandBar() {
21984
+ if (!commandBarEl) return;
21985
+ commandBarEl.hidden = true;
21986
+ isOneShotMode = false;
21987
+ if (commandInput) {
21988
+ commandInput.value = "";
21989
+ }
21990
+ }
21991
+ function isCommandBarOpen() {
21992
+ return isOneShotMode;
21993
+ }
21994
+ function populateModelMenu() {
21995
+ if (!commandModelMenu) return;
21996
+ commandModelMenu.innerHTML = "";
21997
+ const providers = ["all", ...Array.from(new Set(state.MODELS.map((m) => m.provider)))];
21998
+ for (const provider of providers) {
21999
+ const section = document.createElement("div");
22000
+ section.className = "lasso-command-model-section";
22001
+ if (provider !== "all") {
22002
+ const header = document.createElement("div");
22003
+ header.className = "lasso-command-model-section-header";
22004
+ header.textContent = provider === "anthropic" ? "Anthropic" : provider === "openai" ? "OpenAI" : provider === "google" ? "Google" : provider === "ollama" ? "Ollama" : provider === "cli" ? "Local Agents" : provider;
22005
+ section.appendChild(header);
22006
+ }
22007
+ const models = provider === "all" ? state.MODELS : state.MODELS.filter((m) => m.provider === provider);
22008
+ for (const model of models) {
22009
+ const item = document.createElement("button");
22010
+ item.type = "button";
22011
+ item.className = "lasso-command-model-item";
22012
+ item.dataset.model = model.id;
22013
+ item.innerHTML = `${providerIcon(model.provider)}<span>${model.label}</span>`;
22014
+ if (model.id === state.selectedModel.id) {
22015
+ item.classList.add("selected");
22016
+ }
22017
+ section.appendChild(item);
22018
+ }
22019
+ commandModelMenu.appendChild(section);
22020
+ }
22021
+ }
22022
+ async function handleSubmit() {
22023
+ if (!commandInput) return;
22024
+ const prompt = commandInput.value.trim();
22025
+ if (!prompt) return;
22026
+ closeCommandBar();
22027
+ if (state.bridgeSocket?.readyState === WebSocket.OPEN) {
22028
+ state.bridgeSocket.send(JSON.stringify({
22029
+ type: "oneshot",
22030
+ prompt,
22031
+ scope: "project",
22032
+ model: state.selectedModel.id,
22033
+ provider: state.selectedModel.provider === "cli" ? "cli" : void 0
22034
+ }));
22035
+ }
22036
+ }
22037
+
21643
22038
  // src/overlay/toolbar/toolbar.ts
21644
22039
  var COMMENT_CURSOR_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="32" viewBox="0 0 28 32"><defs><filter id="s" x="-30%" y="-30%" width="160%" height="160%"><feDropShadow dx="0" dy="1" stdDeviation="1.5" flood-color="rgba(0,0,0,0.5)"/></filter></defs><g filter="url(#s)"><path d="M4 2h16a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H10l-6 6V5a3 3 0 0 1 3-3z" fill="#7C3AED"/><path d="M4 2h16a3 3 0 0 1 3 3v13a3 3 0 0 1-3 3H10l-6 6V5a3 3 0 0 1 3-3z" fill="none" stroke="rgba(255,255,255,0.25)" stroke-width="0.8"/><line x1="12" y1="7" x2="12" y2="15" stroke="white" stroke-width="1.8" stroke-linecap="round"/><line x1="8" y1="11" x2="16" y2="11" stroke="white" stroke-width="1.8" stroke-linecap="round"/></g></svg>`;
21645
22040
  var COMMENT_CURSOR_URL = `url("data:image/svg+xml,${encodeURIComponent(COMMENT_CURSOR_SVG)}") 4 2, crosshair`;
@@ -21767,6 +22162,14 @@
21767
22162
  </svg>
21768
22163
  </button>
21769
22164
 
22165
+ <button class="lasso-tool-btn command-tool" type="button" aria-label="Command bar" title="Command bar (\u2318K)">
22166
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
22167
+ <path d="M12 2v20M2 12h20"/>
22168
+ <circle cx="12" cy="12" r="9"/>
22169
+ </svg>
22170
+ <span class="lasso-tool-label">Command</span>
22171
+ </button>
22172
+
21770
22173
  <div class="lasso-presence" style="display: none;">
21771
22174
  <div class="lasso-presence-avatars"></div>
21772
22175
  <span class="lasso-presence-count"></span>
@@ -21871,6 +22274,7 @@
21871
22274
  const notepadBtn = toolbar.querySelector(".notepad-tool");
21872
22275
  const clipboardBtn = toolbar.querySelector(".clipboard-tool");
21873
22276
  const voiceBtn = toolbar.querySelector(".voice-tool");
22277
+ const commandBtn = toolbar.querySelector(".command-tool");
21874
22278
  const dismissBtn = toolbar.querySelector(".lasso-toolbar-dismiss");
21875
22279
  const newPageBtn = miniToolbar.querySelector(".lasso-new-page-btn");
21876
22280
  newPageBtn.addEventListener("click", (event) => {
@@ -21934,6 +22338,11 @@
21934
22338
  e.preventDefault();
21935
22339
  if (state.voiceOn) setVoiceMuted(!state.voiceMuted);
21936
22340
  });
22341
+ commandBtn.addEventListener("click", (event) => {
22342
+ event.preventDefault();
22343
+ event.stopPropagation();
22344
+ openCommandBar();
22345
+ });
21937
22346
  dismissBtn.addEventListener("click", () => {
21938
22347
  toolbar.classList.add("dismissed");
21939
22348
  miniToolbar.classList.add("dismissed");
@@ -21953,190 +22362,6 @@
21953
22362
  return { toolbar, voiceBar };
21954
22363
  }
21955
22364
 
21956
- // src/overlay/commandbar/commandbar.ts
21957
- var commandBarEl = null;
21958
- var commandInput = null;
21959
- var commandBackdrop = null;
21960
- var commandSubmit = null;
21961
- var commandVoice = null;
21962
- var commandClose = null;
21963
- var isOpen = false;
21964
- function buildCommandBar() {
21965
- const dom = getDOM();
21966
- const backdrop = document.createElement("div");
21967
- backdrop.className = "lasso-command-backdrop";
21968
- backdrop.hidden = true;
21969
- backdrop.innerHTML = ``;
21970
- dom.shadow.appendChild(backdrop);
21971
- commandBackdrop = backdrop;
21972
- const el = document.createElement("div");
21973
- el.className = "lasso-command-bar";
21974
- el.hidden = true;
21975
- el.innerHTML = `
21976
- <div class="lasso-command-card">
21977
- <div class="lasso-command-header">
21978
- <div class="lasso-command-brand">
21979
- <img class="lasso-command-brand-logo" src="${LASSO_ICON_DATA_URL}" alt="Lasso" />
21980
- <span class="lasso-command-brand-title">What do you want to build?</span>
21981
- </div>
21982
- <button class="lasso-command-close" type="button" aria-label="Close">
21983
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
21984
- <path d="M18 6L6 18M6 6l12 12"/>
21985
- </svg>
21986
- </button>
21987
- </div>
21988
-
21989
- <div class="lasso-command-body">
21990
- <textarea
21991
- class="lasso-command-input"
21992
- placeholder="Describe what you want to build...&#10;&#10;Examples:&#10;\u2022 Build a settings page with profile, notifications and security sections&#10;\u2022 Add authentication with login and signup&#10;\u2022 Create a dashboard with customer table and analytics"
21993
- rows="3"
21994
- ></textarea>
21995
-
21996
- <div class="lasso-command-actions">
21997
- <div class="lasso-command-icon-group">
21998
- <button class="lasso-command-voice" type="button" aria-label="Voice input" title="Voice input (speech-to-text)">
21999
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
22000
- <rect x="9" y="3" width="6" height="11" rx="3"/>
22001
- <path d="M5 11a7 7 0 0 0 14 0M12 18v3M8 21h8"/>
22002
- </svg>
22003
- </button>
22004
- </div>
22005
-
22006
- <button class="lasso-command-submit primary" type="button" aria-label="Build">
22007
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
22008
- <path d="M5 12h14M12 5l7 7-7 7"/>
22009
- </svg>
22010
- <span>Build</span>
22011
- </button>
22012
- </div>
22013
-
22014
- <div class="lasso-command-shortcuts">
22015
- <span class="lasso-command-shortcut">
22016
- <kbd>\u2318</kbd><kbd>K</kbd>
22017
- <span>Open command bar</span>
22018
- </span>
22019
- <span class="lasso-command-shortcut">
22020
- <kbd>\u2318</kbd><kbd>\u21B5</kbd>
22021
- <span>Submit</span>
22022
- </span>
22023
- <span class="lasso-command-shortcut">
22024
- <kbd>Esc</kbd>
22025
- <span>Close</span>
22026
- </span>
22027
- </div>
22028
- </div>
22029
- </div>
22030
- `;
22031
- dom.shadow.appendChild(el);
22032
- commandBarEl = el;
22033
- commandInput = el.querySelector(".lasso-command-input");
22034
- commandSubmit = el.querySelector(".lasso-command-submit");
22035
- commandVoice = el.querySelector(".lasso-command-voice");
22036
- commandClose = el.querySelector(".lasso-command-close");
22037
- backdrop.addEventListener("click", closeCommandBar);
22038
- commandClose.addEventListener("click", (event) => {
22039
- event.preventDefault();
22040
- event.stopPropagation();
22041
- closeCommandBar();
22042
- });
22043
- commandVoice.addEventListener("click", async (event) => {
22044
- event.preventDefault();
22045
- event.stopPropagation();
22046
- if (isRecordingVoice()) {
22047
- commandVoice.classList.remove("recording");
22048
- commandVoice.classList.add("transcribing");
22049
- commandVoice.title = "Transcribing\u2026";
22050
- try {
22051
- const result = await stopVoiceRecording();
22052
- if (result.text && commandInput) {
22053
- const prev = commandInput.value.trim();
22054
- commandInput.value = prev ? `${prev} ${result.text}` : result.text;
22055
- commandInput.focus();
22056
- autoResizeTextarea(commandInput);
22057
- showActivity("Transcribed", "#81c995");
22058
- } else {
22059
- showActivity("No speech detected.", "#fdd663");
22060
- }
22061
- } catch (err) {
22062
- showActivity(err?.message || "Transcription failed", "#f28b82");
22063
- } finally {
22064
- commandVoice.classList.remove("transcribing");
22065
- commandVoice.title = "Voice input";
22066
- }
22067
- } else {
22068
- try {
22069
- await startVoiceRecording();
22070
- commandVoice.classList.add("recording");
22071
- commandVoice.title = "Recording\u2026 Click again to stop";
22072
- showActivity("Listening\u2026 speak now", "#ea4335");
22073
- } catch (err) {
22074
- showActivity(err?.message || "Microphone access denied", "#f28b82");
22075
- }
22076
- }
22077
- });
22078
- commandInput.addEventListener("keydown", (event) => {
22079
- if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
22080
- event.preventDefault();
22081
- handleSubmit();
22082
- }
22083
- });
22084
- commandSubmit.addEventListener("click", (event) => {
22085
- event.preventDefault();
22086
- event.stopPropagation();
22087
- handleSubmit();
22088
- });
22089
- commandInput.addEventListener("input", () => {
22090
- if (commandInput) autoResizeTextarea(commandInput);
22091
- });
22092
- commandInput.addEventListener("keydown", (event) => {
22093
- if (event.key === "Escape") {
22094
- event.preventDefault();
22095
- closeCommandBar();
22096
- }
22097
- });
22098
- }
22099
- function autoResizeTextarea(textarea) {
22100
- textarea.style.height = "auto";
22101
- textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
22102
- }
22103
- function openCommandBar() {
22104
- if (!commandBarEl || !commandBackdrop) return;
22105
- commandBarEl.hidden = false;
22106
- commandBackdrop.hidden = false;
22107
- isOpen = true;
22108
- if (commandInput) {
22109
- commandInput.value = "";
22110
- commandInput.focus();
22111
- autoResizeTextarea(commandInput);
22112
- }
22113
- }
22114
- function closeCommandBar() {
22115
- if (!commandBarEl || !commandBackdrop) return;
22116
- commandBarEl.hidden = true;
22117
- commandBackdrop.hidden = true;
22118
- isOpen = false;
22119
- if (commandInput) {
22120
- commandInput.value = "";
22121
- }
22122
- }
22123
- function isCommandBarOpen() {
22124
- return isOpen;
22125
- }
22126
- async function handleSubmit() {
22127
- if (!commandInput) return;
22128
- const prompt = commandInput.value.trim();
22129
- if (!prompt) return;
22130
- closeCommandBar();
22131
- if (state.bridgeSocket?.readyState === WebSocket.OPEN) {
22132
- state.bridgeSocket.send(JSON.stringify({
22133
- type: "oneshot",
22134
- prompt,
22135
- scope: "project"
22136
- }));
22137
- }
22138
- }
22139
-
22140
22365
  // src/overlay/index.ts
22141
22366
  console.log("[lasso] overlay initializing");
22142
22367
  function init() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Select any part of your running app, describe a change, and let AI edit the real source code.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli/index.js",